From 1fa0d2bcfa7aebdcec8644f53acc58465c109b72 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Fri, 30 Jan 2026 16:44:28 +0100 Subject: reachability: Add basic reachability API Bitmaps not supported yet --- reachability_test.go | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 reachability_test.go (limited to 'reachability_test.go') diff --git a/reachability_test.go b/reachability_test.go new file mode 100644 index 00000000..2a2d5060 --- /dev/null +++ b/reachability_test.go @@ -0,0 +1,196 @@ +package furgit + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestReachabilityCommitsWantHave(t *testing.T) { + repoPath, cleanup := setupTestRepo(t) + defer cleanup() + + workDir, cleanupWork := setupWorkDir(t) + defer cleanupWork() + + var commits []string + for i := 0; i < 3; i++ { + path := filepath.Join(workDir, "file.txt") + if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") + commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD")) + } + + repo, err := OpenRepository(repoPath) + if err != nil { + t.Fatalf("OpenRepository failed: %v", err) + } + defer func() { _ = repo.Close() }() + + wantID, _ := repo.ParseHash(commits[2]) + haveID, _ := repo.ParseHash(commits[1]) + walk, err := repo.ReachableObjects(ReachabilityQuery{ + Wants: []Hash{wantID}, + Haves: []Hash{haveID}, + Mode: ReachabilityCommitsOnly, + }) + if err != nil { + t.Fatalf("ReachableObjects failed: %v", err) + } + + seen := make(map[Hash]ReachableObject) + for obj := range walk.Seq() { + seen[obj.ID] = obj + if obj.Type != ObjectTypeCommit { + t.Fatalf("unexpected object type: %v", obj.Type) + } + } + if err := walk.Err(); err != nil { + t.Fatalf("Reachability walk error: %v", err) + } + + headID := wantID + parentID, _ := repo.ParseHash(commits[1]) + rootID, _ := repo.ParseHash(commits[0]) + if _, ok := seen[headID]; !ok { + t.Fatalf("missing head commit") + } + if _, ok := seen[parentID]; !ok { + t.Fatalf("missing parent commit") + } + if _, ok := seen[rootID]; !ok { + t.Fatalf("missing root commit") + } + if seen[headID].InHave { + t.Fatalf("head commit incorrectly marked InHave") + } + if !seen[parentID].InHave || !seen[rootID].InHave { + t.Fatalf("expected parent and root commits to be InHave") + } + + inHave, err := walk.HaveContains(parentID) + if err != nil { + t.Fatalf("HaveContains failed: %v", err) + } + if !inHave { + t.Fatalf("expected parent to be reachable from have") + } +} + +func TestReachabilityAllObjects(t *testing.T) { + repoPath, cleanup := setupTestRepo(t) + defer cleanup() + + workDir, cleanupWork := setupWorkDir(t) + defer cleanupWork() + + if err := os.WriteFile(filepath.Join(workDir, "file1.txt"), []byte("one\n"), 0o644); err != nil { + t.Fatalf("write file1: %v", err) + } + if err := os.Mkdir(filepath.Join(workDir, "dir"), 0o755); err != nil { + t.Fatalf("mkdir dir: %v", err) + } + if err := os.WriteFile(filepath.Join(workDir, "dir", "file2.txt"), []byte("two\n"), 0o644); err != nil { + t.Fatalf("write file2: %v", err) + } + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") + + repo, err := OpenRepository(repoPath) + if err != nil { + t.Fatalf("OpenRepository failed: %v", err) + } + defer func() { _ = repo.Close() }() + + head := gitCmd(t, repoPath, "rev-parse", "HEAD") + wantID, _ := repo.ParseHash(head) + walk, err := repo.ReachableObjects(ReachabilityQuery{ + Wants: []Hash{wantID}, + Mode: ReachabilityAllObjects, + }) + if err != nil { + t.Fatalf("ReachableObjects failed: %v", err) + } + + seen := make(map[Hash]ObjectType) + for obj := range walk.Seq() { + seen[obj.ID] = obj.Type + } + if err := walk.Err(); err != nil { + t.Fatalf("Reachability walk error: %v", err) + } + + treeStr := gitCmd(t, repoPath, "show", "-s", "--format=%T", head) + treeID, _ := repo.ParseHash(treeStr) + lsTree := gitCmd(t, repoPath, "ls-tree", "-r", treeStr) + fields := strings.Fields(lsTree) + if len(fields) < 3 { + t.Fatalf("unexpected ls-tree output: %q", lsTree) + } + blobID, _ := repo.ParseHash(fields[2]) + + if seen[wantID] != ObjectTypeCommit { + t.Fatalf("missing commit in reachability walk") + } + if seen[treeID] != ObjectTypeTree { + t.Fatalf("missing tree in reachability walk") + } + if seen[blobID] != ObjectTypeBlob { + t.Fatalf("missing blob in reachability walk") + } +} + +func TestReachabilityStopAtHaves(t *testing.T) { + repoPath, cleanup := setupTestRepo(t) + defer cleanup() + + workDir, cleanupWork := setupWorkDir(t) + defer cleanupWork() + + var commits []string + for i := 0; i < 3; i++ { + path := filepath.Join(workDir, "file.txt") + if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") + gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit") + commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD")) + } + + repo, err := OpenRepository(repoPath) + if err != nil { + t.Fatalf("OpenRepository failed: %v", err) + } + defer func() { _ = repo.Close() }() + + wantID, _ := repo.ParseHash(commits[2]) + haveID, _ := repo.ParseHash(commits[1]) + walk, err := repo.ReachableObjects(ReachabilityQuery{ + Wants: []Hash{wantID}, + Haves: []Hash{haveID}, + Mode: ReachabilityCommitsOnly, + StopAtHaves: true, + }) + if err != nil { + t.Fatalf("ReachableObjects failed: %v", err) + } + + var got []Hash + for obj := range walk.Seq() { + got = append(got, obj.ID) + if obj.InHave { + t.Fatalf("unexpected InHave object in send set") + } + } + if err := walk.Err(); err != nil { + t.Fatalf("Reachability walk error: %v", err) + } + if len(got) != 1 || got[0] != wantID { + t.Fatalf("StopAtHaves mismatch: got %d objects", len(got)) + } +} -- cgit v1.3.1-10-gc9f91