arnaucube
arnaucube/gitviewPublic Self-hosted repository
gitview/internal/git/git.go
internal/git/git.goRaw
  1package git
  2
  3import (
  4	"archive/tar"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"io/fs"
  9	"path"
 10	"sort"
 11	"time"
 12
 13	"github.com/go-git/go-git/v5"
 14	"github.com/go-git/go-git/v5/plumbing"
 15	"github.com/go-git/go-git/v5/plumbing/object"
 16)
 17
 18type GitRepo struct {
 19	r *git.Repository
 20	h plumbing.Hash
 21}
 22
 23type TagList struct {
 24	refs []*TagReference
 25	r    *git.Repository
 26}
 27
 28// TagReference is used to list both tag and non-annotated tags.
 29// Non-annotated tags should only contains a reference.
 30// Annotated tags should contain its reference and its tag information.
 31type TagReference struct {
 32	ref *plumbing.Reference
 33	tag *object.Tag
 34}
 35
 36// CommitReference aggregate all the references for a given commit
 37type CommitReference struct {
 38	commit *object.Commit
 39	refs   []*plumbing.Reference
 40}
 41
 42// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
 43// to tar WriteHeader
 44type infoWrapper struct {
 45	name    string
 46	size    int64
 47	mode    fs.FileMode
 48	modTime time.Time
 49	isDir   bool
 50}
 51
 52func (t *TagList) Len() int {
 53	return len(t.refs)
 54}
 55
 56func (t *TagList) Swap(i, j int) {
 57	t.refs[i], t.refs[j] = t.refs[j], t.refs[i]
 58}
 59
 60// Less sorts tags in reverse chronological order.
 61func (t *TagList) Less(i, j int) bool {
 62	var dateI time.Time
 63	var dateJ time.Time
 64
 65	if t.refs[i].tag != nil {
 66		dateI = t.refs[i].tag.Tagger.When
 67	} else {
 68		c, err := t.r.CommitObject(t.refs[i].ref.Hash())
 69		if err != nil {
 70			dateI = time.Now()
 71		} else {
 72			dateI = c.Committer.When
 73		}
 74	}
 75
 76	if t.refs[j].tag != nil {
 77		dateJ = t.refs[j].tag.Tagger.When
 78	} else {
 79		c, err := t.r.CommitObject(t.refs[j].ref.Hash())
 80		if err != nil {
 81			dateJ = time.Now()
 82		} else {
 83			dateJ = c.Committer.When
 84		}
 85	}
 86
 87	return dateI.After(dateJ)
 88}
 89
 90func Open(path string, ref string) (*GitRepo, error) {
 91	var err error
 92	g := GitRepo{}
 93	g.r, err = git.PlainOpen(path)
 94	if err != nil {
 95		return nil, fmt.Errorf("opening %s: %w", path, err)
 96	}
 97
 98	if ref == "" {
 99		head, err := g.r.Head()
100		if err != nil {
101			return nil, fmt.Errorf("getting head of %s: %w", path, err)
102		}
103		g.h = head.Hash()
104	} else {
105		hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
106		if err != nil {
107			return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
108		}
109		g.h = *hash
110	}
111	return &g, nil
112}
113
114func (g *GitRepo) Commits() ([]*CommitReference, error) {
115	ci, err := g.r.Log(&git.LogOptions{From: g.h})
116	if err != nil {
117		return nil, fmt.Errorf("commits from ref: %w", err)
118	}
119
120	commitRefs := []*CommitReference{}
121	if err := ci.ForEach(func(c *object.Commit) error {
122		commitRefs = append(commitRefs, &CommitReference{commit: c})
123		return nil
124	}); err != nil {
125		return nil, err
126	}
127
128	// new we fetch for possible tags for each commit
129	iter, err := g.r.References()
130	if err != nil {
131		return nil, err
132	}
133
134	if err := iter.ForEach(func(ref *plumbing.Reference) error {
135		for _, c := range commitRefs {
136			obj, err := g.r.TagObject(ref.Hash())
137			switch {
138			case err == nil:
139				if obj.Target == c.commit.Hash {
140					c.AddReference(ref)
141				}
142			case errors.Is(err, plumbing.ErrObjectNotFound):
143				if c.commit.Hash == ref.Hash() {
144					c.AddReference(ref)
145				}
146			default:
147				return err
148			}
149		}
150		return nil
151	}); err != nil {
152		return nil, err
153	}
154
155	return commitRefs, nil
156}
157
158func (g *GitRepo) LastCommit() (*CommitReference, error) {
159	c, err := g.r.CommitObject(g.h)
160	if err != nil {
161		return nil, fmt.Errorf("last commit: %w", err)
162	}
163
164	iter, err := g.r.Tags()
165	if err != nil {
166		return nil, err
167	}
168
169	commitRef := &CommitReference{commit: c}
170	if err := iter.ForEach(func(ref *plumbing.Reference) error {
171		obj, err := g.r.TagObject(ref.Hash())
172		switch {
173		case err == nil:
174			if obj.Target == commitRef.commit.Hash {
175				commitRef.AddReference(ref)
176			}
177		case errors.Is(err, plumbing.ErrObjectNotFound):
178			if commitRef.commit.Hash == ref.Hash() {
179				commitRef.AddReference(ref)
180			}
181		default:
182			return err
183		}
184
185		return nil
186	}); err != nil {
187		return nil, err
188	}
189
190	return commitRef, nil
191}
192
193func (g *GitRepo) FileContent(path string) (string, error) {
194	c, err := g.r.CommitObject(g.h)
195	if err != nil {
196		return "", fmt.Errorf("commit object: %w", err)
197	}
198
199	tree, err := c.Tree()
200	if err != nil {
201		return "", fmt.Errorf("file tree: %w", err)
202	}
203
204	file, err := tree.File(path)
205	if err != nil {
206		return "", err
207	}
208
209	isbin, _ := file.IsBinary()
210
211	if !isbin {
212		return file.Contents()
213	} else {
214		return "Not displaying binary file", nil
215	}
216}
217
218func (g *GitRepo) Tags() ([]*TagReference, error) {
219	iter, err := g.r.Tags()
220	if err != nil {
221		return nil, fmt.Errorf("tag objects: %w", err)
222	}
223
224	tags := make([]*TagReference, 0)
225
226	if err := iter.ForEach(func(ref *plumbing.Reference) error {
227		obj, err := g.r.TagObject(ref.Hash())
228		switch {
229		case err == nil:
230			tags = append(tags, &TagReference{
231				ref: ref,
232				tag: obj,
233			})
234		case errors.Is(err, plumbing.ErrObjectNotFound):
235			tags = append(tags, &TagReference{
236				ref: ref,
237			})
238		default:
239			return err
240		}
241		return nil
242	}); err != nil {
243		return nil, err
244	}
245
246	tagList := &TagList{r: g.r, refs: tags}
247	sort.Sort(tagList)
248	return tags, nil
249}
250
251func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
252	bi, err := g.r.Branches()
253	if err != nil {
254		return nil, fmt.Errorf("branchs: %w", err)
255	}
256
257	branches := []*plumbing.Reference{}
258
259	_ = bi.ForEach(func(ref *plumbing.Reference) error {
260		branches = append(branches, ref)
261		return nil
262	})
263
264	return branches, nil
265}
266
267func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
268	for _, b := range branches {
269		_, err := g.r.ResolveRevision(plumbing.Revision(b))
270		if err == nil {
271			return b, nil
272		}
273	}
274	// Repositories may use a default branch other than main or master.
275	head, err := g.r.Head()
276	if err == nil && head.Name().IsBranch() {
277		return head.Name().Short(), nil
278	}
279	return "HEAD", nil
280}
281
282// WriteTar writes itself from a tree into a binary tar file format.
283// prefix is root folder to be appended.
284func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
285	tw := tar.NewWriter(w)
286	defer func() { _ = tw.Close() }()
287
288	c, err := g.r.CommitObject(g.h)
289	if err != nil {
290		return fmt.Errorf("commit object: %w", err)
291	}
292
293	tree, err := c.Tree()
294	if err != nil {
295		return err
296	}
297
298	walker := object.NewTreeWalker(tree, true, nil)
299	defer walker.Close()
300
301	name, entry, err := walker.Next()
302	for ; err == nil; name, entry, err = walker.Next() {
303		info, err := newInfoWrapper(name, prefix, &entry, tree)
304		if err != nil {
305			return err
306		}
307
308		header, err := tar.FileInfoHeader(info, "")
309		if err != nil {
310			return err
311		}
312
313		err = tw.WriteHeader(header)
314		if err != nil {
315			return err
316		}
317
318		if !info.IsDir() {
319			file, err := tree.File(name)
320			if err != nil {
321				return err
322			}
323
324			reader, err := file.Reader()
325			if err != nil {
326				return err
327			}
328
329			_, err = io.Copy(tw, reader)
330			if err != nil {
331				_ = reader.Close()
332				return err
333			}
334			_ = reader.Close()
335		}
336	}
337
338	return nil
339}
340
341func newInfoWrapper(
342	name string,
343	prefix string,
344	entry *object.TreeEntry,
345	tree *object.Tree,
346) (*infoWrapper, error) {
347	var (
348		size  int64
349		mode  fs.FileMode
350		isDir bool
351	)
352
353	if entry.Mode.IsFile() {
354		file, err := tree.TreeEntryFile(entry)
355		if err != nil {
356			return nil, err
357		}
358		mode = fs.FileMode(file.Mode)
359
360		size, err = tree.Size(name)
361		if err != nil {
362			return nil, err
363		}
364	} else {
365		isDir = true
366		mode = fs.ModeDir | fs.ModePerm
367	}
368
369	fullname := path.Join(prefix, name)
370	return &infoWrapper{
371		name:    fullname,
372		size:    size,
373		mode:    mode,
374		modTime: time.Unix(0, 0),
375		isDir:   isDir,
376	}, nil
377}
378
379func (i *infoWrapper) Name() string {
380	return i.name
381}
382
383func (i *infoWrapper) Size() int64 {
384	return i.size
385}
386
387func (i *infoWrapper) Mode() fs.FileMode {
388	return i.mode
389}
390
391func (i *infoWrapper) ModTime() time.Time {
392	return i.modTime
393}
394
395func (i *infoWrapper) IsDir() bool {
396	return i.isDir
397}
398
399func (i *infoWrapper) Sys() any {
400	return nil
401}
402
403func (t *TagReference) Name() string {
404	return t.ref.Name().Short()
405}
406
407func (t *TagReference) Message() string {
408	if t.tag != nil {
409		return t.tag.Message
410	}
411	return ""
412}
413
414func (c *CommitReference) Commit() *object.Commit {
415	return c.commit
416}
417
418func (c *CommitReference) HasReference() bool {
419	return len(c.refs) > 0
420}
421
422func (c *CommitReference) References() []*plumbing.Reference {
423	return c.refs
424}
425
426func (c *CommitReference) AddReference(ref *plumbing.Reference) {
427	c.refs = append(c.refs, ref)
428}