~bigbes/sr-ht-dolt

ref: 2c8903fd8ef6f2e3843f9373be268cf9136ba9e2 sr-ht-dolt/browse/log.go -rw-r--r-- 6.8 KiB
2c8903fd — Eugene Blikh browse: report an unparseable start hash as a missing ref 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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
}