Files
realy/cmd/legit/git/git.go

345 lines
6.4 KiB
Go

package git
import (
"archive/tar"
"fmt"
"io"
"io/fs"
"path"
"sort"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
type GitRepo struct {
r *git.Repository
h plumbing.Hash
}
type TagList struct {
refs []*TagReference
r *git.Repository
}
// TagReference is used to list both tag and non-annotated tags.
// Non-annotated tags should only contains a reference.
// Annotated tags should contain its reference and its tag information.
type TagReference struct {
ref *plumbing.Reference
tag *object.Tag
}
// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
// to tar WriteHeader
type infoWrapper struct {
name string
size int64
mode fs.FileMode
modTime time.Time
isDir bool
}
func (self *TagList) Len() int {
return len(self.refs)
}
func (self *TagList) Swap(i, j int) {
self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
}
// sorting tags in reverse chronological order
func (self *TagList) Less(i, j int) bool {
var dateI time.Time
var dateJ time.Time
if self.refs[i].tag != nil {
dateI = self.refs[i].tag.Tagger.When
} else {
c, err := self.r.CommitObject(self.refs[i].ref.Hash())
if err != nil {
dateI = time.Now()
} else {
dateI = c.Committer.When
}
}
if self.refs[j].tag != nil {
dateJ = self.refs[j].tag.Tagger.When
} else {
c, err := self.r.CommitObject(self.refs[j].ref.Hash())
if err != nil {
dateJ = time.Now()
} else {
dateJ = c.Committer.When
}
}
return dateI.After(dateJ)
}
func Open(path string, ref string) (*GitRepo, error) {
var err error
g := GitRepo{}
g.r, err = git.PlainOpen(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
}
if ref == "" {
head, err := g.r.Head()
if err != nil {
return nil, fmt.Errorf("getting head of %s: %w", path, err)
}
g.h = head.Hash()
} else {
hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
if err != nil {
return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
}
g.h = *hash
}
return &g, nil
}
func (g *GitRepo) Commits() ([]*object.Commit, error) {
ci, err := g.r.Log(&git.LogOptions{From: g.h})
if err != nil {
return nil, fmt.Errorf("commits from ref: %w", err)
}
commits := []*object.Commit{}
ci.ForEach(func(c *object.Commit) error {
commits = append(commits, c)
return nil
})
return commits, nil
}
func (g *GitRepo) LastCommit() (*object.Commit, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return nil, fmt.Errorf("last commit: %w", err)
}
return c, nil
}
func (g *GitRepo) FileContent(path string) (string, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return "", fmt.Errorf("commit object: %w", err)
}
tree, err := c.Tree()
if err != nil {
return "", fmt.Errorf("file tree: %w", err)
}
file, err := tree.File(path)
if err != nil {
return "", err
}
isbin, _ := file.IsBinary()
if !isbin {
return file.Contents()
} else {
return "Not displaying binary file", nil
}
}
func (g *GitRepo) Tags() ([]*TagReference, error) {
iter, err := g.r.Tags()
if err != nil {
return nil, fmt.Errorf("tag objects: %w", err)
}
tags := make([]*TagReference, 0)
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := g.r.TagObject(ref.Hash())
switch err {
case nil:
tags = append(tags, &TagReference{
ref: ref,
tag: obj,
})
case plumbing.ErrObjectNotFound:
tags = append(tags, &TagReference{
ref: ref,
})
default:
return err
}
return nil
}); err != nil {
return nil, err
}
tagList := &TagList{r: g.r, refs: tags}
sort.Sort(tagList)
return tags, nil
}
func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
bi, err := g.r.Branches()
if err != nil {
return nil, fmt.Errorf("branchs: %w", err)
}
branches := []*plumbing.Reference{}
_ = bi.ForEach(func(ref *plumbing.Reference) error {
branches = append(branches, ref)
return nil
})
return branches, nil
}
func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
for _, b := range branches {
_, err := g.r.ResolveRevision(plumbing.Revision(b))
if err == nil {
return b, nil
}
}
return "", fmt.Errorf("unable to find main branch")
}
// WriteTar writes itself from a tree into a binary tar file format.
// prefix is root folder to be appended.
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
tw := tar.NewWriter(w)
defer tw.Close()
c, err := g.r.CommitObject(g.h)
if err != nil {
return fmt.Errorf("commit object: %w", err)
}
tree, err := c.Tree()
if err != nil {
return err
}
walker := object.NewTreeWalker(tree, true, nil)
defer walker.Close()
name, entry, err := walker.Next()
for ; err == nil; name, entry, err = walker.Next() {
info, err := newInfoWrapper(name, prefix, &entry, tree)
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
err = tw.WriteHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := tree.File(name)
if err != nil {
return err
}
reader, err := file.Blob.Reader()
if err != nil {
return err
}
_, err = io.Copy(tw, reader)
if err != nil {
reader.Close()
return err
}
reader.Close()
}
}
return nil
}
func newInfoWrapper(
name string,
prefix string,
entry *object.TreeEntry,
tree *object.Tree,
) (*infoWrapper, error) {
var (
size int64
mode fs.FileMode
isDir bool
)
if entry.Mode.IsFile() {
file, err := tree.TreeEntryFile(entry)
if err != nil {
return nil, err
}
mode = fs.FileMode(file.Mode)
size, err = tree.Size(name)
if err != nil {
return nil, err
}
} else {
isDir = true
mode = fs.ModeDir | fs.ModePerm
}
fullname := path.Join(prefix, name)
return &infoWrapper{
name: fullname,
size: size,
mode: mode,
modTime: time.Unix(0, 0),
isDir: isDir,
}, nil
}
func (i *infoWrapper) Name() string {
return i.name
}
func (i *infoWrapper) Size() int64 {
return i.size
}
func (i *infoWrapper) Mode() fs.FileMode {
return i.mode
}
func (i *infoWrapper) ModTime() time.Time {
return i.modTime
}
func (i *infoWrapper) IsDir() bool {
return i.isDir
}
func (i *infoWrapper) Sys() any {
return nil
}
func (t *TagReference) Name() string {
return t.ref.Name().Short()
}
func (t *TagReference) Message() string {
if t.tag != nil {
return t.tag.Message
}
return ""
}