package browse
import (
"context"
"errors"
"fmt"
"io"
"sort"
"time"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk"
"github.com/dolthub/dolt/go/libraries/doltcore/ref"
"github.com/dolthub/dolt/go/store/datas"
"github.com/dolthub/dolt/go/store/hash"
)
// defaultBranchName is preferred as the default branch when present.
const defaultBranchName = "main"
// Branch is a named branch and the hash of its head commit.
type Branch struct {
Name string
Head string
}
// CommitInfo is a single commit in a log listing.
type CommitInfo struct {
Hash string
Author string
Email string
Date time.Time
Message string
ParentHashes []string
}
// Branches returns all branches in the store, sorted by name with the default
// branch (see DefaultBranch) first.
func (db *DB) Branches(ctx context.Context) ([]Branch, error) {
refs, err := db.ddb.GetBranchesWithHashes(ctx)
if err != nil {
return nil, fmt.Errorf("browse: list branches: %w", err)
}
branches := make([]Branch, 0, len(refs))
for _, r := range refs {
branches = append(branches, Branch{Name: r.Ref.GetPath(), Head: r.Hash.String()})
}
sort.Slice(branches, func(i, j int) bool {
// "main" sorts before everything else; otherwise alphabetical.
if branches[i].Name == defaultBranchName {
return branches[j].Name != defaultBranchName
}
if branches[j].Name == defaultBranchName {
return false
}
return branches[i].Name < branches[j].Name
})
return branches, nil
}
// DefaultBranch picks the default branch name from a list produced by
// Branches: "main" if present, otherwise the first branch. It returns "" when
// the list is empty.
func DefaultBranch(branches []Branch) string {
for _, b := range branches {
if b.Name == defaultBranchName {
return b.Name
}
}
if len(branches) > 0 {
return branches[0].Name
}
return ""
}
// Log returns up to limit commits in reverse-topological order starting from
// the head of ref (a branch name or a commit hash). When fromHash is non-empty
// the walk starts there instead of ref's head, which is how pages after the
// first are fetched: pass the nextHash returned by the previous call. nextHash
// is the hash of the first commit of the following page, or "" when the last
// page was returned.
//
// A caller may rely on errors.Is(err, ErrRefNotFound) to hold whenever refStr
// or fromHash names nothing this store can resolve — including a fromHash
// that does not even parse as a hash. Any other error means the store could
// not answer, not that the answer is "not found".
func (db *DB) Log(ctx context.Context, refStr, fromHash string, limit int) ([]CommitInfo, string, error) {
if limit <= 0 {
return nil, "", fmt.Errorf("browse: log limit must be positive, got %d", limit)
}
var start hash.Hash
if fromHash != "" {
h, ok := hash.MaybeParse(fromHash)
if !ok {
return nil, "", fmt.Errorf("%w: invalid from hash %q", ErrRefNotFound, fromHash)
}
start = h
} else {
c, err := db.resolveCommit(ctx, refStr)
if err != nil {
return nil, "", err
}
start, err = c.HashOf()
if err != nil {
return nil, "", fmt.Errorf("browse: head hash of %q: %w", refStr, err)
}
}
itr, err := commitwalk.GetTopologicalOrderIterator[context.Context](ctx, db.ddb, []hash.Hash{start}, nil)
if errors.Is(err, datas.ErrCommitNotFound) {
// A well-formed hash (parsed above, or the head of a resolved ref)
// that names no commit in this store is a miss, not a store failure.
return nil, "", fmt.Errorf("%w: %s", ErrRefNotFound, start.String())
}
if err != nil {
return nil, "", fmt.Errorf("browse: topological iterator: %w", err)
}
out := make([]CommitInfo, 0, limit)
nextHash := ""
for {
h, oc, meta, _, err := itr.Next(ctx)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, "", fmt.Errorf("browse: walk commits: %w", err)
}
if len(out) == limit {
// One past the requested page: its hash is the next page's start.
nextHash = h.String()
break
}
ci, err := commitInfo(ctx, h, oc, meta)
if err != nil {
return nil, "", err
}
out = append(out, ci)
}
return out, nextHash, nil
}
// commitInfo builds a CommitInfo from the iterator's outputs. meta may be
// supplied by the iterator; when nil we read it from the commit.
func commitInfo(ctx context.Context, h hash.Hash, oc *doltdb.OptionalCommit, meta *datas.CommitMeta) (CommitInfo, error) {
commit, ok := oc.ToCommit()
if !ok {
return CommitInfo{}, fmt.Errorf("browse: commit %s is not resolvable (ghost)", h.String())
}
if meta == nil {
var err error
meta, err = commit.GetCommitMeta(ctx)
if err != nil {
return CommitInfo{}, fmt.Errorf("browse: commit meta %s: %w", h.String(), err)
}
}
parents, err := commit.ParentHashes(ctx)
if err != nil {
return CommitInfo{}, fmt.Errorf("browse: parent hashes %s: %w", h.String(), err)
}
parentStrs := make([]string, len(parents))
for i, p := range parents {
parentStrs[i] = p.String()
}
return CommitInfo{
Hash: h.String(),
Author: meta.Author.Name,
Email: meta.Author.Email,
Date: time.UnixMilli(int64(meta.TimestampMillis())),
Message: meta.Description,
ParentHashes: parentStrs,
}, nil
}
// resolveCommit resolves a ref string to a commit. It is tried first as a
// branch name, then as a commit hash. Every way refStr can fail to name a
// commit — not a hash, a hash with no matching commit, a hash that resolves
// only to a ghost — is reported via ErrRefNotFound; other errors mean the
// store itself could not answer.
func (db *DB) resolveCommit(ctx context.Context, refStr string) (*doltdb.Commit, error) {
if _, ok, err := db.ddb.HasBranch(ctx, refStr); err != nil {
return nil, fmt.Errorf("browse: check branch %q: %w", refStr, err)
} else if ok {
c, err := db.ddb.ResolveCommitRef(ctx, ref.NewBranchRef(refStr))
if err != nil {
return nil, fmt.Errorf("browse: resolve branch %q: %w", refStr, err)
}
return c, nil
}
if h, ok := hash.MaybeParse(refStr); ok {
oc, err := db.ddb.ResolveHash(ctx, h)
if errors.Is(err, datas.ErrCommitNotFound) {
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr)
}
if err != nil {
return nil, fmt.Errorf("browse: resolve hash %q: %w", refStr, err)
}
c, ok := oc.ToCommit()
if !ok {
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr)
}
return c, nil
}
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr)
}
// resolveRoot resolves a ref string to its committed root value.
func (db *DB) resolveRoot(ctx context.Context, refStr string) (doltdb.RootValue, error) {
c, err := db.resolveCommit(ctx, refStr)
if err != nil {
return nil, err
}
root, err := c.GetRootValue(ctx)
if err != nil {
return nil, fmt.Errorf("browse: root value for %q: %w", refStr, err)
}
return root, nil
}