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