409 lines
7.7 kB
1
package git
2
3
import (
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
19
var (
20
commitCache *ristretto.Cache
21
cacheMu sync.RWMutex
22
)
23
24
func init() {
25
cache, _ := ristretto.NewCache(&ristretto.Config{
26
NumCounters: 1e7,
27
MaxCost: 1 << 30,
28
BufferItems: 64,
29
})
30
commitCache = cache
31
}
32
33
var (
34
ErrBinaryFile = fmt.Errorf("binary file")
35
)
36
37
type GitRepo struct {
38
r *git.Repository
39
h plumbing.Hash
40
}
41
42
type 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.
50
type 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
57
type infoWrapper struct {
58
name string
59
size int64
60
mode fs.FileMode
61
modTime time.Time
62
isDir bool
63
}
64
65
func (self *TagList) Len() int {
66
return len(self.refs)
67
}
68
69
func (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
74
func (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
103
func 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
127
func (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
142
func (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
150
func (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
175
func (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
208
func (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
224
func (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.
241
func (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
298
func (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
330
func 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
368
func (i *infoWrapper) Name() string {
369
return i.name
370
}
371
372
func (i *infoWrapper) Size() int64 {
373
return i.size
374
}
375
376
func (i *infoWrapper) Mode() fs.FileMode {
377
return i.mode
378
}
379
380
func (i *infoWrapper) ModTime() time.Time {
381
return i.modTime
382
}
383
384
func (i *infoWrapper) IsDir() bool {
385
return i.isDir
386
}
387
388
func (i *infoWrapper) Sys() any {
389
return nil
390
}
391
392
func (t *TagReference) Name() string {
393
return t.ref.Name().Short()
394
}
395
396
func (t *TagReference) Message() string {
397
if t.tag != nil {
398
return t.tag.Message
399
}
400
return ""
401
}
402
403
func (t *TagReference) TagObject() *object.Tag {
404
return t.tag
405
}
406
407
func (t *TagReference) Hash() plumbing.Hash {
408
return t.ref.Hash()
409
}
410