| 1 |
|
package browse |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"errors" |
| 6 |
|
"fmt" |
| 7 |
|
"io" |
| 8 |
|
"sort" |
| 9 |
|
"time" |
| 10 |
|
|
| 11 |
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb" |
| 12 |
|
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk" |
| 13 |
|
"github.com/dolthub/dolt/go/libraries/doltcore/ref" |
| 14 |
|
"github.com/dolthub/dolt/go/store/datas" |
| 15 |
|
"github.com/dolthub/dolt/go/store/hash" |
| 16 |
|
) |
| 17 |
|
|
| 18 |
|
// defaultBranchName is preferred as the default branch when present. |
| 19 |
|
const defaultBranchName = "main" |
| 20 |
|
|
| 21 |
|
// Branch is a named branch and the hash of its head commit. |
| 22 |
|
type Branch struct { |
| 23 |
|
Name string |
| 24 |
|
Head string |
| 25 |
|
} |
| 26 |
|
|
| 27 |
|
// CommitInfo is a single commit in a log listing. |
| 28 |
|
type CommitInfo struct { |
| 29 |
|
Hash string |
| 30 |
|
Author string |
| 31 |
|
Email string |
| 32 |
|
Date time.Time |
| 33 |
|
Message string |
| 34 |
|
ParentHashes []string |
| 35 |
|
} |
| 36 |
|
|
| 37 |
|
// Branches returns all branches in the store, sorted by name with the default |
| 38 |
|
// branch (see DefaultBranch) first. |
| 39 |
0 |
func (db *DB) Branches(ctx context.Context) ([]Branch, error) { |
| 40 |
0 |
refs, err := db.ddb.GetBranchesWithHashes(ctx) |
| 41 |
0 |
if err != nil { |
| 42 |
0 |
return nil, fmt.Errorf("browse: list branches: %w", err) |
| 43 |
0 |
} |
| 44 |
|
|
| 45 |
0 |
branches := make([]Branch, 0, len(refs)) |
| 46 |
0 |
for _, r := range refs { |
| 47 |
0 |
branches = append(branches, Branch{Name: r.Ref.GetPath(), Head: r.Hash.String()}) |
| 48 |
0 |
} |
| 49 |
|
|
| 50 |
0 |
sort.Slice(branches, func(i, j int) bool { |
| 51 |
0 |
// "main" sorts before everything else; otherwise alphabetical. |
| 52 |
0 |
if branches[i].Name == defaultBranchName { |
| 53 |
0 |
return branches[j].Name != defaultBranchName |
| 54 |
0 |
} |
| 55 |
0 |
if branches[j].Name == defaultBranchName { |
| 56 |
0 |
return false |
| 57 |
0 |
} |
| 58 |
0 |
return branches[i].Name < branches[j].Name |
| 59 |
|
}) |
| 60 |
|
|
| 61 |
0 |
return branches, nil |
| 62 |
|
} |
| 63 |
|
|
| 64 |
|
// DefaultBranch picks the default branch name from a list produced by |
| 65 |
|
// Branches: "main" if present, otherwise the first branch. It returns "" when |
| 66 |
|
// the list is empty. |
| 67 |
0 |
func DefaultBranch(branches []Branch) string { |
| 68 |
0 |
for _, b := range branches { |
| 69 |
0 |
if b.Name == defaultBranchName { |
| 70 |
0 |
return b.Name |
| 71 |
0 |
} |
| 72 |
|
} |
| 73 |
0 |
if len(branches) > 0 { |
| 74 |
0 |
return branches[0].Name |
| 75 |
0 |
} |
| 76 |
0 |
return "" |
| 77 |
|
} |
| 78 |
|
|
| 79 |
|
// Log returns up to limit commits in reverse-topological order starting from |
| 80 |
|
// the head of ref (a branch name or a commit hash). When fromHash is non-empty |
| 81 |
|
// the walk starts there instead of ref's head, which is how pages after the |
| 82 |
|
// first are fetched: pass the nextHash returned by the previous call. nextHash |
| 83 |
|
// is the hash of the first commit of the following page, or "" when the last |
| 84 |
|
// page was returned. |
| 85 |
|
// |
| 86 |
|
// A caller may rely on errors.Is(err, ErrRefNotFound) to hold whenever refStr |
| 87 |
|
// or fromHash names nothing this store can resolve — including a fromHash |
| 88 |
|
// that does not even parse as a hash. Any other error means the store could |
| 89 |
|
// not answer, not that the answer is "not found". |
| 90 |
0 |
func (db *DB) Log(ctx context.Context, refStr, fromHash string, limit int) ([]CommitInfo, string, error) { |
| 91 |
0 |
if limit <= 0 { |
| 92 |
0 |
return nil, "", fmt.Errorf("browse: log limit must be positive, got %d", limit) |
| 93 |
0 |
} |
| 94 |
|
|
| 95 |
0 |
var start hash.Hash |
| 96 |
0 |
if fromHash != "" { |
| 97 |
0 |
h, ok := hash.MaybeParse(fromHash) |
| 98 |
0 |
if !ok { |
| 99 |
0 |
return nil, "", fmt.Errorf("%w: invalid from hash %q", ErrRefNotFound, fromHash) |
| 100 |
0 |
} |
| 101 |
0 |
start = h |
| 102 |
0 |
} else { |
| 103 |
0 |
c, err := db.resolveCommit(ctx, refStr) |
| 104 |
0 |
if err != nil { |
| 105 |
0 |
return nil, "", err |
| 106 |
0 |
} |
| 107 |
0 |
start, err = c.HashOf() |
| 108 |
0 |
if err != nil { |
| 109 |
0 |
return nil, "", fmt.Errorf("browse: head hash of %q: %w", refStr, err) |
| 110 |
0 |
} |
| 111 |
|
} |
| 112 |
|
|
| 113 |
0 |
itr, err := commitwalk.GetTopologicalOrderIterator[context.Context](ctx, db.ddb, []hash.Hash{start}, nil) |
| 114 |
0 |
if errors.Is(err, datas.ErrCommitNotFound) { |
| 115 |
0 |
// A well-formed hash (parsed above, or the head of a resolved ref) |
| 116 |
0 |
// that names no commit in this store is a miss, not a store failure. |
| 117 |
0 |
return nil, "", fmt.Errorf("%w: %s", ErrRefNotFound, start.String()) |
| 118 |
0 |
} |
| 119 |
0 |
if err != nil { |
| 120 |
0 |
return nil, "", fmt.Errorf("browse: topological iterator: %w", err) |
| 121 |
0 |
} |
| 122 |
|
|
| 123 |
0 |
out := make([]CommitInfo, 0, limit) |
| 124 |
0 |
nextHash := "" |
| 125 |
0 |
for { |
| 126 |
0 |
h, oc, meta, _, err := itr.Next(ctx) |
| 127 |
0 |
if errors.Is(err, io.EOF) { |
| 128 |
0 |
break |
| 129 |
|
} |
| 130 |
0 |
if err != nil { |
| 131 |
0 |
return nil, "", fmt.Errorf("browse: walk commits: %w", err) |
| 132 |
0 |
} |
| 133 |
|
|
| 134 |
0 |
if len(out) == limit { |
| 135 |
0 |
// One past the requested page: its hash is the next page's start. |
| 136 |
0 |
nextHash = h.String() |
| 137 |
0 |
break |
| 138 |
|
} |
| 139 |
|
|
| 140 |
0 |
ci, err := commitInfo(ctx, h, oc, meta) |
| 141 |
0 |
if err != nil { |
| 142 |
0 |
return nil, "", err |
| 143 |
0 |
} |
| 144 |
0 |
out = append(out, ci) |
| 145 |
|
} |
| 146 |
|
|
| 147 |
0 |
return out, nextHash, nil |
| 148 |
|
} |
| 149 |
|
|
| 150 |
|
// commitInfo builds a CommitInfo from the iterator's outputs. meta may be |
| 151 |
|
// supplied by the iterator; when nil we read it from the commit. |
| 152 |
0 |
func commitInfo(ctx context.Context, h hash.Hash, oc *doltdb.OptionalCommit, meta *datas.CommitMeta) (CommitInfo, error) { |
| 153 |
0 |
commit, ok := oc.ToCommit() |
| 154 |
0 |
if !ok { |
| 155 |
0 |
return CommitInfo{}, fmt.Errorf("browse: commit %s is not resolvable (ghost)", h.String()) |
| 156 |
0 |
} |
| 157 |
|
|
| 158 |
0 |
if meta == nil { |
| 159 |
0 |
var err error |
| 160 |
0 |
meta, err = commit.GetCommitMeta(ctx) |
| 161 |
0 |
if err != nil { |
| 162 |
0 |
return CommitInfo{}, fmt.Errorf("browse: commit meta %s: %w", h.String(), err) |
| 163 |
0 |
} |
| 164 |
|
} |
| 165 |
|
|
| 166 |
0 |
parents, err := commit.ParentHashes(ctx) |
| 167 |
0 |
if err != nil { |
| 168 |
0 |
return CommitInfo{}, fmt.Errorf("browse: parent hashes %s: %w", h.String(), err) |
| 169 |
0 |
} |
| 170 |
0 |
parentStrs := make([]string, len(parents)) |
| 171 |
0 |
for i, p := range parents { |
| 172 |
0 |
parentStrs[i] = p.String() |
| 173 |
0 |
} |
| 174 |
|
|
| 175 |
0 |
return CommitInfo{ |
| 176 |
0 |
Hash: h.String(), |
| 177 |
0 |
Author: meta.Author.Name, |
| 178 |
0 |
Email: meta.Author.Email, |
| 179 |
0 |
Date: time.UnixMilli(int64(meta.TimestampMillis())), |
| 180 |
0 |
Message: meta.Description, |
| 181 |
0 |
ParentHashes: parentStrs, |
| 182 |
0 |
}, nil |
| 183 |
|
} |
| 184 |
|
|
| 185 |
|
// resolveCommit resolves a ref string to a commit. It is tried first as a |
| 186 |
|
// branch name, then as a commit hash. Every way refStr can fail to name a |
| 187 |
|
// commit — not a hash, a hash with no matching commit, a hash that resolves |
| 188 |
|
// only to a ghost — is reported via ErrRefNotFound; other errors mean the |
| 189 |
|
// store itself could not answer. |
| 190 |
0 |
func (db *DB) resolveCommit(ctx context.Context, refStr string) (*doltdb.Commit, error) { |
| 191 |
0 |
if _, ok, err := db.ddb.HasBranch(ctx, refStr); err != nil { |
| 192 |
0 |
return nil, fmt.Errorf("browse: check branch %q: %w", refStr, err) |
| 193 |
0 |
} else if ok { |
| 194 |
0 |
c, err := db.ddb.ResolveCommitRef(ctx, ref.NewBranchRef(refStr)) |
| 195 |
0 |
if err != nil { |
| 196 |
0 |
return nil, fmt.Errorf("browse: resolve branch %q: %w", refStr, err) |
| 197 |
0 |
} |
| 198 |
0 |
return c, nil |
| 199 |
|
} |
| 200 |
|
|
| 201 |
0 |
if h, ok := hash.MaybeParse(refStr); ok { |
| 202 |
0 |
oc, err := db.ddb.ResolveHash(ctx, h) |
| 203 |
0 |
if errors.Is(err, datas.ErrCommitNotFound) { |
| 204 |
0 |
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr) |
| 205 |
0 |
} |
| 206 |
0 |
if err != nil { |
| 207 |
0 |
return nil, fmt.Errorf("browse: resolve hash %q: %w", refStr, err) |
| 208 |
0 |
} |
| 209 |
0 |
c, ok := oc.ToCommit() |
| 210 |
0 |
if !ok { |
| 211 |
0 |
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr) |
| 212 |
0 |
} |
| 213 |
0 |
return c, nil |
| 214 |
|
} |
| 215 |
|
|
| 216 |
0 |
return nil, fmt.Errorf("%w: %s", ErrRefNotFound, refStr) |
| 217 |
|
} |
| 218 |
|
|
| 219 |
|
// resolveRoot resolves a ref string to its committed root value. |
| 220 |
0 |
func (db *DB) resolveRoot(ctx context.Context, refStr string) (doltdb.RootValue, error) { |
| 221 |
0 |
c, err := db.resolveCommit(ctx, refStr) |
| 222 |
0 |
if err != nil { |
| 223 |
0 |
return nil, err |
| 224 |
0 |
} |
| 225 |
0 |
root, err := c.GetRootValue(ctx) |
| 226 |
0 |
if err != nil { |
| 227 |
0 |
return nil, fmt.Errorf("browse: root value for %q: %w", refStr, err) |
| 228 |
0 |
} |
| 229 |
0 |
return root, nil |
| 230 |
|
} |