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