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