1import { assert, describe, it } from "@effect/vitest";
2
3import { parseDiffFileRevisions } from "./diffFileRevisions.ts";
4
5function patchOf(...lines: ReadonlyArray<string>): string {
6 return `${lines.join("\n")}\n`;
7}
8
9describe("parseDiffFileRevisions", () => {
10 it("reads the head id of a changed file off its index line", () => {
11 const revisions = parseDiffFileRevisions(
12 patchOf(
13 "diff --git a/src/a.ts b/src/a.ts",
14 "index 7f2aa0ab6..b4a2a7c9a 100644",
15 "--- a/src/a.ts",
16 "+++ b/src/a.ts",
17 "@@ -1 +1 @@",
18 "-a",
19 "+b",
20 ),
21 );
22
23 assert.deepStrictEqual([...revisions], [["src/a.ts", "b4a2a7c9a"]]);
24 });
25
26 it("names a deletion by the path it had, which is the one still on screen", () => {
27 const revisions = parseDiffFileRevisions(
28 patchOf(
29 "diff --git a/gone.ts b/gone.ts",
30 "deleted file mode 100644",
31 "index 1111111..0000000",
32 "--- a/gone.ts",
33 "+++ /dev/null",
34 "@@ -1 +0,0 @@",
35 "-x",
36 ),
37 );
38
39 assert.deepStrictEqual([...revisions], [["gone.ts", "0000000"]]);
40 });
41
42 it("names a rename by where it moved to", () => {
43 const revisions = parseDiffFileRevisions(
44 patchOf(
45 "diff --git a/old.ts b/new.ts",
46 "similarity index 90%",
47 "rename from old.ts",
48 "rename to new.ts",
49 "index 2222222..3333333 100644",
50 "--- a/old.ts",
51 "+++ b/new.ts",
52 "@@ -1 +1 @@",
53 "-a",
54 "+b",
55 ),
56 );
57
58 assert.deepStrictEqual([...revisions], [["new.ts", "3333333"]]);
59 });
60
61 it("names a rename that changed nothing, which states no paths of its own", () => {
62 const revisions = parseDiffFileRevisions(
63 patchOf(
64 "diff --git a/old.ts b/new.ts",
65 "similarity index 100%",
66 "rename from old.ts",
67 "rename to new.ts",
68 "index 2222222..2222222 100644",
69 ),
70 );
71
72 assert.deepStrictEqual([...revisions], [["new.ts", "2222222"]]);
73 });
74
75 it("leaves out a file Bitbucket excluded, which it gives no index line for", () => {
76 const revisions = parseDiffFileRevisions(
77 patchOf(
78 "diff --git a/package.json b/package.json",
79 "index 7f2aa0ab6..b4a2a7c9a 100644",
80 "--- a/package.json",
81 "+++ b/package.json",
82 "@@ -1 +1 @@",
83 '- "x": "1"',
84 '+ "x": "2"',
85 "diff --git a/yarn.lock b/yarn.lock",
86 'File excluded by pattern "yarn.lock"',
87 ),
88 );
89
90 assert.deepStrictEqual([...revisions], [["package.json", "b4a2a7c9a"]]);
91 });
92
93 it("stops reading headers at the first hunk, so content cannot pose as one", () => {
94 const revisions = parseDiffFileRevisions(
95 patchOf(
96 "diff --git a/notes.md b/notes.md",
97 "index aaaaaaa..bbbbbbb 100644",
98 "--- a/notes.md",
99 "+++ b/notes.md",
100 "@@ -1,2 +1,2 @@",
101 "--- a/decoy.ts",
102 "+++ b/decoy.ts",
103 "+index ccccccc..ddddddd 100644",
104 ),
105 );
106
107 assert.deepStrictEqual([...revisions], [["notes.md", "bbbbbbb"]]);
108 });
109
110 it("splits a header whose paths contain the separator, by the sides agreeing", () => {
111 const revisions = parseDiffFileRevisions(
112 patchOf(
113 "diff --git a/one b/two.ts b/one b/two.ts",
114 "index eeeeeee..fffffff 100644",
115 "@@ -1 +1 @@",
116 ),
117 );
118
119 assert.deepStrictEqual([...revisions], [["one b/two.ts", "fffffff"]]);
120 });
121
122 it("leaves out an added file that Bitbucket sent no index line for", () => {
123 const revisions = parseDiffFileRevisions(
124 patchOf("diff --git a/added.ts b/added.ts", "new file mode 100644", "--- /dev/null"),
125 );
126
127 assert.strictEqual(revisions.size, 0);
128 });
129
130 it("reads nothing out of an empty patch", () => {
131 assert.strictEqual(parseDiffFileRevisions("").size, 0);
132 });
133
134 it("reads a name git had to quote, here one holding a tab", () => {
135 const revisions = parseDiffFileRevisions(
136 patchOf(
137 'diff --git "a/we\\tird.ts" "b/we\\tird.ts"',
138 "index 4444444..5555555 100644",
139 '--- "a/we\\tird.ts"',
140 '+++ "b/we\\tird.ts"',
141 "@@ -1 +1 @@",
142 "-a",
143 "+b",
144 ),
145 );
146
147 assert.deepStrictEqual([...revisions], [["we\tird.ts", "5555555"]]);
148 });
149
150 it("rejoins the octal bytes git writes for a name outside ASCII", () => {
151 const revisions = parseDiffFileRevisions(
152 patchOf(
153 'diff --git "a/caf\\303\\251/r\\303\\251sum\\303\\251.ts" "b/caf\\303\\251/r\\303\\251sum\\303\\251.ts"',
154 "index 6666666..7777777 100644",
155 "@@ -1 +1 @@",
156 ),
157 );
158
159 assert.deepStrictEqual([...revisions], [["café/résumé.ts", "7777777"]]);
160 });
161
162 it("splits a rename header where git quoted only the side that needed it", () => {
163 const revisions = parseDiffFileRevisions(
164 patchOf(
165 'diff --git a/old.ts "b/new\\tname.ts"',
166 "similarity index 90%",
167 "rename from old.ts",
168 'rename to "new\\tname.ts"',
169 "index 8888888..9999999 100644",
170 "--- a/old.ts",
171 '+++ "b/new\\tname.ts"',
172 "@@ -1 +1 @@",
173 "-a",
174 "+b",
175 ),
176 );
177
178 assert.deepStrictEqual([...revisions], [["new\tname.ts", "9999999"]]);
179 });
180
181 it("names a quoted rename that changed nothing, which states no paths of its own", () => {
182 const revisions = parseDiffFileRevisions(
183 patchOf(
184 'diff --git "a/old\\tname.ts" "b/new\\tname.ts"',
185 "similarity index 100%",
186 'rename from "old\\tname.ts"',
187 'rename to "new\\tname.ts"',
188 "index abcabca..abcabca 100644",
189 ),
190 );
191
192 assert.deepStrictEqual([...revisions], [["new\tname.ts", "abcabca"]]);
193 });
194
195 it("keeps a character from outside the basic plane that git left unescaped", () => {
196 // `core.quotePath` off leaves the name's own bytes in place, and git still quotes the header
197 // for the tab. Encoding what it left one unit at a time would split the pair into two halves.
198 const revisions = parseDiffFileRevisions(
199 patchOf(
200 'diff --git "a/we\\tird-\u{1f680}.ts" "b/we\\tird-\u{1f680}.ts"',
201 "index aaaaaaa..bbbbbbb 100644",
202 "@@ -1 +1 @@",
203 ),
204 );
205
206 assert.deepStrictEqual([...revisions], [["we\tird-\u{1f680}.ts", "bbbbbbb"]]);
207 });
208
209 it("drops the tab git ends a name holding a space with", () => {
210 const revisions = parseDiffFileRevisions(
211 patchOf(
212 "diff --git a/with space.txt b/with space.txt",
213 "index 1111111..6178079 100644",
214 "--- a/with space.txt\t",
215 "+++ b/with space.txt\t",
216 "@@ -1 +1 @@",
217 "-a",
218 "+b",
219 ),
220 );
221
222 assert.deepStrictEqual([...revisions], [["with space.txt", "6178079"]]);
223 });
224
225 it("drops that tab from a quoted name too, where it lands past the closing quote", () => {
226 const revisions = parseDiffFileRevisions(
227 patchOf(
228 'diff --git "a/we ird\\tname.ts" "b/we ird\\tname.ts"',
229 "index 2222222..7777777 100644",
230 '--- "a/we ird\\tname.ts"\t',
231 '+++ "b/we ird\\tname.ts"\t',
232 "@@ -1 +1 @@",
233 "-a",
234 "+b",
235 ),
236 );
237
238 assert.deepStrictEqual([...revisions], [["we ird\tname.ts", "7777777"]]);
239 });
240
241 it("reads a deletion whose name holds a space, tab and all", () => {
242 const revisions = parseDiffFileRevisions(
243 patchOf(
244 "diff --git a/with space.txt b/with space.txt",
245 "deleted file mode 100644",
246 "index 3333333..0000000",
247 "--- a/with space.txt\t",
248 "+++ /dev/null",
249 "@@ -1 +0,0 @@",
250 "-a",
251 ),
252 );
253
254 assert.deepStrictEqual([...revisions], [["with space.txt", "0000000"]]);
255 });
256
257 it("drops the timestamp other producers of the format write past that tab", () => {
258 const revisions = parseDiffFileRevisions(
259 patchOf(
260 "diff --git a/stamped.ts b/stamped.ts",
261 "index 4444444..5555555 100644",
262 "--- a/stamped.ts\t2024-01-01 00:00:00.000000000 +0000",
263 "+++ b/stamped.ts\t2024-01-02 00:00:00.000000000 +0000",
264 "@@ -1 +1 @@",
265 "-a",
266 "+b",
267 ),
268 );
269
270 assert.deepStrictEqual([...revisions], [["stamped.ts", "5555555"]]);
271 });
272
273 it("splits an unquoted header whose names hold a space, by the sides agreeing", () => {
274 const revisions = parseDiffFileRevisions(
275 patchOf("diff --git a/one two b/one two", "index ddddddd..eeeeeee 100644", "@@ -1 +1 @@"),
276 );
277
278 assert.deepStrictEqual([...revisions], [["one two", "eeeeeee"]]);
279 });
280});