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