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 and 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 }