Fast implementation of Git in pure Go
at legacy 223 lines 10 kB view raw
1package furgit 2 3import ( 4 "os" 5 "path/filepath" 6 "testing" 7) 8 9func TestDiffTreesComplexNestedChanges(t *testing.T) { 10 repoPath, cleanup := setupTestRepo(t) 11 defer cleanup() 12 13 workDir, cleanupWork := setupWorkDir(t) 14 defer cleanupWork() 15 16 writeTestFile(t, filepath.Join(workDir, "README.md"), "initial readme\n") 17 writeTestFile(t, filepath.Join(workDir, "unchanged.txt"), "leave me as-is\n") 18 writeTestFile(t, filepath.Join(workDir, "dir", "file_a.txt"), "alpha v1\n") 19 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "file_b.txt"), "beta v1\n") 20 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "file_c.txt"), "gamma v1\n") 21 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "old.txt"), "old branch\n") 22 writeTestFile(t, filepath.Join(workDir, "treeB", "legacy.txt"), "legacy root\n") 23 writeTestFile(t, filepath.Join(workDir, "treeB", "sub", "retired.txt"), "retired\n") 24 25 gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 26 baseTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree") 27 28 writeTestFile(t, filepath.Join(workDir, "README.md"), "updated readme\n") 29 gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-f", "dir/file_a.txt") 30 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "file_b.txt"), "beta v2\n") 31 gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-f", "dir/nested/deeper/old.txt") 32 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "new.txt"), "new branch entry\n") 33 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "info.md"), "branch info\n") 34 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "subbranch", "leaf.txt"), "leaf data\n") 35 writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "subbranch", "deep", "final.txt"), "final artifact\n") 36 writeTestFile(t, filepath.Join(workDir, "dir", "newchild.txt"), "brand new sibling\n") 37 gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-r", "-f", "treeB") 38 writeTestFile(t, filepath.Join(workDir, "features", "alpha", "README.md"), "alpha docs\n") 39 writeTestFile(t, filepath.Join(workDir, "features", "alpha", "beta", "gamma.txt"), "gamma payload\n") 40 writeTestFile(t, filepath.Join(workDir, "modules", "v2", "core", "main.go"), "package core\n") 41 writeTestFile(t, filepath.Join(workDir, "root_addition.txt"), "root level file\n") 42 43 gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 44 updatedTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree") 45 46 repo, err := OpenRepository(repoPath) 47 if err != nil { 48 t.Fatalf("OpenRepository failed: %v", err) 49 } 50 defer func() { 51 _ = repo.Close() 52 }() 53 54 baseTree := readStoredTree(t, repo, baseTreeHash) 55 updatedTree := readStoredTree(t, repo, updatedTreeHash) 56 57 diffs, err := repo.DiffTrees(baseTree, updatedTree) 58 if err != nil { 59 t.Fatalf("DiffTrees failed: %v", err) 60 } 61 62 expected := map[string]diffExpectation{ 63 "README.md": {kind: TreeDiffEntryKindModified}, 64 "dir": {kind: TreeDiffEntryKindModified}, 65 "dir/file_a.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 66 "dir/newchild.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 67 "dir/nested": {kind: TreeDiffEntryKindModified}, 68 "dir/nested/file_b.txt": {kind: TreeDiffEntryKindModified}, 69 "dir/nested/deeper": {kind: TreeDiffEntryKindModified}, 70 "dir/nested/deeper/old.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 71 "dir/nested/deeper/new.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 72 "dir/nested/deeper/branch": {kind: TreeDiffEntryKindAdded, oldNil: true}, 73 "dir/nested/deeper/branch/info.md": {kind: TreeDiffEntryKindAdded, oldNil: true}, 74 "dir/nested/deeper/branch/subbranch": {kind: TreeDiffEntryKindAdded, oldNil: true}, 75 "dir/nested/deeper/branch/subbranch/leaf.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 76 "dir/nested/deeper/branch/subbranch/deep": {kind: TreeDiffEntryKindAdded, oldNil: true}, 77 "dir/nested/deeper/branch/subbranch/deep/final.txt": { 78 kind: TreeDiffEntryKindAdded, 79 oldNil: true, 80 }, 81 "features": {kind: TreeDiffEntryKindAdded, oldNil: true}, 82 "features/alpha": {kind: TreeDiffEntryKindAdded, oldNil: true}, 83 "features/alpha/README.md": {kind: TreeDiffEntryKindAdded, oldNil: true}, 84 "features/alpha/beta": {kind: TreeDiffEntryKindAdded, oldNil: true}, 85 "features/alpha/beta/gamma.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 86 "modules": {kind: TreeDiffEntryKindAdded, oldNil: true}, 87 "modules/v2": {kind: TreeDiffEntryKindAdded, oldNil: true}, 88 "modules/v2/core": {kind: TreeDiffEntryKindAdded, oldNil: true}, 89 "modules/v2/core/main.go": {kind: TreeDiffEntryKindAdded, oldNil: true}, 90 "root_addition.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 91 "treeB": {kind: TreeDiffEntryKindDeleted, newNil: true}, 92 "treeB/legacy.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 93 "treeB/sub": {kind: TreeDiffEntryKindDeleted, newNil: true}, 94 "treeB/sub/retired.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 95 } 96 97 checkDiffs(t, diffs, expected) 98} 99 100func TestDiffTreesDirectoryAddDeleteDeep(t *testing.T) { 101 repoPath, cleanup := setupTestRepo(t) 102 defer cleanup() 103 104 workDir, cleanupWork := setupWorkDir(t) 105 defer cleanupWork() 106 107 writeTestFile(t, filepath.Join(workDir, "old_dir", "old.txt"), "stale directory\n") 108 writeTestFile(t, filepath.Join(workDir, "old_dir", "sub1", "legacy.txt"), "legacy path\n") 109 writeTestFile(t, filepath.Join(workDir, "old_dir", "sub1", "nested", "end.txt"), "legacy end\n") 110 111 gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 112 originalTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree") 113 114 gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-r", "-f", "old_dir") 115 writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "beta", "new.txt"), "brand new directory\n") 116 writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "docs", "note.md"), "docs note\n") 117 writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "beta", "gamma", "delta.txt"), "delta payload\n") 118 119 gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".") 120 nextTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree") 121 122 repo, err := OpenRepository(repoPath) 123 if err != nil { 124 t.Fatalf("OpenRepository failed: %v", err) 125 } 126 defer func() { 127 _ = repo.Close() 128 }() 129 130 originalTree := readStoredTree(t, repo, originalTreeHash) 131 nextTree := readStoredTree(t, repo, nextTreeHash) 132 133 diffs, err := repo.DiffTrees(originalTree, nextTree) 134 if err != nil { 135 t.Fatalf("DiffTrees failed: %v", err) 136 } 137 138 expected := map[string]diffExpectation{ 139 "fresh": {kind: TreeDiffEntryKindAdded, oldNil: true}, 140 "fresh/alpha": {kind: TreeDiffEntryKindAdded, oldNil: true}, 141 "fresh/alpha/beta": {kind: TreeDiffEntryKindAdded, oldNil: true}, 142 "fresh/alpha/beta/new.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 143 "fresh/alpha/beta/gamma": {kind: TreeDiffEntryKindAdded, oldNil: true}, 144 "fresh/alpha/beta/gamma/delta.txt": {kind: TreeDiffEntryKindAdded, oldNil: true}, 145 "fresh/alpha/docs": {kind: TreeDiffEntryKindAdded, oldNil: true}, 146 "fresh/alpha/docs/note.md": {kind: TreeDiffEntryKindAdded, oldNil: true}, 147 "old_dir": {kind: TreeDiffEntryKindDeleted, newNil: true}, 148 "old_dir/old.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 149 "old_dir/sub1": {kind: TreeDiffEntryKindDeleted, newNil: true}, 150 "old_dir/sub1/legacy.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 151 "old_dir/sub1/nested": {kind: TreeDiffEntryKindDeleted, newNil: true}, 152 "old_dir/sub1/nested/end.txt": {kind: TreeDiffEntryKindDeleted, newNil: true}, 153 } 154 155 checkDiffs(t, diffs, expected) 156} 157 158type diffExpectation struct { 159 kind TreeDiffEntryKind 160 oldNil bool 161 newNil bool 162} 163 164func writeTestFile(t *testing.T, path string, data string) { 165 t.Helper() 166 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 167 t.Fatalf("failed to create directory for %s: %v", path, err) 168 } 169 if err := os.WriteFile(path, []byte(data), 0o644); err != nil { 170 t.Fatalf("failed to write %s: %v", path, err) 171 } 172} 173 174func readStoredTree(t *testing.T, repo *Repository, hashStr string) *StoredTree { 175 t.Helper() 176 hash, err := repo.ParseHash(hashStr) 177 if err != nil { 178 t.Fatalf("ParseHash failed: %v", err) 179 } 180 obj, err := repo.ReadObject(hash) 181 if err != nil { 182 t.Fatalf("ReadObject failed: %v", err) 183 } 184 tree, ok := obj.(*StoredTree) 185 if !ok { 186 t.Fatalf("expected *StoredTree, got %T", obj) 187 } 188 return tree 189} 190 191func checkDiffs(t *testing.T, diffs []TreeDiffEntry, expected map[string]diffExpectation) { 192 t.Helper() 193 got := make(map[string]TreeDiffEntry, len(diffs)) 194 for _, diff := range diffs { 195 key := string(diff.Path) 196 if _, exists := got[key]; exists { 197 t.Fatalf("duplicate diff entry for %q", key) 198 } 199 got[key] = diff 200 } 201 if len(got) != len(expected) { 202 t.Fatalf("unexpected diff count: got %d, want %d", len(got), len(expected)) 203 } 204 205 for path, want := range expected { 206 diff, ok := got[path] 207 if !ok { 208 t.Fatalf("missing diff for %q", path) 209 } 210 if diff.Kind != want.kind { 211 t.Errorf("%s kind: got %v, want %v", path, diff.Kind, want.kind) 212 } 213 if (diff.Old == nil) != want.oldNil { 214 t.Errorf("%s old nil mismatch: got %v, want %v", path, diff.Old == nil, want.oldNil) 215 } 216 if (diff.New == nil) != want.newNil { 217 t.Errorf("%s new nil mismatch: got %v, want %v", path, diff.New == nil, want.newNil) 218 } 219 if diff.Kind == TreeDiffEntryKindModified && diff.Old != nil && diff.New != nil && diff.Old.ID == diff.New.ID { 220 t.Errorf("%s: modified entry should change IDs", path) 221 } 222 } 223}