package gitx import ( "context" "fmt" "time" "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" "go.bigb.es/sourcehut-compare/core" ) // CommitInfo is the metadata of a single commit. type CommitInfo struct { SHA string ShortSHA string AuthorName string AuthorEmail string Date time.Time Subject string Body string // ParentSHAs lists the commit's parents in order. Empty for a root commit; // length > 1 marks a merge commit. ParentSHAs []string } // resolveCommit validates rev, resolves it to a commit, and returns the go-git // object. A syntactically valid but unresolvable revision yields core.ErrBadRef. func (r *Repo) resolveCommit(rev string) (*object.Commit, error) { if !core.ValidRef(rev) { return nil, fmt.Errorf("%w: %q", core.ErrBadRef, rev) } hash, err := r.repo.ResolveRevision(plumbing.Revision(rev)) if err != nil { return nil, badRef(rev, err) } c, err := r.repo.CommitObject(*hash) if err != nil { return nil, badRef(rev, err) } return c, nil } // ResolveCommit resolves an arbitrary revision (branch, tag, short or full SHA) // to a commit and returns its metadata. func (r *Repo) ResolveCommit(ctx context.Context, rev string) (*CommitInfo, error) { _, cancel := r.withTimeout(ctx) defer cancel() c, err := r.resolveCommit(rev) if err != nil { return nil, err } return commitInfo(c), nil } // Log returns up to limit commits in the range base..head (reachable from head // but not from base), newest first. A non-positive limit defaults to 50. func (r *Repo) Log(ctx context.Context, base, head string, limit int) ([]CommitInfo, error) { if limit <= 0 { limit = 50 } _, cancel := r.withTimeout(ctx) defer cancel() baseCommit, err := r.resolveCommit(base) if err != nil { return nil, err } headCommit, err := r.resolveCommit(head) if err != nil { return nil, err } // Exclude everything reachable from base so a shared ancestor reached via a // non-base path is still dropped — matching git's base..head semantics. excluded, err := reachableSet(baseCommit) if err != nil { return nil, err } var out []CommitInfo iter := object.NewCommitPreorderIter(headCommit, excluded, nil) err = iter.ForEach(func(c *object.Commit) error { if len(out) >= limit { return storer.ErrStop } out = append(out, *commitInfo(c)) return nil }) if err != nil { return nil, err } return out, nil } // Parents returns the parent SHAs of a commit in order. A root commit yields an // empty slice; length > 1 marks a merge commit. func (r *Repo) Parents(ctx context.Context, rev string) ([]string, error) { _, cancel := r.withTimeout(ctx) defer cancel() c, err := r.resolveCommit(rev) if err != nil { return nil, err } if len(c.ParentHashes) == 0 { return nil, nil } parents := make([]string, 0, len(c.ParentHashes)) for _, p := range c.ParentHashes { parents = append(parents, p.String()) } return parents, nil } // reachableSet returns the set of every commit hash reachable from c // (inclusive). func reachableSet(c *object.Commit) (map[plumbing.Hash]bool, error) { set := make(map[plumbing.Hash]bool) iter := object.NewCommitPreorderIter(c, nil, nil) err := iter.ForEach(func(x *object.Commit) error { set[x.Hash] = true return nil }) if err != nil { return nil, err } return set, nil }