cerrado @ 3739c9e14b0c65a59a520dbfefa459e43af3bf20

  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}