419 lines
8.0 kB
1
package git
2
3
import (
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
22
var (
23
commitCache *ristretto.Cache
24
cacheMu sync.RWMutex
25
)
26
27
func init() {
28
cache, _ := ristretto.NewCache(&ristretto.Config{
29
NumCounters: 1e7,
30
MaxCost: 1 << 30,
31
BufferItems: 64,
32
})
33
commitCache = cache
34
}
35
36
var (
37
ErrBinaryFile = fmt.Errorf("binary file")
38
)
39
40
type GitRepo struct {
41
path string
42
r *git.Repository
43
h plumbing.Hash
44
}
45
46
type 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.
54
type 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
61
type infoWrapper struct {
62
name string
63
size int64
64
mode fs.FileMode
65
modTime time.Time
66
isDir bool
67
}
68
69
func (self *TagList) Len() int {
70
return len(self.refs)
71
}
72
73
func (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
78
func (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
107
func 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
131
func (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
146
func (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
154
func (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
179
func (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
212
func (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
228
func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
229
branches = append(branches, []string{
230
"main",
231
"master",
232
"trunk",
233
}...)
234
for _, b := range branches {
235
_, err := g.r.ResolveRevision(plumbing.Revision(b))
236
if err == nil {
237
return b, nil
238
}
239
}
240
return "", fmt.Errorf("unable to find main branch")
241
}
242
243
// WriteTar writes itself from a tree into a binary tar file format.
244
// prefix is root folder to be appended.
245
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
246
tw := tar.NewWriter(w)
247
defer tw.Close()
248
249
c, err := g.r.CommitObject(g.h)
250
if err != nil {
251
return fmt.Errorf("commit object: %w", err)
252
}
253
254
tree, err := c.Tree()
255
if err != nil {
256
return err
257
}
258
259
walker := object.NewTreeWalker(tree, true, nil)
260
defer walker.Close()
261
262
name, entry, err := walker.Next()
263
for ; err == nil; name, entry, err = walker.Next() {
264
info, err := newInfoWrapper(name, prefix, &entry, tree)
265
if err != nil {
266
return err
267
}
268
269
header, err := tar.FileInfoHeader(info, "")
270
if err != nil {
271
return err
272
}
273
274
err = tw.WriteHeader(header)
275
if err != nil {
276
return err
277
}
278
279
if !info.IsDir() {
280
file, err := tree.File(name)
281
if err != nil {
282
return err
283
}
284
285
reader, err := file.Blob.Reader()
286
if err != nil {
287
return err
288
}
289
290
_, err = io.Copy(tw, reader)
291
if err != nil {
292
reader.Close()
293
return err
294
}
295
reader.Close()
296
}
297
}
298
299
return nil
300
}
301
302
func (g *GitRepo) LastCommitForPath(path string) (*object.Commit, error) {
303
cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path)
304
cacheMu.RLock()
305
if commit, found := commitCache.Get(cacheKey); found {
306
cacheMu.RUnlock()
307
return commit.(*object.Commit), nil
308
}
309
cacheMu.RUnlock()
310
311
cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path)
312
313
var out bytes.Buffer
314
cmd.Stdout = &out
315
cmd.Stderr = &out
316
317
if err := cmd.Run(); err != nil {
318
return nil, fmt.Errorf("failed to get commit hash: %w", err)
319
}
320
321
commitHash := strings.TrimSpace(out.String())
322
if commitHash == "" {
323
return nil, fmt.Errorf("no commits found for path: %s", path)
324
}
325
326
hash := plumbing.NewHash(commitHash)
327
328
commit, err := g.r.CommitObject(hash)
329
if err != nil {
330
return nil, err
331
}
332
333
cacheMu.Lock()
334
commitCache.Set(cacheKey, commit, 1)
335
cacheMu.Unlock()
336
337
return commit, nil
338
}
339
340
func newInfoWrapper(
341
name string,
342
prefix string,
343
entry *object.TreeEntry,
344
tree *object.Tree,
345
) (*infoWrapper, error) {
346
var (
347
size int64
348
mode fs.FileMode
349
isDir bool
350
)
351
352
if entry.Mode.IsFile() {
353
file, err := tree.TreeEntryFile(entry)
354
if err != nil {
355
return nil, err
356
}
357
mode = fs.FileMode(file.Mode)
358
359
size, err = tree.Size(name)
360
if err != nil {
361
return nil, err
362
}
363
} else {
364
isDir = true
365
mode = fs.ModeDir | fs.ModePerm
366
}
367
368
fullname := path.Join(prefix, name)
369
return &infoWrapper{
370
name: fullname,
371
size: size,
372
mode: mode,
373
modTime: time.Unix(0, 0),
374
isDir: isDir,
375
}, nil
376
}
377
378
func (i *infoWrapper) Name() string {
379
return i.name
380
}
381
382
func (i *infoWrapper) Size() int64 {
383
return i.size
384
}
385
386
func (i *infoWrapper) Mode() fs.FileMode {
387
return i.mode
388
}
389
390
func (i *infoWrapper) ModTime() time.Time {
391
return i.modTime
392
}
393
394
func (i *infoWrapper) IsDir() bool {
395
return i.isDir
396
}
397
398
func (i *infoWrapper) Sys() any {
399
return nil
400
}
401
402
func (t *TagReference) Name() string {
403
return t.ref.Name().Short()
404
}
405
406
func (t *TagReference) Message() string {
407
if t.tag != nil {
408
return t.tag.Message
409
}
410
return ""
411
}
412
413
func (t *TagReference) TagObject() *object.Tag {
414
return t.tag
415
}
416
417
func (t *TagReference) Hash() plumbing.Hash {
418
return t.ref.Hash()
419
}
420