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