Fast implementation of Git in pure Go
1package read_test
2
3import (
4 "errors"
5 "path/filepath"
6 "strconv"
7 "strings"
8 "testing"
9
10 "codeberg.org/lindenii/furgit/commitgraph/bloom"
11 "codeberg.org/lindenii/furgit/commitgraph/read"
12 "codeberg.org/lindenii/furgit/internal/intconv"
13 "codeberg.org/lindenii/furgit/internal/testgit"
14 "codeberg.org/lindenii/furgit/objectid"
15)
16
17func fixtureRepoPath(t *testing.T, algo objectid.Algorithm, name string) string {
18 t.Helper()
19
20 return filepath.Join("testdata", "fixtures", algo.String(), name, "repo.git")
21}
22
23func fixtureRepo(t *testing.T, algo objectid.Algorithm, name string) *testgit.TestRepo {
24 t.Helper()
25
26 return testgit.NewRepoFromFixture(t, algo, fixtureRepoPath(t, algo, name))
27}
28
29func TestReadSingleMatchesGit(t *testing.T) {
30 t.Parallel()
31
32 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
33 testRepo := fixtureRepo(t, algo, "single_changed")
34
35 reader := openReader(t, testRepo, read.OpenSingle)
36
37 defer func() { _ = reader.Close() }()
38
39 allIDs := testRepo.RevList(t, "--all")
40 if len(allIDs) == 0 {
41 t.Fatal("git rev-list --all returned no commits")
42 }
43
44 wantCommitCount, err := intconv.IntToUint32(len(allIDs))
45 if err != nil {
46 t.Fatalf("len(allIDs) convert: %v", err)
47 }
48
49 if got := reader.NumCommits(); got != wantCommitCount {
50 t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs))
51 }
52
53 if !reader.HasBloom() {
54 t.Fatal("HasBloom() = false, want true")
55 }
56
57 bloomVersion := reader.BloomVersion()
58 if bloomVersion == 0 {
59 t.Fatal("BloomVersion() = 0, want non-zero when HasBloom() is true")
60 }
61
62 for _, id := range allIDs {
63 pos, err := reader.Lookup(id)
64 if err != nil {
65 t.Fatalf("Lookup(%s): %v", id, err)
66 }
67
68 gotID, err := reader.OIDAt(pos)
69 if err != nil {
70 t.Fatalf("OIDAt(%+v): %v", pos, err)
71 }
72
73 if gotID != id {
74 t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id)
75 }
76 }
77
78 step := max(len(allIDs)/24, 1)
79
80 for i, id := range allIDs {
81 if i%step != 0 && i != len(allIDs)-1 {
82 continue
83 }
84
85 verifyCommitAgainstGit(t, testRepo, reader, id)
86 }
87 })
88}
89
90func TestReadChainMatchesGit(t *testing.T) {
91 t.Parallel()
92
93 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
94 testRepo := fixtureRepo(t, algo, "chain_changed")
95
96 reader := openReader(t, testRepo, read.OpenChain)
97
98 defer func() { _ = reader.Close() }()
99
100 layers := reader.Layers()
101 if len(layers) < 2 {
102 t.Fatalf("Layers len = %d, want >= 2", len(layers))
103 }
104
105 allIDs := testRepo.RevList(t, "--all")
106
107 wantCommitCount, err := intconv.IntToUint32(len(allIDs))
108 if err != nil {
109 t.Fatalf("len(allIDs) convert: %v", err)
110 }
111
112 if got := reader.NumCommits(); got != wantCommitCount {
113 t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs))
114 }
115
116 step := max(len(allIDs)/20, 1)
117
118 for i, id := range allIDs {
119 pos, err := reader.Lookup(id)
120 if err != nil {
121 t.Fatalf("Lookup(%s): %v", id, err)
122 }
123
124 if i%step != 0 && i != len(allIDs)-1 {
125 continue
126 }
127
128 gotID, err := reader.OIDAt(pos)
129 if err != nil {
130 t.Fatalf("OIDAt(%+v): %v", pos, err)
131 }
132
133 if gotID != id {
134 t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id)
135 }
136 }
137 })
138}
139
140func TestBloomUnavailableWithoutChangedPaths(t *testing.T) {
141 t.Parallel()
142
143 testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
144 testRepo := fixtureRepo(t, algo, "single_nochanged")
145
146 reader := openReader(t, testRepo, read.OpenSingle)
147
148 defer func() { _ = reader.Close() }()
149
150 head := testRepo.RevParse(t, "HEAD")
151
152 pos, err := reader.Lookup(head)
153 if err != nil {
154 t.Fatalf("Lookup(%s): %v", head, err)
155 }
156
157 _, err = reader.BloomFilterAt(pos)
158 if err == nil {
159 t.Fatal("BloomFilterAt() error = nil, want BloomUnavailableError")
160 }
161
162 unavailable, ok := errors.AsType[*read.BloomUnavailableError](err)
163 if !ok {
164 t.Fatalf("BloomFilterAt() error type = %T, want *BloomUnavailableError", err)
165 }
166
167 if unavailable.Pos != pos {
168 t.Fatalf("BloomUnavailableError.Pos = %+v, want %+v", unavailable.Pos, pos)
169 }
170 })
171}
172
173func openReader(tb testing.TB, testRepo *testgit.TestRepo, mode read.OpenMode) *read.Reader {
174 tb.Helper()
175
176 root := testRepo.OpenObjectsRoot(tb)
177
178 reader, err := read.Open(root, testRepo.Algorithm(), mode)
179 if err != nil {
180 tb.Fatalf("read.Open(objects): %v", err)
181 }
182
183 return reader
184}
185
186func verifyCommitAgainstGit(tb testing.TB, testRepo *testgit.TestRepo, reader *read.Reader, id objectid.ObjectID) {
187 tb.Helper()
188
189 pos, err := reader.Lookup(id)
190 if err != nil {
191 tb.Fatalf("Lookup(%s): %v", id, err)
192 }
193
194 commit, err := reader.CommitAt(pos)
195 if err != nil {
196 tb.Fatalf("CommitAt(%+v): %v", pos, err)
197 }
198
199 if commit.OID != id {
200 tb.Fatalf("CommitAt(%+v).OID = %s, want %s", pos, commit.OID, id)
201 }
202
203 treeHex := testRepo.Run(tb, "show", "-s", "--format=%T", id.String())
204
205 wantTree, err := objectid.ParseHex(testRepo.Algorithm(), treeHex)
206 if err != nil {
207 tb.Fatalf("parse tree id %q: %v", treeHex, err)
208 }
209
210 if commit.TreeOID != wantTree {
211 tb.Fatalf("CommitAt(%+v).TreeOID = %s, want %s", pos, commit.TreeOID, wantTree)
212 }
213
214 wantParents := parseOIDLine(tb, testRepo.Algorithm(), testRepo.Run(tb, "show", "-s", "--format=%P", id.String()))
215
216 gotParents := commitParents(tb, reader, commit)
217 if len(gotParents) != len(wantParents) {
218 tb.Fatalf("parent count for %s = %d, want %d", id, len(gotParents), len(wantParents))
219 }
220
221 for i := range gotParents {
222 if gotParents[i] != wantParents[i] {
223 tb.Fatalf("parent %d for %s = %s, want %s", i, id, gotParents[i], wantParents[i])
224 }
225 }
226
227 commitTimeRaw := testRepo.Run(tb, "show", "-s", "--format=%ct", id.String())
228
229 wantCommitTime, err := strconv.ParseInt(strings.TrimSpace(commitTimeRaw), 10, 64)
230 if err != nil {
231 tb.Fatalf("parse commit time %q: %v", commitTimeRaw, err)
232 }
233
234 if commit.CommitTimeUnix != wantCommitTime {
235 tb.Fatalf("CommitAt(%+v).CommitTimeUnix = %d, want %d", pos, commit.CommitTimeUnix, wantCommitTime)
236 }
237
238 filter, err := reader.BloomFilterAt(pos)
239 if err != nil {
240 tb.Fatalf("BloomFilterAt(%+v): %v", pos, err)
241 }
242
243 if filter.HashVersion != uint32(reader.BloomVersion()) {
244 tb.Fatalf("filter.HashVersion = %d, want %d", filter.HashVersion, reader.BloomVersion())
245 }
246
247 assertChangedPathsBloomPositive(tb, testRepo, filter, id)
248}
249
250func commitParents(tb testing.TB, reader *read.Reader, commit read.Commit) []objectid.ObjectID {
251 tb.Helper()
252
253 out := make([]objectid.ObjectID, 0, 2+len(commit.ExtraParents))
254
255 if commit.Parent1.Valid {
256 id, err := reader.OIDAt(commit.Parent1.Pos)
257 if err != nil {
258 tb.Fatalf("OIDAt(parent1 %+v): %v", commit.Parent1.Pos, err)
259 }
260
261 out = append(out, id)
262 }
263
264 if commit.Parent2.Valid {
265 id, err := reader.OIDAt(commit.Parent2.Pos)
266 if err != nil {
267 tb.Fatalf("OIDAt(parent2 %+v): %v", commit.Parent2.Pos, err)
268 }
269
270 out = append(out, id)
271 }
272
273 for _, parentPos := range commit.ExtraParents {
274 id, err := reader.OIDAt(parentPos)
275 if err != nil {
276 tb.Fatalf("OIDAt(extra parent %+v): %v", parentPos, err)
277 }
278
279 out = append(out, id)
280 }
281
282 return out
283}
284
285func assertChangedPathsBloomPositive(tb testing.TB, testRepo *testgit.TestRepo, filter *bloom.Filter, commitID objectid.ObjectID) {
286 tb.Helper()
287
288 changedPaths := testRepo.Run(tb, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commitID.String())
289 for line := range strings.SplitSeq(strings.TrimSpace(changedPaths), "\n") {
290 path := strings.TrimSpace(line)
291 if path == "" {
292 continue
293 }
294
295 mightContain, err := filter.MightContain([]byte(path))
296 if err != nil {
297 tb.Fatalf("MightContain(%q): %v", path, err)
298 }
299
300 if !mightContain {
301 tb.Fatalf("Bloom filter false negative for commit %s path %q", commitID, path)
302 }
303 }
304}
305
306func parseOIDLine(tb testing.TB, algo objectid.Algorithm, line string) []objectid.ObjectID {
307 tb.Helper()
308
309 toks := strings.Fields(line)
310
311 out := make([]objectid.ObjectID, 0, len(toks))
312 for _, tok := range toks {
313 id, err := objectid.ParseHex(algo, tok)
314 if err != nil {
315 tb.Fatalf("parse object id %q: %v", tok, err)
316 }
317
318 out = append(out, id)
319 }
320
321 return out
322}