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 }