gitview/internal/git/git.go
internal/git/git.goRaw
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 // Repositories may use a default branch other than main or master.
275 head, err := g.r.Head()
276 if err == nil && head.Name().IsBranch() {
277 return head.Name().Short(), nil
278 }
279 return "HEAD", nil
280}
281
282// WriteTar writes itself from a tree into a binary tar file format.
283// prefix is root folder to be appended.
284func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
285 tw := tar.NewWriter(w)
286 defer func() { _ = tw.Close() }()
287
288 c, err := g.r.CommitObject(g.h)
289 if err != nil {
290 return fmt.Errorf("commit object: %w", err)
291 }
292
293 tree, err := c.Tree()
294 if err != nil {
295 return err
296 }
297
298 walker := object.NewTreeWalker(tree, true, nil)
299 defer walker.Close()
300
301 name, entry, err := walker.Next()
302 for ; err == nil; name, entry, err = walker.Next() {
303 info, err := newInfoWrapper(name, prefix, &entry, tree)
304 if err != nil {
305 return err
306 }
307
308 header, err := tar.FileInfoHeader(info, "")
309 if err != nil {
310 return err
311 }
312
313 err = tw.WriteHeader(header)
314 if err != nil {
315 return err
316 }
317
318 if !info.IsDir() {
319 file, err := tree.File(name)
320 if err != nil {
321 return err
322 }
323
324 reader, err := file.Reader()
325 if err != nil {
326 return err
327 }
328
329 _, err = io.Copy(tw, reader)
330 if err != nil {
331 _ = reader.Close()
332 return err
333 }
334 _ = reader.Close()
335 }
336 }
337
338 return nil
339}
340
341func newInfoWrapper(
342 name string,
343 prefix string,
344 entry *object.TreeEntry,
345 tree *object.Tree,
346) (*infoWrapper, error) {
347 var (
348 size int64
349 mode fs.FileMode
350 isDir bool
351 )
352
353 if entry.Mode.IsFile() {
354 file, err := tree.TreeEntryFile(entry)
355 if err != nil {
356 return nil, err
357 }
358 mode = fs.FileMode(file.Mode)
359
360 size, err = tree.Size(name)
361 if err != nil {
362 return nil, err
363 }
364 } else {
365 isDir = true
366 mode = fs.ModeDir | fs.ModePerm
367 }
368
369 fullname := path.Join(prefix, name)
370 return &infoWrapper{
371 name: fullname,
372 size: size,
373 mode: mode,
374 modTime: time.Unix(0, 0),
375 isDir: isDir,
376 }, nil
377}
378
379func (i *infoWrapper) Name() string {
380 return i.name
381}
382
383func (i *infoWrapper) Size() int64 {
384 return i.size
385}
386
387func (i *infoWrapper) Mode() fs.FileMode {
388 return i.mode
389}
390
391func (i *infoWrapper) ModTime() time.Time {
392 return i.modTime
393}
394
395func (i *infoWrapper) IsDir() bool {
396 return i.isDir
397}
398
399func (i *infoWrapper) Sys() any {
400 return nil
401}
402
403func (t *TagReference) Name() string {
404 return t.ref.Name().Short()
405}
406
407func (t *TagReference) Message() string {
408 if t.tag != nil {
409 return t.tag.Message
410 }
411 return ""
412}
413
414func (c *CommitReference) Commit() *object.Commit {
415 return c.commit
416}
417
418func (c *CommitReference) HasReference() bool {
419 return len(c.refs) > 0
420}
421
422func (c *CommitReference) References() []*plumbing.Reference {
423 return c.refs
424}
425
426func (c *CommitReference) AddReference(ref *plumbing.Reference) {
427 c.refs = append(c.refs, ref)
428}