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