git/git.go (view raw)
1package git
2
3import (
4 "archive/tar"
5 "fmt"
6 "io"
7 "io/fs"
8 "path"
9 "sort"
10 "time"
11
12 "github.com/go-git/go-git/v5"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/object"
15)
16
17type GitRepo struct {
18 r *git.Repository
19 h plumbing.Hash
20}
21
22type TagList struct {
23 refs []*TagReference
24 r *git.Repository
25}
26
27// TagReference is used to list both tag and non-annotated tags.
28// Non-annotated tags should only contains a reference.
29// Annotated tags should contain its reference and its tag information.
30type TagReference struct {
31 ref *plumbing.Reference
32 tag *object.Tag
33}
34
35// CommitReference aggregate all the references for a given commit
36type CommitReference struct {
37 commit *object.Commit
38 refs []*plumbing.Reference
39}
40
41// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
42// to tar WriteHeader
43type infoWrapper struct {
44 name string
45 size int64
46 mode fs.FileMode
47 modTime time.Time
48 isDir bool
49}
50
51func (self *TagList) Len() int {
52 return len(self.refs)
53}
54
55func (self *TagList) Swap(i, j int) {
56 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
57}
58
59// sorting tags in reverse chronological order
60func (self *TagList) Less(i, j int) bool {
61 var dateI time.Time
62 var dateJ time.Time
63
64 if self.refs[i].tag != nil {
65 dateI = self.refs[i].tag.Tagger.When
66 } else {
67 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
68 if err != nil {
69 dateI = time.Now()
70 } else {
71 dateI = c.Committer.When
72 }
73 }
74
75 if self.refs[j].tag != nil {
76 dateJ = self.refs[j].tag.Tagger.When
77 } else {
78 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
79 if err != nil {
80 dateJ = time.Now()
81 } else {
82 dateJ = c.Committer.When
83 }
84 }
85
86 return dateI.After(dateJ)
87}
88
89func Open(path string, ref string) (*GitRepo, error) {
90 var err error
91 g := GitRepo{}
92 g.r, err = git.PlainOpen(path)
93 if err != nil {
94 return nil, fmt.Errorf("opening %s: %w", path, err)
95 }
96
97 if ref == "" {
98 head, err := g.r.Head()
99 if err != nil {
100 return nil, fmt.Errorf("getting head of %s: %w", path, err)
101 }
102 g.h = head.Hash()
103 } else {
104 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
105 if err != nil {
106 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
107 }
108 g.h = *hash
109 }
110 return &g, nil
111}
112
113func (g *GitRepo) Commits() ([]*CommitReference, error) {
114 ci, err := g.r.Log(&git.LogOptions{From: g.h})
115 if err != nil {
116 return nil, fmt.Errorf("commits from ref: %w", err)
117 }
118
119 commitRefs := []*CommitReference{}
120 ci.ForEach(func(c *object.Commit) error {
121 commitRefs = append(commitRefs, &CommitReference{commit: c})
122 return nil
123 })
124
125 // new we fetch for possible tags for each commit
126 iter, err := g.r.References()
127 if err != nil {
128 return nil, err
129 }
130
131 if err := iter.ForEach(func(ref *plumbing.Reference) error {
132 for _, c := range commitRefs {
133 obj, err := g.r.TagObject(ref.Hash())
134 switch err {
135 case nil:
136 if obj.Target == c.commit.Hash {
137 c.AddReference(ref)
138 }
139 case plumbing.ErrObjectNotFound:
140 if c.commit.Hash == ref.Hash() {
141 c.AddReference(ref)
142 }
143 default:
144 return err
145 }
146 }
147 return nil
148 }); err != nil {
149 return nil, err
150 }
151
152 return commitRefs, nil
153}
154
155func (g *GitRepo) LastCommit() (*CommitReference, error) {
156 c, err := g.r.CommitObject(g.h)
157 if err != nil {
158 return nil, fmt.Errorf("last commit: %w", err)
159 }
160
161 iter, err := g.r.Tags()
162 if err != nil {
163 return nil, err
164 }
165
166 commitRef := &CommitReference{commit: c}
167 if err := iter.ForEach(func(ref *plumbing.Reference) error {
168 obj, err := g.r.TagObject(ref.Hash())
169 switch err {
170 case nil:
171 if obj.Target == commitRef.commit.Hash {
172 commitRef.AddReference(ref)
173 }
174 case plumbing.ErrObjectNotFound:
175 if commitRef.commit.Hash == ref.Hash() {
176 commitRef.AddReference(ref)
177 }
178 default:
179 return err
180 }
181
182 return nil
183 }); err != nil {
184 return nil, err
185 }
186
187 return commitRef, nil
188}
189
190func (g *GitRepo) FileContent(path string) (string, error) {
191 c, err := g.r.CommitObject(g.h)
192 if err != nil {
193 return "", fmt.Errorf("commit object: %w", err)
194 }
195
196 tree, err := c.Tree()
197 if err != nil {
198 return "", fmt.Errorf("file tree: %w", err)
199 }
200
201 file, err := tree.File(path)
202 if err != nil {
203 return "", err
204 }
205
206 isbin, _ := file.IsBinary()
207
208 if !isbin {
209 return file.Contents()
210 } else {
211 return "Not displaying binary file", nil
212 }
213}
214
215func (g *GitRepo) Tags() ([]*TagReference, error) {
216 iter, err := g.r.Tags()
217 if err != nil {
218 return nil, fmt.Errorf("tag objects: %w", err)
219 }
220
221 tags := make([]*TagReference, 0)
222
223 if err := iter.ForEach(func(ref *plumbing.Reference) error {
224 obj, err := g.r.TagObject(ref.Hash())
225 switch err {
226 case nil:
227 tags = append(tags, &TagReference{
228 ref: ref,
229 tag: obj,
230 })
231 case plumbing.ErrObjectNotFound:
232 tags = append(tags, &TagReference{
233 ref: ref,
234 })
235 default:
236 return err
237 }
238 return nil
239 }); err != nil {
240 return nil, err
241 }
242
243 tagList := &TagList{r: g.r, refs: tags}
244 sort.Sort(tagList)
245 return tags, nil
246}
247
248func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
249 bi, err := g.r.Branches()
250 if err != nil {
251 return nil, fmt.Errorf("branchs: %w", err)
252 }
253
254 branches := []*plumbing.Reference{}
255
256 _ = bi.ForEach(func(ref *plumbing.Reference) error {
257 branches = append(branches, ref)
258 return nil
259 })
260
261 return branches, nil
262}
263
264func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
265 for _, b := range branches {
266 _, err := g.r.ResolveRevision(plumbing.Revision(b))
267 if err == nil {
268 return b, nil
269 }
270 }
271 return "", fmt.Errorf("unable to find main branch")
272}
273
274// WriteTar writes itself from a tree into a binary tar file format.
275// prefix is root folder to be appended.
276func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
277 tw := tar.NewWriter(w)
278 defer tw.Close()
279
280 c, err := g.r.CommitObject(g.h)
281 if err != nil {
282 return fmt.Errorf("commit object: %w", err)
283 }
284
285 tree, err := c.Tree()
286 if err != nil {
287 return err
288 }
289
290 walker := object.NewTreeWalker(tree, true, nil)
291 defer walker.Close()
292
293 name, entry, err := walker.Next()
294 for ; err == nil; name, entry, err = walker.Next() {
295 info, err := newInfoWrapper(name, prefix, &entry, tree)
296 if err != nil {
297 return err
298 }
299
300 header, err := tar.FileInfoHeader(info, "")
301 if err != nil {
302 return err
303 }
304
305 err = tw.WriteHeader(header)
306 if err != nil {
307 return err
308 }
309
310 if !info.IsDir() {
311 file, err := tree.File(name)
312 if err != nil {
313 return err
314 }
315
316 reader, err := file.Blob.Reader()
317 if err != nil {
318 return err
319 }
320
321 _, err = io.Copy(tw, reader)
322 if err != nil {
323 reader.Close()
324 return err
325 }
326 reader.Close()
327 }
328 }
329
330 return nil
331}
332
333func newInfoWrapper(
334 name string,
335 prefix string,
336 entry *object.TreeEntry,
337 tree *object.Tree,
338) (*infoWrapper, error) {
339 var (
340 size int64
341 mode fs.FileMode
342 isDir bool
343 )
344
345 if entry.Mode.IsFile() {
346 file, err := tree.TreeEntryFile(entry)
347 if err != nil {
348 return nil, err
349 }
350 mode = fs.FileMode(file.Mode)
351
352 size, err = tree.Size(name)
353 if err != nil {
354 return nil, err
355 }
356 } else {
357 isDir = true
358 mode = fs.ModeDir | fs.ModePerm
359 }
360
361 fullname := path.Join(prefix, name)
362 return &infoWrapper{
363 name: fullname,
364 size: size,
365 mode: mode,
366 modTime: time.Unix(0, 0),
367 isDir: isDir,
368 }, nil
369}
370
371func (i *infoWrapper) Name() string {
372 return i.name
373}
374
375func (i *infoWrapper) Size() int64 {
376 return i.size
377}
378
379func (i *infoWrapper) Mode() fs.FileMode {
380 return i.mode
381}
382
383func (i *infoWrapper) ModTime() time.Time {
384 return i.modTime
385}
386
387func (i *infoWrapper) IsDir() bool {
388 return i.isDir
389}
390
391func (i *infoWrapper) Sys() any {
392 return nil
393}
394
395func (t *TagReference) Name() string {
396 return t.ref.Name().Short()
397}
398
399func (t *TagReference) Message() string {
400 if t.tag != nil {
401 return t.tag.Message
402 }
403 return ""
404}
405
406func (c *CommitReference) Commit() *object.Commit {
407 return c.commit
408}
409
410func (c *CommitReference) HasReference() bool {
411 return len(c.refs) > 0
412}
413
414func (c *CommitReference) References() []*plumbing.Reference {
415 return c.refs
416}
417
418func (c *CommitReference) AddReference(ref *plumbing.Reference) {
419 c.refs = append(c.refs, ref)
420}