cerrado @ c1247a52e92523be8d225d20dc9592f32dca5088

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