Monorepo for Tangled
tangled.org
1package repo
2
3import (
4 "strings"
5 "testing"
6 "time"
7
8 "github.com/bluekeyes/go-gitdiff/gitdiff"
9 "github.com/go-git/go-git/v5/plumbing"
10 "github.com/go-git/go-git/v5/plumbing/object"
11 "tangled.org/core/types"
12)
13
14// parseDiff is a test helper that parses a unified diff string into gitdiff.TextFragment slices.
15func parseDiff(t *testing.T, src string) []*gitdiff.File {
16 t.Helper()
17 files, _, err := gitdiff.Parse(strings.NewReader(src))
18 if err != nil {
19 t.Fatalf("gitdiff.Parse: %v", err)
20 }
21 return files
22}
23
24// niceDiffFromParsed builds a NiceDiff from parsed gitdiff.File entries, mirroring
25// what the knotserver does in git.Diff().
26func niceDiffFromParsed(files []*gitdiff.File, commit types.Commit, stat types.DiffStat) *types.NiceDiff {
27 nd := &types.NiceDiff{Commit: commit, Stat: stat}
28 for _, f := range files {
29 d := types.Diff{
30 IsBinary: f.IsBinary,
31 IsNew: f.IsNew,
32 IsDelete: f.IsDelete,
33 IsCopy: f.IsCopy,
34 IsRename: f.IsRename,
35 }
36 d.Name.Old = f.OldName
37 d.Name.New = f.NewName
38 for _, tf := range f.TextFragments {
39 d.TextFragments = append(d.TextFragments, *tf)
40 }
41 nd.Diff = append(nd.Diff, d)
42 }
43 return nd
44}
45
46func TestRenderUnifiedDiff_nil(t *testing.T) {
47 if got := renderUnifiedDiff(nil); got != "" {
48 t.Errorf("expected empty string for nil NiceDiff, got %q", got)
49 }
50}
51
52func TestRenderUnifiedDiff_modified(t *testing.T) {
53 const src = `diff --git a/foo.go b/foo.go
54--- a/foo.go
55+++ b/foo.go
56@@ -1,3 +1,3 @@
57 package main
58-// old comment
59+// new comment
60 func main() {}
61`
62 files := parseDiff(t, src)
63 nd := niceDiffFromParsed(files, types.Commit{}, types.DiffStat{})
64 got := renderUnifiedDiff(nd)
65
66 checks := []string{
67 "diff --git a/foo.go b/foo.go\n",
68 "--- a/foo.go\n",
69 "+++ b/foo.go\n",
70 "@@ -1,3 +1,3 @@",
71 "-// old comment\n",
72 "+// new comment\n",
73 }
74 for _, want := range checks {
75 if !strings.Contains(got, want) {
76 t.Errorf("renderUnifiedDiff output missing %q\ngot:\n%s", want, got)
77 }
78 }
79}
80
81func TestRenderUnifiedDiff_newFile(t *testing.T) {
82 const src = `diff --git a/new.go b/new.go
83new file mode 100644
84--- /dev/null
85+++ b/new.go
86@@ -0,0 +1,2 @@
87+package main
88+func main() {}
89`
90 files := parseDiff(t, src)
91 nd := niceDiffFromParsed(files, types.Commit{}, types.DiffStat{})
92 got := renderUnifiedDiff(nd)
93
94 checks := []string{
95 "diff --git a/new.go b/new.go\n",
96 "new file mode 100644\n",
97 "--- /dev/null\n",
98 "+++ b/new.go\n",
99 "+package main\n",
100 }
101 for _, want := range checks {
102 if !strings.Contains(got, want) {
103 t.Errorf("renderUnifiedDiff output missing %q\ngot:\n%s", want, got)
104 }
105 }
106}
107
108func TestRenderUnifiedDiff_deletedFile(t *testing.T) {
109 const src = `diff --git a/old.go b/old.go
110deleted file mode 100644
111--- a/old.go
112+++ /dev/null
113@@ -1,2 +0,0 @@
114-package main
115-func main() {}
116`
117 files := parseDiff(t, src)
118 nd := niceDiffFromParsed(files, types.Commit{}, types.DiffStat{})
119 got := renderUnifiedDiff(nd)
120
121 checks := []string{
122 "diff --git a/old.go b/old.go\n",
123 "deleted file mode 100644\n",
124 "--- a/old.go\n",
125 "+++ /dev/null\n",
126 "-package main\n",
127 }
128 for _, want := range checks {
129 if !strings.Contains(got, want) {
130 t.Errorf("renderUnifiedDiff output missing %q\ngot:\n%s", want, got)
131 }
132 }
133}
134
135func TestRenderUnifiedDiff_renamedFile(t *testing.T) {
136 const src = `diff --git a/old.go b/renamed.go
137rename from old.go
138rename to renamed.go
139--- a/old.go
140+++ b/renamed.go
141@@ -1,2 +1,2 @@
142 package main
143-func old() {}
144+func renamed() {}
145`
146 files := parseDiff(t, src)
147 nd := niceDiffFromParsed(files, types.Commit{}, types.DiffStat{})
148 got := renderUnifiedDiff(nd)
149
150 checks := []string{
151 "diff --git a/old.go b/renamed.go\n",
152 "rename from old.go\n",
153 "rename to renamed.go\n",
154 "--- a/old.go\n",
155 "+++ b/renamed.go\n",
156 }
157 for _, want := range checks {
158 if !strings.Contains(got, want) {
159 t.Errorf("renderUnifiedDiff output missing %q\ngot:\n%s", want, got)
160 }
161 }
162}
163
164func TestRenderUnifiedDiff_multipleFiles(t *testing.T) {
165 const src = `diff --git a/a.go b/a.go
166--- a/a.go
167+++ b/a.go
168@@ -1,1 +1,1 @@
169-old a
170+new a
171diff --git a/b.go b/b.go
172--- a/b.go
173+++ b/b.go
174@@ -1,1 +1,1 @@
175-old b
176+new b
177`
178 files := parseDiff(t, src)
179 nd := niceDiffFromParsed(files, types.Commit{}, types.DiffStat{})
180 got := renderUnifiedDiff(nd)
181
182 for _, want := range []string{"diff --git a/a.go b/a.go", "diff --git a/b.go b/b.go"} {
183 if !strings.Contains(got, want) {
184 t.Errorf("missing %q in output:\n%s", want, got)
185 }
186 }
187}
188
189func TestRenderFormatPatch_nil(t *testing.T) {
190 if got := renderFormatPatch(nil); got != "" {
191 t.Errorf("expected empty string for nil NiceDiff, got %q", got)
192 }
193}
194
195func TestRenderFormatPatch_headers(t *testing.T) {
196 when := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
197 hash := plumbing.NewHash("abc1234567890000000000000000000000000000")
198
199 nd := &types.NiceDiff{
200 Commit: types.Commit{
201 Hash: hash,
202 Message: "Fix the bug\n\nThis patch resolves the long-standing issue.\n",
203 Author: object.Signature{
204 Name: "Alice Dev",
205 Email: "alice@example.com",
206 When: when,
207 },
208 },
209 Stat: types.DiffStat{FilesChanged: 1, Insertions: 2, Deletions: 1},
210 }
211
212 got := renderFormatPatch(nd)
213
214 checks := []string{
215 "From abc1234567890000000000000000000000000000 Mon Sep 17 00:00:00 2001\n",
216 "From: Alice Dev <alice@example.com>\n",
217 "Date: Fri, 15 Mar 2024 10:30:00 +0000\n",
218 "Subject: [PATCH] Fix the bug\n",
219 "This patch resolves the long-standing issue.\n",
220 "---\n",
221 " 1 file(s) changed, 2 insertion(s)(+), 1 deletion(s)(-)\n",
222 "\n--\ntangled.sh\n",
223 }
224 for _, want := range checks {
225 if !strings.Contains(got, want) {
226 t.Errorf("renderFormatPatch output missing %q\ngot:\n%s", want, got)
227 }
228 }
229}
230
231func TestRenderFormatPatch_subjectOnly(t *testing.T) {
232 // Single-line message (no body) should not emit a blank body section.
233 nd := &types.NiceDiff{
234 Commit: types.Commit{
235 Message: "Single line commit",
236 Author: object.Signature{When: time.Now()},
237 },
238 }
239 got := renderFormatPatch(nd)
240
241 if !strings.Contains(got, "Subject: [PATCH] Single line commit\n") {
242 t.Errorf("missing subject in output:\n%s", got)
243 }
244 // Body should not appear between Subject and "---"
245 parts := strings.SplitN(got, "---\n", 2)
246 if len(parts) < 2 {
247 t.Fatalf("expected '---' separator in output:\n%s", got)
248 }
249 beforeSep := parts[0]
250 // Only the blank line between headers and body should be there, no extra content.
251 afterSubject := strings.SplitN(beforeSep, "Subject: [PATCH] Single line commit\n", 2)
252 if len(afterSubject) == 2 && strings.TrimSpace(afterSubject[1]) != "" {
253 t.Errorf("unexpected body content before '---': %q", afterSubject[1])
254 }
255}
256
257func TestRenderFormatPatch_containsDiff(t *testing.T) {
258 const src = `diff --git a/foo.go b/foo.go
259--- a/foo.go
260+++ b/foo.go
261@@ -1,2 +1,2 @@
262 package main
263-// old
264+// new
265`
266 files := parseDiff(t, src)
267 nd := niceDiffFromParsed(files, types.Commit{
268 Author: object.Signature{When: time.Now()},
269 }, types.DiffStat{FilesChanged: 1, Insertions: 1, Deletions: 1})
270
271 got := renderFormatPatch(nd)
272
273 checks := []string{
274 "diff --git a/foo.go b/foo.go\n",
275 "-// old\n",
276 "+// new\n",
277 " foo.go |",
278 }
279 for _, want := range checks {
280 if !strings.Contains(got, want) {
281 t.Errorf("renderFormatPatch output missing %q\ngot:\n%s", want, got)
282 }
283 }
284}