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