Fast implementation of Git in pure Go
at master 322 lines 8.1 kB view raw
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}