changeloguru/internal/git/repository.go

203 lines
4.1 KiB
Go

package git
import (
"fmt"
"sort"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/storer"
)
const (
head = "HEAD"
defaultCommitCount = 128
defaultTagCount = 32
)
// Repository is an abstraction for git-repository
type Repository interface {
Log(fromRev, toRev string) ([]Commit, error)
Commit(commitMessage string, paths ...string) error
SemVerTags() ([]SemVerTag, error)
}
type repo struct {
gitRepo *git.Repository
}
type stopFn func(*object.Commit) error
// NewRepository return Repository from path
func NewRepository(path string) (Repository, error) {
gitRepo, err := git.PlainOpen(path)
if err != nil {
return nil, err
}
return &repo{
gitRepo: gitRepo,
}, nil
}
// Log return all commits between <from revision> and <to revision>
func (r *repo) Log(fromRev, toRev string) ([]Commit, error) {
if fromRev == "" {
fromRev = head
}
fromHash, err := r.gitRepo.ResolveRevision(plumbing.Revision(fromRev))
if err != nil {
return nil, fmt.Errorf("failed to resolve %s: %w", fromRev, err)
}
if toRev == "" {
return r.logWithStopFn(fromHash, nil, nil)
}
toHash, err := r.gitRepo.ResolveRevision(plumbing.Revision(toRev))
if err != nil {
return nil, fmt.Errorf("failed to resolve %s: %w", toRev, err)
}
return r.logWithStopFn(fromHash, nil, stopAtHash(toHash))
}
func (r *repo) Commit(commitMessage string, paths ...string) error {
gitWorktree, err := r.gitRepo.Worktree()
if err != nil {
return fmt.Errorf("failed to get git worktree: %w", err)
}
for _, path := range paths {
if err := gitWorktree.AddWithOptions(&git.AddOptions{
Path: path,
}); err != nil {
return fmt.Errorf("failed to git add %s: %w", path, err)
}
}
if _, err := gitWorktree.Commit(commitMessage, &git.CommitOptions{}); err != nil {
return fmt.Errorf("failed to git commit: %w", err)
}
return nil
}
func (r *repo) logWithStopFn(fromHash *plumbing.Hash, beginFn, endFn stopFn) ([]Commit, error) {
cIter, err := r.gitRepo.Log(&git.LogOptions{
From: *fromHash,
})
if err != nil {
return nil, fmt.Errorf("failed to git log: %w", err)
}
commits := make([]Commit, 0, defaultCommitCount)
if err := cIter.ForEach(newIterFn(&commits, beginFn, endFn)); err != nil {
return nil, fmt.Errorf("failed to iterate each git log: %w", err)
}
return commits, nil
}
func stopAtHash(hash *plumbing.Hash) stopFn {
return func(c *object.Commit) error {
if c.Hash == *hash {
return storer.ErrStop
}
return nil
}
}
func newIterFn(commits *[]Commit, beginFn, endFn stopFn) func(c *object.Commit) error {
if beginFn == nil && endFn == nil {
return func(c *object.Commit) error {
commit := newCommit(c)
*commits = append(*commits, commit)
return nil
}
}
if beginFn == nil {
return func(c *object.Commit) error {
commit := newCommit(c)
*commits = append(*commits, commit)
if err := endFn(c); err != nil {
return err
}
return nil
}
}
if endFn == nil {
return func(c *object.Commit) error {
if err := beginFn(c); err != nil {
return err
}
commit := newCommit(c)
*commits = append(*commits, commit)
return nil
}
}
return func(c *object.Commit) error {
if err := beginFn(c); err != nil {
return err
}
commit := newCommit(c)
*commits = append(*commits, commit)
if err := endFn(c); err != nil {
return err
}
return nil
}
}
// Return SemVer tags from earliest to latest
func (r *repo) SemVerTags() ([]SemVerTag, error) {
iter, err := r.gitRepo.Tags()
if err != nil {
return nil, err
}
versions := make([]*semver.Version, 0, defaultTagCount)
if err := iter.ForEach(func(r *plumbing.Reference) error {
version, err := semver.NewVersion(r.Name().Short())
if err != nil {
// Ignore bad tag
return nil
}
versions = append(versions, version)
return nil
}); err != nil {
return nil, err
}
sort.Sort(semver.Collection(versions))
tags := make([]SemVerTag, 0, len(versions))
for _, version := range versions {
tags = append(tags, SemVerTag{
Version: version,
})
}
return tags, nil
}