|
1 | 1 | import fs from "fs-extra" |
2 | 2 | import { dirname } from "path" |
3 | | -import { ParsedPatchFile, FilePatch } from "./parse" |
| 3 | +import { ParsedPatchFile, FilePatch, Hunk } from "./parse" |
4 | 4 | import { assertNever } from "../assertNever" |
5 | 5 |
|
6 | 6 | export const executeEffects = ( |
@@ -75,17 +75,8 @@ function isExecutable(fileMode: number) { |
75 | 75 | } |
76 | 76 |
|
77 | 77 | const trimRight = (s: string) => s.replace(/\s+$/, "") |
78 | | -function assertLineEquality(onDisk: string, expected: string) { |
79 | | - if (trimRight(onDisk) !== trimRight(expected)) { |
80 | | - throw new Error( |
81 | | - `Line mismatch |
82 | | -
|
83 | | - Expected: ${JSON.stringify(expected)} |
84 | | - Observed: ${JSON.stringify(onDisk)} |
85 | | -
|
86 | | -`, |
87 | | - ) |
88 | | - } |
| 78 | +function linesAreEqual(a: string, b: string) { |
| 79 | + return trimRight(a) === trimRight(b) |
89 | 80 | } |
90 | 81 |
|
91 | 82 | /** |
@@ -120,57 +111,135 @@ function applyPatch( |
120 | 111 |
|
121 | 112 | const fileLines: string[] = fileContents.split(/\n/) |
122 | 113 |
|
123 | | - // when adding or removing lines from a file, gotta |
124 | | - // make sure that the original lines in hunk headers match up |
125 | | - // this effectively measures the total +/- in line count during the course |
126 | | - // of the patching process |
127 | | - let contextIndexOffset = 0 |
128 | | - |
129 | | - for (const { parts, header } of hunks) { |
130 | | - // contextIndex is the offest from the hunk header start but in the original file |
131 | | - let contextIndex = header.original.start - 1 + contextIndexOffset |
132 | | - |
133 | | - contextIndexOffset += header.patched.length - header.original.length |
134 | | - |
135 | | - for (const part of parts) { |
136 | | - switch (part.type) { |
137 | | - case "deletion": |
138 | | - case "context": |
139 | | - for (const line of part.lines) { |
140 | | - const originalLine = fileLines[contextIndex] |
141 | | - assertLineEquality(originalLine, line) |
142 | | - contextIndex++ |
143 | | - } |
| 114 | + const result: Modificaiton[][] = [] |
144 | 115 |
|
145 | | - if (part.type === "deletion") { |
146 | | - fileLines.splice( |
147 | | - contextIndex - part.lines.length, |
148 | | - part.lines.length, |
149 | | - ) |
150 | | - contextIndex -= part.lines.length |
| 116 | + for (const hunk of hunks) { |
| 117 | + let fuzzingOffset = 0 |
| 118 | + while (true) { |
| 119 | + const modifications = evaluateHunk(hunk, fileLines, fuzzingOffset) |
| 120 | + if (modifications) { |
| 121 | + result.push(modifications) |
| 122 | + break |
| 123 | + } |
151 | 124 |
|
152 | | - if (part.noNewlineAtEndOfFile) { |
153 | | - fileLines.push("") |
154 | | - } |
155 | | - } |
| 125 | + fuzzingOffset = |
| 126 | + fuzzingOffset < 0 ? fuzzingOffset * -1 : fuzzingOffset * -1 - 1 |
| 127 | + |
| 128 | + if (Math.abs(fuzzingOffset) > 20) { |
| 129 | + throw new Error( |
| 130 | + `Cant apply hunk ${hunks.indexOf(hunk)} for file ${path}`, |
| 131 | + ) |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + if (dryRun) { |
| 137 | + return |
| 138 | + } |
| 139 | + |
| 140 | + let diffOffset = 0 |
| 141 | + |
| 142 | + for (const modifications of result) { |
| 143 | + for (const modification of modifications) { |
| 144 | + switch (modification.type) { |
| 145 | + case "splice": |
| 146 | + fileLines.splice( |
| 147 | + modification.index + diffOffset, |
| 148 | + modification.numToDelete, |
| 149 | + ...modification.linesToInsert, |
| 150 | + ) |
| 151 | + diffOffset += |
| 152 | + modification.linesToInsert.length - modification.numToDelete |
156 | 153 | break |
157 | | - case "insertion": |
158 | | - fileLines.splice(contextIndex, 0, ...part.lines) |
159 | | - contextIndex += part.lines.length |
160 | | - if (part.noNewlineAtEndOfFile) { |
161 | | - if (contextIndex !== fileLines.length - 1) { |
162 | | - throw new Error("Invalid patch application state.") |
163 | | - } |
164 | | - fileLines.pop() |
165 | | - } |
| 154 | + case "pop": |
| 155 | + fileLines.pop() |
| 156 | + break |
| 157 | + case "push": |
| 158 | + fileLines.push(modification.line) |
166 | 159 | break |
167 | 160 | default: |
168 | | - assertNever(part.type) |
| 161 | + assertNever(modification) |
169 | 162 | } |
170 | 163 | } |
171 | 164 | } |
172 | 165 |
|
173 | | - if (!dryRun) { |
174 | | - fs.writeFileSync(path, fileLines.join("\n"), { mode }) |
| 166 | + fs.writeFileSync(path, fileLines.join("\n"), { mode }) |
| 167 | +} |
| 168 | + |
| 169 | +interface Push { |
| 170 | + type: "push" |
| 171 | + line: string |
| 172 | +} |
| 173 | +interface Pop { |
| 174 | + type: "pop" |
| 175 | +} |
| 176 | +interface Splice { |
| 177 | + type: "splice" |
| 178 | + index: number |
| 179 | + numToDelete: number |
| 180 | + linesToInsert: string[] |
| 181 | +} |
| 182 | + |
| 183 | +type Modificaiton = Push | Pop | Splice |
| 184 | + |
| 185 | +function evaluateHunk( |
| 186 | + hunk: Hunk, |
| 187 | + fileLines: string[], |
| 188 | + fuzzingOffset: number, |
| 189 | +): Modificaiton[] | null { |
| 190 | + const result: Modificaiton[] = [] |
| 191 | + let contextIndex = hunk.header.original.start - 1 + fuzzingOffset |
| 192 | + // do bounds checks for index |
| 193 | + if (contextIndex < 0) { |
| 194 | + return null |
| 195 | + } |
| 196 | + if (fileLines.length - contextIndex < hunk.header.original.length) { |
| 197 | + return null |
| 198 | + } |
| 199 | + |
| 200 | + for (const part of hunk.parts) { |
| 201 | + switch (part.type) { |
| 202 | + case "deletion": |
| 203 | + case "context": |
| 204 | + for (const line of part.lines) { |
| 205 | + const originalLine = fileLines[contextIndex] |
| 206 | + if (!linesAreEqual(originalLine, line)) { |
| 207 | + return null |
| 208 | + } |
| 209 | + contextIndex++ |
| 210 | + } |
| 211 | + |
| 212 | + if (part.type === "deletion") { |
| 213 | + result.push({ |
| 214 | + type: "splice", |
| 215 | + index: contextIndex - part.lines.length, |
| 216 | + numToDelete: part.lines.length, |
| 217 | + linesToInsert: [], |
| 218 | + }) |
| 219 | + |
| 220 | + if (part.noNewlineAtEndOfFile) { |
| 221 | + result.push({ |
| 222 | + type: "push", |
| 223 | + line: "", |
| 224 | + }) |
| 225 | + } |
| 226 | + } |
| 227 | + break |
| 228 | + case "insertion": |
| 229 | + result.push({ |
| 230 | + type: "splice", |
| 231 | + index: contextIndex, |
| 232 | + numToDelete: 0, |
| 233 | + linesToInsert: part.lines, |
| 234 | + }) |
| 235 | + if (part.noNewlineAtEndOfFile) { |
| 236 | + result.push({ type: "pop" }) |
| 237 | + } |
| 238 | + break |
| 239 | + default: |
| 240 | + assertNever(part.type) |
| 241 | + } |
175 | 242 | } |
| 243 | + |
| 244 | + return result |
176 | 245 | } |
0 commit comments