cerrado @ cb9036aab96895ddf03cab68f75d3356f5227968

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