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