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