1package git
2
3import (
4 "archive/tar"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "path"
11 "sort"
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 MissingRefErr = errors.New("Reference not found")
21 TreeForFileErr = errors.New("Trying to get tree of a file")
22)
23
24type (
25 GitRepository struct {
26 path string
27 repository *git.Repository
28 ref plumbing.Hash
29 setRef bool
30 }
31 TagReference struct {
32 ref *plumbing.Reference
33 tag *object.Tag
34 }
35 infoWrapper struct {
36 name string
37 size int64
38 mode fs.FileMode
39 modTime time.Time
40 isDir bool
41 }
42 tagList struct {
43 refs []*TagReference
44 r *git.Repository
45 }
46)
47
48func OpenRepository(dir string) (*GitRepository, error) {
49 g := &GitRepository{
50 path: dir,
51 }
52
53 repo, err := git.PlainOpen(dir)
54 if err != nil {
55 return nil, err
56 }
57 g.repository = repo
58
59 return g, nil
60}
61
62func (g *GitRepository) SetRef(ref string) error {
63 if ref == "" {
64 head, err := g.repository.Head()
65 if err != nil {
66 return errors.Join(MissingRefErr, err)
67 }
68 g.ref = head.Hash()
69 } else {
70 hash, err := g.repository.ResolveRevision(plumbing.Revision(ref))
71 if err != nil {
72 return errors.Join(MissingRefErr, err)
73 }
74 g.ref = *hash
75 }
76 g.setRef = true
77 return nil
78}
79
80func (g *GitRepository) Path() string {
81 return g.path
82}
83
84func (g *GitRepository) LastCommit() (*object.Commit, error) {
85 err := g.validateRef()
86 if err != nil {
87 return nil, err
88 }
89
90 c, err := g.repository.CommitObject(g.ref)
91 if err != nil {
92 return nil, err
93 }
94 return c, nil
95}
96
97func (g *GitRepository) Commits(count int, from string) ([]*object.Commit, *object.Commit, error) {
98 err := g.validateRef()
99 if err != nil {
100 return nil, nil, err
101 }
102
103 opts := &git.LogOptions{Order: git.LogOrderCommitterTime}
104
105 if from != "" {
106 hash, err := g.repository.ResolveRevision(plumbing.Revision(from))
107 if err != nil {
108 return nil, nil, errors.Join(MissingRefErr, err)
109 }
110 opts.From = *hash
111 }
112
113 ci, err := g.repository.Log(opts)
114 if err != nil {
115 return nil, nil, fmt.Errorf("commits from ref: %w", err)
116 }
117
118 commits := []*object.Commit{}
119 var next *object.Commit
120
121 // iterate one more item so we can fetch the next commit
122 for x := 0; x < (count + 1); x++ {
123 c, err := ci.Next()
124 if err != nil && errors.Is(err, io.EOF) {
125 break
126 } else if err != nil {
127 return nil, nil, err
128 }
129 if x == count {
130 next = c
131 } else {
132 commits = append(commits, c)
133 }
134 }
135
136 return commits, next, nil
137}
138
139func (g *GitRepository) Head() (*plumbing.Reference, error) {
140 return g.repository.Head()
141}
142
143func (g *GitRepository) Tags() ([]*TagReference, error) {
144 iter, err := g.repository.Tags()
145 if err != nil {
146 return nil, err
147 }
148
149 tags := make([]*TagReference, 0)
150
151 if err := iter.ForEach(func(ref *plumbing.Reference) error {
152 obj, err := g.repository.TagObject(ref.Hash())
153 switch err {
154 case nil:
155 tags = append(tags, &TagReference{
156 ref: ref,
157 tag: obj,
158 })
159 case plumbing.ErrObjectNotFound:
160 tags = append(tags, &TagReference{
161 ref: ref,
162 })
163 default:
164 return err
165 }
166 return nil
167 }); err != nil {
168 return nil, err
169 }
170
171 // tagList modify the underlying tag list.
172 tagList := &tagList{r: g.repository, refs: tags}
173 sort.Sort(tagList)
174
175 return tags, nil
176}
177
178func (g *GitRepository) Branches() ([]*plumbing.Reference, error) {
179 bs, err := g.repository.Branches()
180 if err != nil {
181 return nil, err
182 }
183
184 branches := []*plumbing.Reference{}
185 err = bs.ForEach(func(ref *plumbing.Reference) error {
186 branches = append(branches, ref)
187 return nil
188 })
189 if err != nil {
190 return nil, err
191 }
192
193 return branches, nil
194}
195
196func (g *GitRepository) Diff() (string, error) {
197 err := g.validateRef()
198 if err != nil {
199 return "", err
200 }
201
202 c, err := g.repository.CommitObject(g.ref)
203 if err != nil {
204 return "", err
205 }
206
207 commitTree, err := c.Tree()
208 if err != nil {
209 return "", err
210 }
211
212 patch := &object.Patch{}
213 parentTree := &object.Tree{}
214 if c.NumParents() != 0 {
215 parent, err := c.Parents().Next()
216 if err == nil {
217 parentTree, err = parent.Tree()
218 if err == nil {
219 patch, err = parentTree.Patch(commitTree)
220 if err != nil {
221 return "", err
222 }
223 }
224 }
225 } else {
226 patch, err = parentTree.Patch(commitTree)
227 if err != nil {
228 return "", err
229 }
230 }
231
232 return patch.String(), nil
233}
234
235func (g *GitRepository) Tree(path string) (*object.Tree, error) {
236 err := g.validateRef()
237 if err != nil {
238 return nil, err
239 }
240
241 c, err := g.repository.CommitObject(g.ref)
242 if err != nil {
243 return nil, err
244 }
245
246 tree, err := c.Tree()
247 if err != nil {
248 return nil, err
249 }
250
251 if path == "" {
252 return tree, nil
253 } else {
254 o, err := tree.FindEntry(path)
255 if err != nil {
256 return nil, err
257 }
258
259 if !o.Mode.IsFile() {
260 subtree, err := tree.Tree(path)
261 if err != nil {
262 return nil, err
263 }
264 return subtree, nil
265 } else {
266 return nil, TreeForFileErr
267 }
268 }
269}
270
271func (g *GitRepository) validateRef() error {
272 if !g.setRef {
273 return g.SetRef("")
274 }
275 return nil
276}
277
278func (g *GitRepository) IsBinary(path string) (bool, error) {
279 tree, err := g.Tree("")
280 if err != nil {
281 return false, err
282 }
283
284 file, err := tree.File(path)
285 if err != nil {
286 return false, err
287 }
288
289 return file.IsBinary()
290}
291
292func (g *GitRepository) FileContent(path string) ([]byte, error) {
293 err := g.validateRef()
294 if err != nil {
295 return nil, err
296 }
297
298 c, err := g.repository.CommitObject(g.ref)
299 if err != nil {
300 return nil, err
301 }
302
303 tree, err := c.Tree()
304 if err != nil {
305 return nil, err
306 }
307
308 file, err := tree.File(path)
309 if err != nil {
310 return nil, err
311 }
312
313 r, err := file.Blob.Reader()
314 if err != nil {
315 return nil, err
316 }
317 defer r.Close()
318
319 var buf bytes.Buffer
320 _, err = io.Copy(&buf, r)
321 if err != nil {
322 return nil, err
323 }
324
325 return buf.Bytes(), nil
326}
327
328func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
329 tw := tar.NewWriter(w)
330 defer tw.Close()
331
332 tree, err := g.Tree("")
333 if err != nil {
334 return err
335 }
336
337 walker := object.NewTreeWalker(tree, true, nil)
338 defer walker.Close()
339
340 name, entry, err := walker.Next()
341 for ; err == nil; name, entry, err = walker.Next() {
342 info, err := newInfoWrapper(name, prefix, &entry, tree)
343 if err != nil {
344 return err
345 }
346
347 header, err := tar.FileInfoHeader(info, "")
348 if err != nil {
349 return err
350 }
351
352 err = tw.WriteHeader(header)
353 if err != nil {
354 return err
355 }
356
357 if !info.IsDir() {
358 file, err := tree.File(name)
359 if err != nil {
360 return err
361 }
362
363 reader, err := file.Blob.Reader()
364 if err != nil {
365 return err
366 }
367
368 _, err = io.Copy(tw, reader)
369 if err != nil {
370 reader.Close()
371 return err
372 }
373 reader.Close()
374 }
375 }
376
377 return nil
378}
379
380func newInfoWrapper(
381 filename string,
382 prefix string,
383 entry *object.TreeEntry,
384 tree *object.Tree,
385) (*infoWrapper, error) {
386 var (
387 size int64
388 mode fs.FileMode
389 isDir bool
390 )
391
392 if entry.Mode.IsFile() {
393 file, err := tree.TreeEntryFile(entry)
394 if err != nil {
395 return nil, err
396 }
397 mode = fs.FileMode(file.Mode)
398
399 size, err = tree.Size(filename)
400 if err != nil {
401 return nil, err
402 }
403 } else {
404 isDir = true
405 mode = fs.ModeDir | fs.ModePerm
406 }
407
408 fullname := path.Join(prefix, filename)
409 return &infoWrapper{
410 name: fullname,
411 size: size,
412 mode: mode,
413 modTime: time.Unix(0, 0),
414 isDir: isDir,
415 }, nil
416}
417
418func (i *infoWrapper) Name() string {
419 return i.name
420}
421
422func (i *infoWrapper) Size() int64 {
423 return i.size
424}
425
426func (i *infoWrapper) Mode() fs.FileMode {
427 return i.mode
428}
429
430func (i *infoWrapper) ModTime() time.Time {
431 return i.modTime
432}
433
434func (i *infoWrapper) IsDir() bool {
435 return i.isDir
436}
437
438func (i *infoWrapper) Sys() any {
439 return nil
440}
441
442func (t *TagReference) HashString() string {
443 return t.ref.Hash().String()
444}
445
446func (t *TagReference) ShortName() string {
447 return t.ref.Name().Short()
448}
449
450func (t *TagReference) Message() string {
451 if t.tag != nil {
452 return t.tag.Message
453 }
454 return ""
455}
456
457func (self *tagList) Len() int {
458 return len(self.refs)
459}
460
461func (self *tagList) Swap(i, j int) {
462 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
463}
464
465func (self *tagList) Less(i, j int) bool {
466 var dateI time.Time
467 var dateJ time.Time
468
469 if self.refs[i].tag != nil {
470 dateI = self.refs[i].tag.Tagger.When
471 } else {
472 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
473 if err != nil {
474 dateI = time.Now()
475 } else {
476 dateI = c.Committer.When
477 }
478 }
479
480 if self.refs[j].tag != nil {
481 dateJ = self.refs[j].tag.Tagger.When
482 } else {
483 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
484 if err != nil {
485 dateJ = time.Now()
486 } else {
487 dateJ = c.Committer.When
488 }
489 }
490
491 return dateI.After(dateJ)
492}