package git import ( "archive/tar" "bytes" "errors" "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" ) var () var ( MissingRefErr = errors.New("Reference not found") TreeForFileErr = errors.New("Trying to get tree of a file") ) type ( GitRepository struct { path string repository *git.Repository ref plumbing.Hash setRef bool } TagReference struct { ref *plumbing.Reference tag *object.Tag } infoWrapper struct { name string size int64 mode fs.FileMode modTime time.Time isDir bool } tagList struct { refs []*TagReference r *git.Repository } ) func OpenRepository(dir string) (*GitRepository, error) { g := &GitRepository{ path: dir, } repo, err := git.PlainOpen(dir) if err != nil { return nil, err } g.repository = repo return g, nil } func (g *GitRepository) SetRef(ref string) error { if ref == "" { head, err := g.repository.Head() if err != nil { return errors.Join(MissingRefErr, err) } g.ref = head.Hash() } else { hash, err := g.repository.ResolveRevision(plumbing.Revision(ref)) if err != nil { return errors.Join(MissingRefErr, err) } g.ref = *hash } g.setRef = true return nil } func (g *GitRepository) Path() string { return g.path } func (g *GitRepository) LastCommit() (*object.Commit, error) { err := g.validateRef() if err != nil { return nil, err } c, err := g.repository.CommitObject(g.ref) if err != nil { return nil, err } return c, nil } func (g *GitRepository) Commits(count int) ([]*object.Commit, error) { err := g.validateRef() if err != nil { return nil, err } ci, err := g.repository.Log(&git.LogOptions{From: g.ref}) if err != nil { return nil, fmt.Errorf("commits from ref: %w", err) } commits := []*object.Commit{} // TODO: for now only load first 1000 for x := 0; x < count; x++ { c, err := ci.Next() if err != nil && errors.Is(err, io.EOF) { break } else if err != nil { return nil, err } commits = append(commits, c) } return commits, nil } func (g *GitRepository) Head() (*plumbing.Reference, error) { return g.repository.Head() } func (g *GitRepository) Tags() ([]*TagReference, error) { iter, err := g.repository.Tags() if err != nil { return nil, err } tags := make([]*TagReference, 0) if err := iter.ForEach(func(ref *plumbing.Reference) error { obj, err := g.repository.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 modify the underlying tag list. tagList := &tagList{r: g.repository, refs: tags} sort.Sort(tagList) return tags, nil } func (g *GitRepository) Branches() ([]*plumbing.Reference, error) { bs, err := g.repository.Branches() if err != nil { return nil, err } branches := []*plumbing.Reference{} err = bs.ForEach(func(ref *plumbing.Reference) error { branches = append(branches, ref) return nil }) if err != nil { return nil, err } return branches, nil } func (g *GitRepository) Diff() (string, error) { err := g.validateRef() if err != nil { return "", err } c, err := g.repository.CommitObject(g.ref) if err != nil { return "", err } commitTree, err := c.Tree() if err != nil { return "", err } patch := &object.Patch{} parentTree := &object.Tree{} if c.NumParents() != 0 { parent, err := c.Parents().Next() if err == nil { parentTree, err = parent.Tree() if err == nil { patch, err = parentTree.Patch(commitTree) if err != nil { return "", err } } } } else { patch, err = parentTree.Patch(commitTree) if err != nil { return "", err } } return patch.String(), nil } func (g *GitRepository) Tree(path string) (*object.Tree, error) { err := g.validateRef() if err != nil { return nil, err } c, err := g.repository.CommitObject(g.ref) if err != nil { return nil, err } tree, err := c.Tree() if err != nil { return nil, err } if path == "" { return tree, nil } else { o, err := tree.FindEntry(path) if err != nil { return nil, err } if !o.Mode.IsFile() { subtree, err := tree.Tree(path) if err != nil { return nil, err } return subtree, nil } else { return nil, TreeForFileErr } } } func (g *GitRepository) validateRef() error { if !g.setRef { return g.SetRef("") } return nil } func (g *GitRepository) IsBinary(path string) (bool, error) { tree, err := g.Tree("") if err != nil { return false, err } file, err := tree.File(path) if err != nil { return false, err } return file.IsBinary() } func (g *GitRepository) FileContent(path string) ([]byte, error) { err := g.validateRef() if err != nil { return nil, err } c, err := g.repository.CommitObject(g.ref) if err != nil { return nil, err } tree, err := c.Tree() if err != nil { return nil, err } file, err := tree.File(path) if err != nil { return nil, err } r, err := file.Blob.Reader() if err != nil { return nil, err } defer r.Close() var buf bytes.Buffer _, err = io.Copy(&buf, r) if err != nil { return nil, err } return buf.Bytes(), nil } func (g *GitRepository) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) defer tw.Close() tree, err := g.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( filename 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(filename) if err != nil { return nil, err } } else { isDir = true mode = fs.ModeDir | fs.ModePerm } fullname := path.Join(prefix, filename) 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) HashString() string { return t.ref.Hash().String() } func (t *TagReference) ShortName() string { return t.ref.Name().Short() } func (t *TagReference) Message() string { if t.tag != nil { return t.tag.Message } return "" } 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] } 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) }