git-preview @ main

static git web frontend but with images and html previews + code truncation

git/git.go

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