| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { parseDiffFileRevisions } from "./diffFileRevisions.ts"; |
| 4 | |
| 5 | function patchOf(...lines: ReadonlyArray<string>): string { |
| 6 | return `${lines.join("\n")}\n`; |
| 7 | } |
| 8 | |
| 9 | describe("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 | }); |