Fast implementation of Git in pure Go
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}