cerrado @ master

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