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