cerrado @ v0.2.1

  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}