coverage~bigbes/sr-ht-dolt3523280cbrowse/log.go

Coverage
0.0% 0/94 statements
Δ
+0.0
Blob
93ac43e
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 }