blockpatch 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +9 -1
- package/README.md +100 -468
- package/conformance/README.md +29 -0
- package/conformance/cases/ambiguous-source/case.json +8 -0
- package/conformance/cases/ambiguous-target/case.json +8 -0
- package/conformance/cases/create-file/case.json +8 -0
- package/conformance/cases/crlf/case.json +10 -0
- package/conformance/cases/cross-file-relocation/case.json +12 -0
- package/conformance/cases/delete-existing-file/case.json +10 -0
- package/conformance/cases/insert-existing-file/case.json +10 -0
- package/conformance/cases/no-trailing-newline/case.json +10 -0
- package/conformance/cases/remove-file/case.json +9 -0
- package/conformance/cases/same-file-relocation/case.json +10 -0
- package/conformance/runner.mjs +221 -0
- package/dist/cli.js +1038 -526
- package/docs/behavior.md +207 -0
- package/docs/commands.md +88 -0
- package/docs/spec.md +366 -0
- package/examples/README.md +15 -0
- package/examples/create-file/README.md +9 -0
- package/examples/create-file/expected/file.txt +1 -0
- package/examples/create-file/patch.blockpatch +8 -0
- package/examples/create-file/work/.gitkeep +1 -0
- package/examples/cross-file-relocation/README.md +10 -0
- package/examples/cross-file-relocation/expected/source.txt +2 -0
- package/examples/cross-file-relocation/expected/target.txt +6 -0
- package/examples/cross-file-relocation/patch.blockpatch +26 -0
- package/examples/cross-file-relocation/work/source.txt +5 -0
- package/examples/cross-file-relocation/work/target.txt +3 -0
- package/examples/delete-existing-file/README.md +9 -0
- package/examples/delete-existing-file/expected/file.txt +2 -0
- package/examples/delete-existing-file/patch.blockpatch +10 -0
- package/examples/delete-existing-file/work/file.txt +3 -0
- package/examples/failure-ambiguous-target/README.md +9 -0
- package/examples/failure-ambiguous-target/patch.blockpatch +14 -0
- package/examples/failure-ambiguous-target/work/file.txt +5 -0
- package/examples/insert-existing-file/README.md +9 -0
- package/examples/insert-existing-file/expected/file.txt +3 -0
- package/examples/insert-existing-file/patch.blockpatch +10 -0
- package/examples/insert-existing-file/work/file.txt +2 -0
- package/examples/remove-file/README.md +9 -0
- package/examples/remove-file/expected/.gitkeep +1 -0
- package/examples/remove-file/patch.blockpatch +8 -0
- package/examples/remove-file/work/file.txt +1 -0
- package/examples/reverse/README.md +9 -0
- package/examples/reverse/expected/file.txt +4 -0
- package/examples/reverse/patch.blockpatch +14 -0
- package/examples/reverse/work/file.txt +4 -0
- package/examples/same-file-relocation/README.md +9 -0
- package/examples/same-file-relocation/expected/file.txt +4 -0
- package/examples/same-file-relocation/patch.blockpatch +14 -0
- package/examples/same-file-relocation/work/file.txt +4 -0
- package/package.json +13 -5
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,12 @@ class BlockPatchError extends Error {
|
|
|
20
20
|
function fail(code, message, details) {
|
|
21
21
|
throw new BlockPatchError(code, message, details);
|
|
22
22
|
}
|
|
23
|
+
function matchedLocations(count, truncated) {
|
|
24
|
+
return `matched ${truncated ? "at least " : ""}${count} locations`;
|
|
25
|
+
}
|
|
26
|
+
function matchCountDetails(count, truncated) {
|
|
27
|
+
return truncated ? { matches: count, matches_truncated: true } : { matches: count };
|
|
28
|
+
}
|
|
23
29
|
function boundedMatchRanges(matches, byteLength) {
|
|
24
30
|
const ranges = [];
|
|
25
31
|
for (const start of matches) {
|
|
@@ -79,10 +85,11 @@ function clamp(value, min, max) {
|
|
|
79
85
|
// src/engine.ts
|
|
80
86
|
import { Buffer as Buffer3 } from "node:buffer";
|
|
81
87
|
import { randomBytes } from "node:crypto";
|
|
82
|
-
import { chmod, mkdir, rename, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
83
|
-
import { basename, dirname, join as join2, resolve as resolve2 } from "node:path";
|
|
88
|
+
import { chmod, lstat, mkdir, rename, rmdir, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
89
|
+
import { basename, dirname, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
84
90
|
|
|
85
91
|
// src/files.ts
|
|
92
|
+
import { createHash } from "node:crypto";
|
|
86
93
|
import { lstatSync, realpathSync } from "node:fs";
|
|
87
94
|
import { readFile, stat } from "node:fs/promises";
|
|
88
95
|
async function readFileChecked(path, label) {
|
|
@@ -92,6 +99,22 @@ async function readFileChecked(path, label) {
|
|
|
92
99
|
failFileSystem(error, path, `Could not read ${label}`);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
102
|
+
async function readFileSnapshot(path, label) {
|
|
103
|
+
const bytes = await readFileChecked(path, label);
|
|
104
|
+
const info = await statChecked(path, label);
|
|
105
|
+
assertRegularFile(info, path, label);
|
|
106
|
+
return fileSnapshot(bytes, info);
|
|
107
|
+
}
|
|
108
|
+
function fileSnapshot(bytes, info) {
|
|
109
|
+
return {
|
|
110
|
+
bytes,
|
|
111
|
+
sha256: hashBytes(bytes),
|
|
112
|
+
stat: statSnapshot(info)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function hashBytes(bytes) {
|
|
116
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
117
|
+
}
|
|
95
118
|
async function statChecked(path, label) {
|
|
96
119
|
try {
|
|
97
120
|
return await stat(path);
|
|
@@ -125,6 +148,16 @@ function failFileSystem(error, path, action, phase = "io") {
|
|
|
125
148
|
const message = error instanceof Error ? `${action}: ${error.message}` : `${action}: ${path}`;
|
|
126
149
|
fail(fileSystemErrorCode(error), message, { path, phase });
|
|
127
150
|
}
|
|
151
|
+
function statSnapshot(info) {
|
|
152
|
+
return {
|
|
153
|
+
dev: info.dev,
|
|
154
|
+
ino: info.ino,
|
|
155
|
+
mode: info.mode,
|
|
156
|
+
size: info.size,
|
|
157
|
+
mtimeMs: info.mtimeMs,
|
|
158
|
+
ctimeMs: info.ctimeMs
|
|
159
|
+
};
|
|
160
|
+
}
|
|
128
161
|
function fileSystemErrorCode(error) {
|
|
129
162
|
switch (error.code) {
|
|
130
163
|
case "ENOENT":
|
|
@@ -142,8 +175,151 @@ function fileSystemErrorCode(error) {
|
|
|
142
175
|
|
|
143
176
|
// src/parser.ts
|
|
144
177
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
145
|
-
import { createHash } from "node:crypto";
|
|
178
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
146
179
|
import { posix } from "node:path";
|
|
180
|
+
|
|
181
|
+
// src/paths.ts
|
|
182
|
+
import { lstatSync as lstatSync2 } from "node:fs";
|
|
183
|
+
import { isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
184
|
+
function validateOperationPath(path, label) {
|
|
185
|
+
if (path === "") {
|
|
186
|
+
fail("invalid_path", `Invalid ${label}: ${path}`, { path, phase: "path" });
|
|
187
|
+
}
|
|
188
|
+
rejectUnsafeDisplayPath(path, label);
|
|
189
|
+
rejectAbsolutePath(path, label);
|
|
190
|
+
rejectBackslashPath(path, label);
|
|
191
|
+
}
|
|
192
|
+
function rejectUnsafeDisplayPath(path, label) {
|
|
193
|
+
if (/[\x00-\x1f\x7f-\x9f]/u.test(path)) {
|
|
194
|
+
fail("invalid_path", `${label} contains unsupported control characters`, {
|
|
195
|
+
path,
|
|
196
|
+
phase: "path"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function rejectBackslashPath(path, label) {
|
|
201
|
+
if (path.includes("\\")) {
|
|
202
|
+
fail("invalid_path", `${label} must use POSIX-style / separators: ${path}`, {
|
|
203
|
+
path,
|
|
204
|
+
phase: "path"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function rejectAbsolutePath(path, label) {
|
|
209
|
+
if (isAbsolute(path) || win32.isAbsolute(path)) {
|
|
210
|
+
fail("path_outside_cwd", `${label} must be relative to the working directory: ${path}`, {
|
|
211
|
+
path,
|
|
212
|
+
phase: "path"
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function rejectDotPathSegments(path, label) {
|
|
217
|
+
if (path.split("/").some((part) => part === "." || part === "..")) {
|
|
218
|
+
fail("invalid_path", `${label} must not contain . or .. path segments: ${path}`, {
|
|
219
|
+
path,
|
|
220
|
+
phase: "path"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function resolvePath(cwd, path, label) {
|
|
225
|
+
validateOperationPath(path, label);
|
|
226
|
+
const root = resolve(cwd);
|
|
227
|
+
const resolved = resolve(root, path);
|
|
228
|
+
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
229
|
+
if (!isInside(root, resolved)) {
|
|
230
|
+
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
231
|
+
}
|
|
232
|
+
rejectDotPathSegments(path, label);
|
|
233
|
+
rejectSymlinkComponents(root, resolved, path, label);
|
|
234
|
+
const info = lstatSyncChecked(resolved, label, path);
|
|
235
|
+
assertRegularFile(info, path, label, "path");
|
|
236
|
+
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
237
|
+
if (!isInside(realRoot, realResolved)) {
|
|
238
|
+
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
239
|
+
}
|
|
240
|
+
return resolved;
|
|
241
|
+
}
|
|
242
|
+
function resolvePathAllowMissing(cwd, path, label) {
|
|
243
|
+
validateOperationPath(path, label);
|
|
244
|
+
const root = resolve(cwd);
|
|
245
|
+
const resolved = resolve(root, path);
|
|
246
|
+
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
247
|
+
if (!isInside(root, resolved)) {
|
|
248
|
+
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
249
|
+
}
|
|
250
|
+
rejectDotPathSegments(path, label);
|
|
251
|
+
const deepestExisting = rejectExistingSymlinkComponents(root, resolved, path, label);
|
|
252
|
+
if (deepestExisting !== resolved) {
|
|
253
|
+
return { path: resolved, exists: false };
|
|
254
|
+
}
|
|
255
|
+
const info = lstatSyncChecked(resolved, label, path);
|
|
256
|
+
assertRegularFile(info, path, label, "path");
|
|
257
|
+
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
258
|
+
if (!isInside(realRoot, realResolved)) {
|
|
259
|
+
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
260
|
+
}
|
|
261
|
+
return { path: resolved, exists: true };
|
|
262
|
+
}
|
|
263
|
+
async function sameFileIdentity(left, right) {
|
|
264
|
+
if (left === right) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
const [leftInfo, rightInfo] = await Promise.all([
|
|
268
|
+
statChecked(left, "source path"),
|
|
269
|
+
statChecked(right, "destination path")
|
|
270
|
+
]);
|
|
271
|
+
return leftInfo.dev === rightInfo.dev && leftInfo.ino === rightInfo.ino;
|
|
272
|
+
}
|
|
273
|
+
function rejectSymlinkComponents(root, resolved, originalPath, label) {
|
|
274
|
+
const relativePath = relative(root, resolved);
|
|
275
|
+
if (relativePath === "") {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
let current = root;
|
|
279
|
+
for (const part of relativePath.split(sep)) {
|
|
280
|
+
current = join(current, part);
|
|
281
|
+
if (lstatSyncChecked(current, label, originalPath).isSymbolicLink()) {
|
|
282
|
+
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
283
|
+
path: originalPath,
|
|
284
|
+
phase: "path"
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function rejectExistingSymlinkComponents(root, resolved, originalPath, label) {
|
|
290
|
+
const relativePath = relative(root, resolved);
|
|
291
|
+
if (relativePath === "") {
|
|
292
|
+
return resolved;
|
|
293
|
+
}
|
|
294
|
+
let current = root;
|
|
295
|
+
for (const part of relativePath.split(sep)) {
|
|
296
|
+
const next = join(current, part);
|
|
297
|
+
let info;
|
|
298
|
+
try {
|
|
299
|
+
info = lstatSync2(next);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const code = error.code;
|
|
302
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
failFileSystem(error, originalPath, `Could not stat ${label}`, "path");
|
|
306
|
+
}
|
|
307
|
+
if (info.isSymbolicLink()) {
|
|
308
|
+
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
309
|
+
path: originalPath,
|
|
310
|
+
phase: "path"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
current = next;
|
|
314
|
+
}
|
|
315
|
+
return current;
|
|
316
|
+
}
|
|
317
|
+
function isInside(root, path) {
|
|
318
|
+
const fromRoot = relative(root, path);
|
|
319
|
+
return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/parser.ts
|
|
147
323
|
var devNull = "/dev/null";
|
|
148
324
|
var noNewlineMarker = "\";
|
|
149
325
|
var movePrefix = "blockpatch move ";
|
|
@@ -321,7 +497,7 @@ function buildTargetOnlyBlockPatch(section, targetHunk, options) {
|
|
|
321
497
|
};
|
|
322
498
|
}
|
|
323
499
|
function verifyPayloadHash(payload, payloadSha256) {
|
|
324
|
-
const actualHash =
|
|
500
|
+
const actualHash = createHash2("sha256").update(payload).digest("hex");
|
|
325
501
|
if (actualHash !== payloadSha256) {
|
|
326
502
|
fail("hash_mismatch", "payload-sha256 does not match moved payload", {
|
|
327
503
|
phase: "payload",
|
|
@@ -546,13 +722,17 @@ function parseMetadata(input) {
|
|
|
546
722
|
}
|
|
547
723
|
function parseFileHeader(line, prefix, requiredPathPrefix) {
|
|
548
724
|
const marker = `${prefix} `;
|
|
549
|
-
|
|
725
|
+
const rawPath = line?.startsWith(marker) === true ? line.slice(marker.length) : undefined;
|
|
726
|
+
if (rawPath !== undefined) {
|
|
727
|
+
rejectUnsafeDisplayPath(rawPath, `${prefix} file header path`);
|
|
728
|
+
}
|
|
729
|
+
if (rawPath?.trim() === devNull && line?.startsWith(marker)) {
|
|
550
730
|
return devNull;
|
|
551
731
|
}
|
|
552
732
|
if (line?.startsWith(`${marker}${requiredPathPrefix}`) !== true) {
|
|
553
733
|
fail("parse_error", `Patch must include a ${marker}${requiredPathPrefix}<path> or ${marker}${devNull} header`);
|
|
554
734
|
}
|
|
555
|
-
const path =
|
|
735
|
+
const path = rawPath?.trim() ?? "";
|
|
556
736
|
if (!path || path === requiredPathPrefix) {
|
|
557
737
|
fail("parse_error", `${prefix} file header must include a path`);
|
|
558
738
|
}
|
|
@@ -589,121 +769,6 @@ function text(line) {
|
|
|
589
769
|
return line?.body.toString("utf8");
|
|
590
770
|
}
|
|
591
771
|
|
|
592
|
-
// src/paths.ts
|
|
593
|
-
import { lstatSync as lstatSync2 } from "node:fs";
|
|
594
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
595
|
-
function resolvePath(cwd, path, label) {
|
|
596
|
-
if (path === "" || path.includes("\x00")) {
|
|
597
|
-
fail("invalid_path", `Invalid ${label}: ${path}`, { path, phase: "path" });
|
|
598
|
-
}
|
|
599
|
-
if (isAbsolute(path)) {
|
|
600
|
-
fail("path_outside_cwd", `${label} must be relative to the working directory: ${path}`, {
|
|
601
|
-
path,
|
|
602
|
-
phase: "path"
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
const root = resolve(cwd);
|
|
606
|
-
const resolved = resolve(root, path);
|
|
607
|
-
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
608
|
-
if (!isInside(root, resolved)) {
|
|
609
|
-
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
610
|
-
}
|
|
611
|
-
rejectSymlinkComponents(root, resolved, path, label);
|
|
612
|
-
const info = lstatSyncChecked(resolved, label, path);
|
|
613
|
-
assertRegularFile(info, path, label, "path");
|
|
614
|
-
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
615
|
-
if (!isInside(realRoot, realResolved)) {
|
|
616
|
-
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
617
|
-
}
|
|
618
|
-
return resolved;
|
|
619
|
-
}
|
|
620
|
-
function resolvePathAllowMissing(cwd, path, label) {
|
|
621
|
-
if (path === "" || path.includes("\x00")) {
|
|
622
|
-
fail("invalid_path", `Invalid ${label}: ${path}`, { path, phase: "path" });
|
|
623
|
-
}
|
|
624
|
-
if (isAbsolute(path)) {
|
|
625
|
-
fail("path_outside_cwd", `${label} must be relative to the working directory: ${path}`, {
|
|
626
|
-
path,
|
|
627
|
-
phase: "path"
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
const root = resolve(cwd);
|
|
631
|
-
const resolved = resolve(root, path);
|
|
632
|
-
const realRoot = realpathSyncChecked(root, "working directory", root);
|
|
633
|
-
if (!isInside(root, resolved)) {
|
|
634
|
-
fail("path_outside_cwd", `${label} escapes the working directory: ${path}`, { path, phase: "path" });
|
|
635
|
-
}
|
|
636
|
-
const deepestExisting = rejectExistingSymlinkComponents(root, resolved, path, label);
|
|
637
|
-
if (deepestExisting !== resolved) {
|
|
638
|
-
return { path: resolved, exists: false };
|
|
639
|
-
}
|
|
640
|
-
const info = lstatSyncChecked(resolved, label, path);
|
|
641
|
-
assertRegularFile(info, path, label, "path");
|
|
642
|
-
const realResolved = realpathSyncChecked(resolved, label, path);
|
|
643
|
-
if (!isInside(realRoot, realResolved)) {
|
|
644
|
-
fail("path_outside_cwd", `${label} resolves outside the working directory: ${path}`, { path, phase: "path" });
|
|
645
|
-
}
|
|
646
|
-
return { path: resolved, exists: true };
|
|
647
|
-
}
|
|
648
|
-
async function sameFileIdentity(left, right) {
|
|
649
|
-
if (left === right) {
|
|
650
|
-
return true;
|
|
651
|
-
}
|
|
652
|
-
const [leftInfo, rightInfo] = await Promise.all([
|
|
653
|
-
statChecked(left, "source path"),
|
|
654
|
-
statChecked(right, "destination path")
|
|
655
|
-
]);
|
|
656
|
-
return leftInfo.dev === rightInfo.dev && leftInfo.ino === rightInfo.ino;
|
|
657
|
-
}
|
|
658
|
-
function rejectSymlinkComponents(root, resolved, originalPath, label) {
|
|
659
|
-
const relativePath = relative(root, resolved);
|
|
660
|
-
if (relativePath === "") {
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
let current = root;
|
|
664
|
-
for (const part of relativePath.split(sep)) {
|
|
665
|
-
current = join(current, part);
|
|
666
|
-
if (lstatSyncChecked(current, label, originalPath).isSymbolicLink()) {
|
|
667
|
-
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
668
|
-
path: originalPath,
|
|
669
|
-
phase: "path"
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
function rejectExistingSymlinkComponents(root, resolved, originalPath, label) {
|
|
675
|
-
const relativePath = relative(root, resolved);
|
|
676
|
-
if (relativePath === "") {
|
|
677
|
-
return resolved;
|
|
678
|
-
}
|
|
679
|
-
let current = root;
|
|
680
|
-
for (const part of relativePath.split(sep)) {
|
|
681
|
-
const next = join(current, part);
|
|
682
|
-
let info;
|
|
683
|
-
try {
|
|
684
|
-
info = lstatSync2(next);
|
|
685
|
-
} catch (error) {
|
|
686
|
-
const code = error.code;
|
|
687
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
688
|
-
return current;
|
|
689
|
-
}
|
|
690
|
-
failFileSystem(error, originalPath, `Could not stat ${label}`, "path");
|
|
691
|
-
}
|
|
692
|
-
if (info.isSymbolicLink()) {
|
|
693
|
-
fail("symlink_path", `${label} must not contain symbolic links: ${originalPath}`, {
|
|
694
|
-
path: originalPath,
|
|
695
|
-
phase: "path"
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
current = next;
|
|
699
|
-
}
|
|
700
|
-
return current;
|
|
701
|
-
}
|
|
702
|
-
function isInside(root, path) {
|
|
703
|
-
const fromRoot = relative(root, path);
|
|
704
|
-
return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
772
|
// src/engine.ts
|
|
708
773
|
async function checkPatchFile(patchPath, options = {}) {
|
|
709
774
|
return runPatchFile(patchPath, { ...options, dryRun: true });
|
|
@@ -713,7 +778,8 @@ async function checkPatchBytes(patchBytes, options = {}) {
|
|
|
713
778
|
}
|
|
714
779
|
function checkPatchBytesInMemory(patchBytes, files, options = {}) {
|
|
715
780
|
const patch = parseBlockPatch(patchBytes, { stripComponents: options.stripComponents });
|
|
716
|
-
|
|
781
|
+
const effectivePatch = options.reverse === true ? reverseMovePatch(patch) : patch;
|
|
782
|
+
return planMovePatch(effectivePatch, memoryFileMap(files)).result;
|
|
717
783
|
}
|
|
718
784
|
async function applyPatchFile(patchPath, options = {}) {
|
|
719
785
|
return runPatchFile(patchPath, options);
|
|
@@ -733,119 +799,179 @@ async function runPatchBytes(patchBytes, options) {
|
|
|
733
799
|
}
|
|
734
800
|
async function applyMovePatch(patch, cwd, dryRun, reverse) {
|
|
735
801
|
const effectivePatch = reverse ? reverseMovePatch(patch) : patch;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
802
|
+
const workspace = await readPatchWorkspace(effectivePatch, cwd);
|
|
803
|
+
const plan = planMovePatch(effectivePatch, workspace.files);
|
|
804
|
+
if (!dryRun) {
|
|
805
|
+
await commitPatchMutations(plan.mutations, workspace.paths);
|
|
806
|
+
}
|
|
807
|
+
return resultWithWriteFlag(plan.result, dryRun);
|
|
808
|
+
}
|
|
809
|
+
async function readPatchWorkspace(patch, cwd) {
|
|
810
|
+
const workspace = {
|
|
811
|
+
files: new Map,
|
|
812
|
+
paths: new Map
|
|
813
|
+
};
|
|
814
|
+
if (patch.src === null || patch.dst === null) {
|
|
815
|
+
if (patch.src === null && patch.dst !== null && !patch.hasSourceHunk) {
|
|
816
|
+
const resolved = resolvePathAllowMissing(cwd, patch.dst, "destination path");
|
|
817
|
+
if (resolved.exists) {
|
|
818
|
+
rememberFile(workspace, patch.dst, resolved.path, await readFileSnapshot(resolved.path, "destination file"));
|
|
819
|
+
} else {
|
|
820
|
+
rememberMissingPath(workspace, patch.dst, resolved.path);
|
|
821
|
+
}
|
|
822
|
+
return workspace;
|
|
823
|
+
}
|
|
824
|
+
if (patch.dst === null && patch.src !== null && patch.hasSourceHunk) {
|
|
825
|
+
const resolved = resolvePathAllowMissing(cwd, patch.src, "source path");
|
|
826
|
+
if (resolved.exists) {
|
|
827
|
+
rememberFile(workspace, patch.src, resolved.path, await readFileSnapshot(resolved.path, "source file"));
|
|
828
|
+
} else {
|
|
829
|
+
rememberMissingPath(workspace, patch.src, resolved.path);
|
|
830
|
+
}
|
|
831
|
+
return workspace;
|
|
742
832
|
}
|
|
743
833
|
fail("parse_error", "Invalid /dev/null endpoint move shape");
|
|
744
834
|
}
|
|
745
|
-
if (!
|
|
746
|
-
|
|
835
|
+
if (!patch.hasSourceHunk) {
|
|
836
|
+
const dstPath2 = resolvePath(cwd, patch.dst, "destination path");
|
|
837
|
+
rememberFile(workspace, patch.dst, dstPath2, await readFileSnapshot(dstPath2, "destination file"));
|
|
838
|
+
return workspace;
|
|
747
839
|
}
|
|
748
|
-
if (
|
|
749
|
-
|
|
840
|
+
if (patch.target === null) {
|
|
841
|
+
const srcPath2 = resolvePath(cwd, patch.src, "source path");
|
|
842
|
+
rememberFile(workspace, patch.src, srcPath2, await readFileSnapshot(srcPath2, "source file"));
|
|
843
|
+
return workspace;
|
|
750
844
|
}
|
|
751
|
-
const
|
|
752
|
-
const
|
|
753
|
-
const srcPath = resolvePath(cwd, srcLabel, "source path");
|
|
754
|
-
const dstPath = resolvePath(cwd, dstLabel, "destination path");
|
|
845
|
+
const srcPath = resolvePath(cwd, patch.src, "source path");
|
|
846
|
+
const dstPath = resolvePath(cwd, patch.dst, "destination path");
|
|
755
847
|
const sameFile = await sameFileIdentity(srcPath, dstPath);
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
848
|
+
const srcSnapshot = await readFileSnapshot(srcPath, "source file");
|
|
849
|
+
const dstSnapshot = sameFile ? srcSnapshot : await readFileSnapshot(dstPath, "destination file");
|
|
850
|
+
const identity = sameFile ? "paired-file" : undefined;
|
|
851
|
+
rememberFile(workspace, patch.src, srcPath, srcSnapshot, identity);
|
|
852
|
+
rememberFile(workspace, patch.dst, dstPath, dstSnapshot, identity);
|
|
853
|
+
return workspace;
|
|
854
|
+
}
|
|
855
|
+
function rememberMissingPath(workspace, label, path) {
|
|
856
|
+
workspace.paths.set(label, { path, missing: true });
|
|
857
|
+
}
|
|
858
|
+
function rememberFile(workspace, label, path, snapshot, identity = path) {
|
|
859
|
+
workspace.paths.set(label, { path, snapshot });
|
|
860
|
+
workspace.files.set(label, { bytes: snapshot.bytes, identity });
|
|
861
|
+
}
|
|
862
|
+
async function commitPatchMutations(mutations, paths) {
|
|
863
|
+
const writes = [];
|
|
864
|
+
const deletes = [];
|
|
865
|
+
for (const mutation of mutations) {
|
|
866
|
+
const pathState = mutationPath(paths, mutation.label);
|
|
867
|
+
if (mutation.kind === "write") {
|
|
868
|
+
writes.push({
|
|
869
|
+
path: pathState.path,
|
|
870
|
+
bytes: mutation.bytes,
|
|
871
|
+
create: mutation.create,
|
|
872
|
+
label: mutation.label,
|
|
873
|
+
expected: writeExpectation(mutation, pathState)
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
deletes.push({
|
|
877
|
+
path: pathState.path,
|
|
878
|
+
label: mutation.label,
|
|
879
|
+
expected: fileExpectation(mutation.label, pathState)
|
|
880
|
+
});
|
|
881
|
+
}
|
|
768
882
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
883
|
+
await writeAtomically(writes, deletes);
|
|
884
|
+
}
|
|
885
|
+
function writeExpectation(mutation, state) {
|
|
886
|
+
if (state.snapshot !== undefined) {
|
|
887
|
+
return { kind: "file", label: mutation.label, snapshot: state.snapshot };
|
|
888
|
+
}
|
|
889
|
+
if (state.missing === true && mutation.create === true) {
|
|
890
|
+
return { kind: "missing", label: mutation.label, bytesIfExists: mutation.bytes };
|
|
891
|
+
}
|
|
892
|
+
fail("parse_error", `No original state for planned write: ${mutation.label}`, { path: mutation.label, phase: "path" });
|
|
893
|
+
}
|
|
894
|
+
function fileExpectation(label, state) {
|
|
895
|
+
if (state.snapshot !== undefined) {
|
|
896
|
+
return { kind: "file", label, snapshot: state.snapshot };
|
|
897
|
+
}
|
|
898
|
+
fail("parse_error", `No original file state for planned mutation: ${label}`, { path: label, phase: "path" });
|
|
899
|
+
}
|
|
900
|
+
function mutationPath(paths, label) {
|
|
901
|
+
const state = paths.get(label);
|
|
902
|
+
if (state === undefined) {
|
|
903
|
+
fail("parse_error", `No resolved path for planned mutation: ${label}`, { path: label, phase: "path" });
|
|
904
|
+
}
|
|
905
|
+
return state;
|
|
906
|
+
}
|
|
907
|
+
function resultWithWriteFlag(result, dryRun) {
|
|
781
908
|
return {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
written: !dryRun && changed.length > 0,
|
|
785
|
-
noop: changed.length === 0,
|
|
786
|
-
status: changed.length === 0 ? "noop" : "applied",
|
|
787
|
-
moves: [
|
|
788
|
-
moveResultDetails({
|
|
789
|
-
id: effectivePatch.id,
|
|
790
|
-
src: srcLabel,
|
|
791
|
-
dst: dstLabel,
|
|
792
|
-
payloadSha256: effectivePatch.payloadSha256,
|
|
793
|
-
selection
|
|
794
|
-
})
|
|
795
|
-
]
|
|
909
|
+
...result,
|
|
910
|
+
written: result.status === "applied" && !dryRun && result.changed.length > 0
|
|
796
911
|
};
|
|
797
912
|
}
|
|
798
|
-
function
|
|
799
|
-
const effectivePatch = reverse ? reverseMovePatch(patch) : patch;
|
|
913
|
+
function planMovePatch(effectivePatch, files) {
|
|
800
914
|
if (effectivePatch.src === null || effectivePatch.dst === null) {
|
|
801
915
|
if (effectivePatch.src === null && effectivePatch.dst !== null && !effectivePatch.hasSourceHunk) {
|
|
802
|
-
return
|
|
916
|
+
return planPathCreationMove(effectivePatch, effectivePatch.dst, files);
|
|
803
917
|
}
|
|
804
918
|
if (effectivePatch.dst === null && effectivePatch.src !== null && effectivePatch.hasSourceHunk) {
|
|
805
|
-
return
|
|
919
|
+
return planPathDeletionMove(effectivePatch, effectivePatch.src, files);
|
|
806
920
|
}
|
|
807
921
|
fail("parse_error", "Invalid /dev/null endpoint move shape");
|
|
808
922
|
}
|
|
809
923
|
if (!effectivePatch.hasSourceHunk) {
|
|
810
|
-
return
|
|
924
|
+
return planInFileInsertionMove(effectivePatch, effectivePatch.src, effectivePatch.dst, files);
|
|
811
925
|
}
|
|
812
926
|
if (effectivePatch.target === null) {
|
|
813
|
-
return
|
|
927
|
+
return planInFileDeletionMove(effectivePatch, effectivePatch.src, effectivePatch.dst, files);
|
|
928
|
+
}
|
|
929
|
+
return planPairedMove(effectivePatch, files);
|
|
930
|
+
}
|
|
931
|
+
function planPairedMove(patch, files) {
|
|
932
|
+
if (patch.src === null || patch.dst === null) {
|
|
933
|
+
fail("parse_error", "Paired move requires source and destination paths");
|
|
814
934
|
}
|
|
815
|
-
const srcLabel =
|
|
816
|
-
const dstLabel =
|
|
935
|
+
const srcLabel = patch.src;
|
|
936
|
+
const dstLabel = patch.dst;
|
|
817
937
|
const srcFile = readMemoryFile(files, srcLabel, "source file");
|
|
818
938
|
const sameFile = sameMemoryFile(files, srcLabel, dstLabel);
|
|
819
939
|
const dstFile = sameFile ? srcFile : readMemoryFile(files, dstLabel, "destination file");
|
|
820
|
-
const plan = selectMovePlan(srcFile, dstFile,
|
|
940
|
+
const plan = selectMovePlan(srcFile, dstFile, patch, sameFile);
|
|
821
941
|
if (plan.status === "already_applied") {
|
|
822
942
|
return {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
943
|
+
result: {
|
|
944
|
+
changed: [],
|
|
945
|
+
affected: unique([srcLabel, dstLabel]),
|
|
946
|
+
written: false,
|
|
947
|
+
noop: true,
|
|
948
|
+
status: "already_applied",
|
|
949
|
+
moves: [plan.details]
|
|
950
|
+
},
|
|
951
|
+
mutations: []
|
|
829
952
|
};
|
|
830
953
|
}
|
|
831
954
|
const selection = plan.selection;
|
|
832
955
|
const next = sameFile ? applyMove(srcFile, selection) : applyCrossFileMove(srcFile, dstFile, selection);
|
|
833
956
|
const changed = changedMoveLabels(srcLabel, dstLabel, sameFile, srcFile, dstFile, next);
|
|
834
957
|
return {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
958
|
+
result: {
|
|
959
|
+
changed,
|
|
960
|
+
affected: unique([srcLabel, dstLabel]),
|
|
961
|
+
written: false,
|
|
962
|
+
noop: changed.length === 0,
|
|
963
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
964
|
+
moves: [
|
|
965
|
+
moveResultDetails({
|
|
966
|
+
id: patch.id,
|
|
967
|
+
src: srcLabel,
|
|
968
|
+
dst: dstLabel,
|
|
969
|
+
payloadSha256: patch.payloadSha256,
|
|
970
|
+
selection
|
|
971
|
+
})
|
|
972
|
+
]
|
|
973
|
+
},
|
|
974
|
+
mutations: moveMutations(srcLabel, dstLabel, sameFile, srcFile, dstFile, next)
|
|
849
975
|
};
|
|
850
976
|
}
|
|
851
977
|
function reverseMovePatch(patch) {
|
|
@@ -864,64 +990,24 @@ function reverseMovePatch(patch) {
|
|
|
864
990
|
target: reversedTarget
|
|
865
991
|
};
|
|
866
992
|
}
|
|
867
|
-
|
|
868
|
-
const target = requireTarget(patch);
|
|
869
|
-
const dstPath = resolvePath(cwd, dstLabel, "destination path");
|
|
870
|
-
const original = await readFileChecked(dstPath, "destination file");
|
|
871
|
-
const alreadyApplied = findAlreadyAppliedTargetSelection(original, patch, dstLabel);
|
|
872
|
-
if (alreadyApplied !== undefined) {
|
|
873
|
-
return oneSidedResult({
|
|
874
|
-
patch,
|
|
875
|
-
src: srcLabel,
|
|
876
|
-
dst: dstLabel,
|
|
877
|
-
changed: [],
|
|
878
|
-
dryRun,
|
|
879
|
-
status: "already_applied",
|
|
880
|
-
sourceRange: null,
|
|
881
|
-
targetRange: alreadyApplied.range,
|
|
882
|
-
insertIndex: alreadyApplied.insertIndex
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
const selection = findTargetSelection(original, target.before, target.after, dstLabel, {
|
|
886
|
-
phase: "target",
|
|
887
|
-
anchor: "blockpatch-target"
|
|
888
|
-
});
|
|
889
|
-
const next = Buffer3.concat([
|
|
890
|
-
original.subarray(0, selection.insertIndex),
|
|
891
|
-
patch.sourcePayload,
|
|
892
|
-
original.subarray(selection.insertIndex)
|
|
893
|
-
]);
|
|
894
|
-
if (!dryRun) {
|
|
895
|
-
await writeAtomically([{ path: dstPath, bytes: next }]);
|
|
896
|
-
}
|
|
897
|
-
return oneSidedResult({
|
|
898
|
-
patch,
|
|
899
|
-
src: srcLabel,
|
|
900
|
-
dst: dstLabel,
|
|
901
|
-
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
902
|
-
dryRun,
|
|
903
|
-
status: next.equals(original) ? "noop" : "applied",
|
|
904
|
-
sourceRange: null,
|
|
905
|
-
targetRange: selection.range,
|
|
906
|
-
insertIndex: selection.insertIndex
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
function checkInFileInsertionMoveInMemory(patch, srcLabel, dstLabel, files) {
|
|
993
|
+
function planInFileInsertionMove(patch, srcLabel, dstLabel, files) {
|
|
910
994
|
const target = requireTarget(patch);
|
|
911
995
|
const original = readMemoryFile(files, dstLabel, "destination file");
|
|
912
996
|
const alreadyApplied = findAlreadyAppliedTargetSelection(original, patch, dstLabel);
|
|
913
997
|
if (alreadyApplied !== undefined) {
|
|
914
|
-
return
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
998
|
+
return {
|
|
999
|
+
result: oneSidedResult({
|
|
1000
|
+
patch,
|
|
1001
|
+
src: srcLabel,
|
|
1002
|
+
dst: dstLabel,
|
|
1003
|
+
changed: [],
|
|
1004
|
+
status: "already_applied",
|
|
1005
|
+
sourceRange: null,
|
|
1006
|
+
targetRange: alreadyApplied.range,
|
|
1007
|
+
insertIndex: alreadyApplied.insertIndex
|
|
1008
|
+
}),
|
|
1009
|
+
mutations: []
|
|
1010
|
+
};
|
|
925
1011
|
}
|
|
926
1012
|
const selection = findTargetSelection(original, target.before, target.after, dstLabel, {
|
|
927
1013
|
phase: "target",
|
|
@@ -932,85 +1018,60 @@ function checkInFileInsertionMoveInMemory(patch, srcLabel, dstLabel, files) {
|
|
|
932
1018
|
patch.sourcePayload,
|
|
933
1019
|
original.subarray(selection.insertIndex)
|
|
934
1020
|
]);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
dst: dstLabel,
|
|
939
|
-
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
940
|
-
dryRun: true,
|
|
941
|
-
status: next.equals(original) ? "noop" : "applied",
|
|
942
|
-
sourceRange: null,
|
|
943
|
-
targetRange: selection.range,
|
|
944
|
-
insertIndex: selection.insertIndex
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
async function applyInFileDeletionMove(patch, srcLabel, dstLabel, cwd, dryRun) {
|
|
948
|
-
const srcPath = resolvePath(cwd, srcLabel, "source path");
|
|
949
|
-
const original = await readFileChecked(srcPath, "source file");
|
|
950
|
-
const source = findDeletionSourceRange(original, patch, srcLabel);
|
|
951
|
-
if (source === undefined) {
|
|
952
|
-
return oneSidedResult({
|
|
1021
|
+
const changed = next.equals(original) ? [] : unique([srcLabel, dstLabel]);
|
|
1022
|
+
return {
|
|
1023
|
+
result: oneSidedResult({
|
|
953
1024
|
patch,
|
|
954
1025
|
src: srcLabel,
|
|
955
1026
|
dst: dstLabel,
|
|
956
|
-
changed
|
|
957
|
-
|
|
958
|
-
status: "already_applied",
|
|
1027
|
+
changed,
|
|
1028
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
959
1029
|
sourceRange: null,
|
|
960
|
-
targetRange:
|
|
961
|
-
insertIndex:
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (!dryRun) {
|
|
966
|
-
await writeAtomically([{ path: srcPath, bytes: next }]);
|
|
967
|
-
}
|
|
968
|
-
return oneSidedResult({
|
|
969
|
-
patch,
|
|
970
|
-
src: srcLabel,
|
|
971
|
-
dst: dstLabel,
|
|
972
|
-
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
973
|
-
dryRun,
|
|
974
|
-
status: next.equals(original) ? "noop" : "applied",
|
|
975
|
-
sourceRange: source,
|
|
976
|
-
targetRange: null,
|
|
977
|
-
insertIndex: null
|
|
978
|
-
});
|
|
1030
|
+
targetRange: selection.range,
|
|
1031
|
+
insertIndex: selection.insertIndex
|
|
1032
|
+
}),
|
|
1033
|
+
mutations: changed.length === 0 ? [] : [{ kind: "write", label: dstLabel, bytes: next }]
|
|
1034
|
+
};
|
|
979
1035
|
}
|
|
980
|
-
function
|
|
1036
|
+
function planInFileDeletionMove(patch, srcLabel, dstLabel, files) {
|
|
981
1037
|
const original = readMemoryFile(files, srcLabel, "source file");
|
|
982
1038
|
const source = findDeletionSourceRange(original, patch, srcLabel);
|
|
983
1039
|
if (source === undefined) {
|
|
984
|
-
return
|
|
1040
|
+
return {
|
|
1041
|
+
result: oneSidedResult({
|
|
1042
|
+
patch,
|
|
1043
|
+
src: srcLabel,
|
|
1044
|
+
dst: dstLabel,
|
|
1045
|
+
changed: [],
|
|
1046
|
+
status: "already_applied",
|
|
1047
|
+
sourceRange: null,
|
|
1048
|
+
targetRange: null,
|
|
1049
|
+
insertIndex: null
|
|
1050
|
+
}),
|
|
1051
|
+
mutations: []
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const next = Buffer3.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
1055
|
+
const changed = next.equals(original) ? [] : unique([srcLabel, dstLabel]);
|
|
1056
|
+
return {
|
|
1057
|
+
result: oneSidedResult({
|
|
985
1058
|
patch,
|
|
986
1059
|
src: srcLabel,
|
|
987
1060
|
dst: dstLabel,
|
|
988
|
-
changed
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
sourceRange: null,
|
|
1061
|
+
changed,
|
|
1062
|
+
status: changed.length === 0 ? "noop" : "applied",
|
|
1063
|
+
sourceRange: source,
|
|
992
1064
|
targetRange: null,
|
|
993
1065
|
insertIndex: null
|
|
994
|
-
})
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
return oneSidedResult({
|
|
998
|
-
patch,
|
|
999
|
-
src: srcLabel,
|
|
1000
|
-
dst: dstLabel,
|
|
1001
|
-
changed: next.equals(original) ? [] : unique([srcLabel, dstLabel]),
|
|
1002
|
-
dryRun: true,
|
|
1003
|
-
status: next.equals(original) ? "noop" : "applied",
|
|
1004
|
-
sourceRange: source,
|
|
1005
|
-
targetRange: null,
|
|
1006
|
-
insertIndex: null
|
|
1007
|
-
});
|
|
1066
|
+
}),
|
|
1067
|
+
mutations: changed.length === 0 ? [] : [{ kind: "write", label: srcLabel, bytes: next }]
|
|
1068
|
+
};
|
|
1008
1069
|
}
|
|
1009
1070
|
function oneSidedResult(args) {
|
|
1010
1071
|
return {
|
|
1011
1072
|
changed: args.changed,
|
|
1012
1073
|
affected: unique([args.src, args.dst]),
|
|
1013
|
-
written:
|
|
1074
|
+
written: false,
|
|
1014
1075
|
noop: args.status !== "applied" || args.changed.length === 0,
|
|
1015
1076
|
status: args.status,
|
|
1016
1077
|
moves: [
|
|
@@ -1027,12 +1088,15 @@ function oneSidedResult(args) {
|
|
|
1027
1088
|
]
|
|
1028
1089
|
};
|
|
1029
1090
|
}
|
|
1030
|
-
function
|
|
1091
|
+
function planPathCreationMove(patch, dstLabel, files) {
|
|
1031
1092
|
const fullTarget = fullTargetBytes(patch);
|
|
1032
1093
|
const original = files.get(dstLabel)?.bytes;
|
|
1033
1094
|
if (original !== undefined) {
|
|
1034
1095
|
if (original.equals(fullTarget)) {
|
|
1035
|
-
return
|
|
1096
|
+
return {
|
|
1097
|
+
result: nullSourceResult(patch, dstLabel, "already_applied", { start: 0, end: original.length }, 0),
|
|
1098
|
+
mutations: []
|
|
1099
|
+
};
|
|
1036
1100
|
}
|
|
1037
1101
|
fail("destination_exists", `Destination path for file creation already exists with different bytes: ${dstLabel}`, {
|
|
1038
1102
|
path: dstLabel,
|
|
@@ -1040,52 +1104,19 @@ function checkPathCreationMoveInMemory(patch, dstLabel, files) {
|
|
|
1040
1104
|
anchor: "blockpatch-target"
|
|
1041
1105
|
});
|
|
1042
1106
|
}
|
|
1043
|
-
return
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const fullTarget = fullTargetBytes(patch);
|
|
1048
|
-
if (resolved.exists) {
|
|
1049
|
-
const original = await readFileChecked(resolved.path, "destination file");
|
|
1050
|
-
if (original.equals(fullTarget)) {
|
|
1051
|
-
return nullSourceResult(patch, dstLabel, dryRun, "already_applied", { start: 0, end: original.length }, 0);
|
|
1052
|
-
}
|
|
1053
|
-
fail("destination_exists", `Destination path for file creation already exists with different bytes: ${dstLabel}`, {
|
|
1054
|
-
path: dstLabel,
|
|
1055
|
-
phase: "target",
|
|
1056
|
-
anchor: "blockpatch-target"
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
if (!dryRun) {
|
|
1060
|
-
await writeAtomically([{ path: resolved.path, bytes: fullTarget, create: true }]);
|
|
1061
|
-
}
|
|
1062
|
-
return nullSourceResult(patch, dstLabel, dryRun, "applied", { start: 0, end: 0 }, 0);
|
|
1107
|
+
return {
|
|
1108
|
+
result: nullSourceResult(patch, dstLabel, "applied", { start: 0, end: 0 }, 0),
|
|
1109
|
+
mutations: [{ kind: "write", label: dstLabel, bytes: fullTarget, create: true }]
|
|
1110
|
+
};
|
|
1063
1111
|
}
|
|
1064
|
-
function
|
|
1112
|
+
function planPathDeletionMove(patch, srcLabel, files) {
|
|
1065
1113
|
const original = files.get(srcLabel)?.bytes;
|
|
1066
1114
|
if (original === undefined) {
|
|
1067
|
-
return
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
fail("source_not_found", `Whole-file source payload was not found in ${srcLabel}`, {
|
|
1072
|
-
path: srcLabel,
|
|
1073
|
-
phase: "source",
|
|
1074
|
-
anchor: "blockpatch-source",
|
|
1075
|
-
matches: 0
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
return nullTargetResult(patch, srcLabel, true, "applied", {
|
|
1079
|
-
start: patch.sourceBefore.length,
|
|
1080
|
-
end: patch.sourceBefore.length + patch.sourcePayload.length
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
async function applyPathDeletionMove(patch, srcLabel, cwd, dryRun) {
|
|
1084
|
-
const resolved = resolvePathAllowMissing(cwd, srcLabel, "source path");
|
|
1085
|
-
if (!resolved.exists) {
|
|
1086
|
-
return nullTargetResult(patch, srcLabel, dryRun, "already_applied", null);
|
|
1115
|
+
return {
|
|
1116
|
+
result: nullTargetResult(patch, srcLabel, "already_applied", null),
|
|
1117
|
+
mutations: []
|
|
1118
|
+
};
|
|
1087
1119
|
}
|
|
1088
|
-
const original = await readFileChecked(resolved.path, "source file");
|
|
1089
1120
|
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1090
1121
|
if (!original.equals(fullSource)) {
|
|
1091
1122
|
fail("source_not_found", `Whole-file source payload was not found in ${srcLabel}`, {
|
|
@@ -1095,17 +1126,13 @@ async function applyPathDeletionMove(patch, srcLabel, cwd, dryRun) {
|
|
|
1095
1126
|
matches: 0
|
|
1096
1127
|
});
|
|
1097
1128
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return nullTargetResult(patch, srcLabel, dryRun, "applied", {
|
|
1106
|
-
start: patch.sourceBefore.length,
|
|
1107
|
-
end: patch.sourceBefore.length + patch.sourcePayload.length
|
|
1108
|
-
});
|
|
1129
|
+
return {
|
|
1130
|
+
result: nullTargetResult(patch, srcLabel, "applied", {
|
|
1131
|
+
start: patch.sourceBefore.length,
|
|
1132
|
+
end: patch.sourceBefore.length + patch.sourcePayload.length
|
|
1133
|
+
}),
|
|
1134
|
+
mutations: [{ kind: "delete", label: srcLabel }]
|
|
1135
|
+
};
|
|
1109
1136
|
}
|
|
1110
1137
|
function fullTargetBytes(patch) {
|
|
1111
1138
|
const target = requireTarget(patch);
|
|
@@ -1119,17 +1146,18 @@ function requireTarget(patch) {
|
|
|
1119
1146
|
}
|
|
1120
1147
|
function findDeletionSourceRange(file, patch, srcLabel) {
|
|
1121
1148
|
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1149
|
+
const fullMatchResult = indexesOfLimited(file, fullSource);
|
|
1150
|
+
const fullMatches = fullMatchResult.matches;
|
|
1151
|
+
if (fullMatches.length === 1 && !fullMatchResult.truncated) {
|
|
1124
1152
|
const start = fullMatches[0] + patch.sourceBefore.length;
|
|
1125
1153
|
return { start, end: start + patch.sourcePayload.length };
|
|
1126
1154
|
}
|
|
1127
|
-
if (fullMatches.length > 1) {
|
|
1128
|
-
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel};
|
|
1155
|
+
if (fullMatches.length > 1 || fullMatchResult.truncated) {
|
|
1156
|
+
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel}; ${matchedLocations(fullMatches.length, fullMatchResult.truncated)}`, {
|
|
1129
1157
|
path: srcLabel,
|
|
1130
1158
|
phase: "source",
|
|
1131
1159
|
anchor: "blockpatch-source",
|
|
1132
|
-
|
|
1160
|
+
...matchCountDetails(fullMatches.length, fullMatchResult.truncated),
|
|
1133
1161
|
ranges: boundedMatchRanges(fullMatches, fullSource.length),
|
|
1134
1162
|
line_ranges: boundedMatchLineRanges(file, fullMatches, fullSource.length)
|
|
1135
1163
|
});
|
|
@@ -1142,36 +1170,37 @@ function deletionAlreadyAppliedOrFail(file, patch, srcLabel) {
|
|
|
1142
1170
|
return;
|
|
1143
1171
|
}
|
|
1144
1172
|
const adjacent = Buffer3.concat([patch.sourceBefore, patch.sourceAfter]);
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1173
|
+
const adjacentMatchResult = indexesOfLimitedWhere(file, adjacent, (start) => isDeletionAlreadyAppliedMatch(file, patch, start));
|
|
1174
|
+
const adjacentMatches = adjacentMatchResult.matches;
|
|
1175
|
+
if (adjacentMatches.length === 1 && !adjacentMatchResult.truncated) {
|
|
1147
1176
|
return;
|
|
1148
1177
|
}
|
|
1149
|
-
if (adjacentMatches.length > 1) {
|
|
1178
|
+
if (adjacentMatches.length > 1 || adjacentMatchResult.truncated) {
|
|
1150
1179
|
fail("source_ambiguous", `Already-deleted source anchors are ambiguous in ${srcLabel}`, {
|
|
1151
1180
|
path: srcLabel,
|
|
1152
1181
|
phase: "source",
|
|
1153
1182
|
anchor: "blockpatch-source",
|
|
1154
|
-
|
|
1183
|
+
...matchCountDetails(adjacentMatches.length, adjacentMatchResult.truncated),
|
|
1155
1184
|
ranges: boundedMatchRanges(adjacentMatches, adjacent.length),
|
|
1156
1185
|
line_ranges: boundedMatchLineRanges(file, adjacentMatches, adjacent.length)
|
|
1157
1186
|
});
|
|
1158
1187
|
}
|
|
1159
1188
|
const envelopes = findSourceEnvelopes(file, patch);
|
|
1160
|
-
if (envelopes.length === 1) {
|
|
1189
|
+
if (envelopes.ranges.length === 1 && !envelopes.truncated) {
|
|
1161
1190
|
fail("payload_mismatch", `Source payload does not match located source anchors in ${srcLabel}`, {
|
|
1162
1191
|
path: srcLabel,
|
|
1163
1192
|
phase: "payload",
|
|
1164
1193
|
anchor: "blockpatch-source"
|
|
1165
1194
|
});
|
|
1166
1195
|
}
|
|
1167
|
-
if (envelopes.length > 1) {
|
|
1168
|
-
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel};
|
|
1196
|
+
if (envelopes.ranges.length > 1 || envelopes.truncated) {
|
|
1197
|
+
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel}; ${matchedLocations(envelopes.ranges.length, envelopes.truncated)}`, {
|
|
1169
1198
|
path: srcLabel,
|
|
1170
1199
|
phase: "source",
|
|
1171
1200
|
anchor: "blockpatch-source",
|
|
1172
|
-
|
|
1173
|
-
ranges: boundedRanges(envelopes),
|
|
1174
|
-
line_ranges: boundedLineRanges(file, envelopes)
|
|
1201
|
+
...matchCountDetails(envelopes.ranges.length, envelopes.truncated),
|
|
1202
|
+
ranges: boundedRanges(envelopes.ranges),
|
|
1203
|
+
line_ranges: boundedLineRanges(file, envelopes.ranges)
|
|
1175
1204
|
});
|
|
1176
1205
|
}
|
|
1177
1206
|
fail("source_not_found", `Source anchors were not found in ${srcLabel}`, {
|
|
@@ -1181,12 +1210,21 @@ function deletionAlreadyAppliedOrFail(file, patch, srcLabel) {
|
|
|
1181
1210
|
matches: 0
|
|
1182
1211
|
});
|
|
1183
1212
|
}
|
|
1184
|
-
function
|
|
1213
|
+
function isDeletionAlreadyAppliedMatch(file, patch, start) {
|
|
1214
|
+
if (patch.sourceBefore.length === 0) {
|
|
1215
|
+
return start === 0;
|
|
1216
|
+
}
|
|
1217
|
+
if (patch.sourceAfter.length === 0) {
|
|
1218
|
+
return start + patch.sourceBefore.length === file.length;
|
|
1219
|
+
}
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
function nullSourceResult(patch, dstLabel, status, targetRange, insertIndex) {
|
|
1185
1223
|
const applied = status === "applied";
|
|
1186
1224
|
return {
|
|
1187
1225
|
changed: applied ? [dstLabel] : [],
|
|
1188
1226
|
affected: [dstLabel],
|
|
1189
|
-
written:
|
|
1227
|
+
written: false,
|
|
1190
1228
|
noop: !applied,
|
|
1191
1229
|
status,
|
|
1192
1230
|
moves: [
|
|
@@ -1203,12 +1241,12 @@ function nullSourceResult(patch, dstLabel, dryRun, status, targetRange, insertIn
|
|
|
1203
1241
|
]
|
|
1204
1242
|
};
|
|
1205
1243
|
}
|
|
1206
|
-
function nullTargetResult(patch, srcLabel,
|
|
1244
|
+
function nullTargetResult(patch, srcLabel, status, sourceRange) {
|
|
1207
1245
|
const applied = status === "applied";
|
|
1208
1246
|
return {
|
|
1209
1247
|
changed: applied ? [srcLabel] : [],
|
|
1210
1248
|
affected: [srcLabel],
|
|
1211
|
-
written:
|
|
1249
|
+
written: false,
|
|
1212
1250
|
noop: !applied,
|
|
1213
1251
|
status,
|
|
1214
1252
|
moves: [
|
|
@@ -1252,6 +1290,12 @@ function selectMovePlan(srcFile, dstFile, patch, sameFile) {
|
|
|
1252
1290
|
})
|
|
1253
1291
|
};
|
|
1254
1292
|
}
|
|
1293
|
+
if (!sameFile) {
|
|
1294
|
+
const alreadyAppliedTarget = findAlreadyAppliedTargetSelection(dstFile, patch, dstLabel);
|
|
1295
|
+
if (alreadyAppliedTarget !== undefined) {
|
|
1296
|
+
failPartialAppliedDuplicate(srcLabel, dstLabel, patch, source, alreadyAppliedTarget);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1255
1299
|
const target = findTargetSelection(dstFile, targetAnchor.before, targetAnchor.after, dstLabel, {
|
|
1256
1300
|
phase: "target",
|
|
1257
1301
|
anchor: "blockpatch-target"
|
|
@@ -1261,6 +1305,17 @@ function selectMovePlan(srcFile, dstFile, patch, sameFile) {
|
|
|
1261
1305
|
selection: buildMoveSelection(srcFile, source, target, sameFile, dstLabel)
|
|
1262
1306
|
};
|
|
1263
1307
|
}
|
|
1308
|
+
function failPartialAppliedDuplicate(srcLabel, dstLabel, patch, source, target) {
|
|
1309
|
+
fail("partial_applied_duplicate", `Cross-file move appears partially applied: ${dstLabel} already contains the moved payload while ${srcLabel} still contains it`, {
|
|
1310
|
+
path: dstLabel,
|
|
1311
|
+
phase: "target",
|
|
1312
|
+
anchor: "blockpatch-target",
|
|
1313
|
+
source_range: source,
|
|
1314
|
+
target_range: target.range,
|
|
1315
|
+
payload_sha256: patch.payloadSha256,
|
|
1316
|
+
suggested_action: "review_then_remove_source"
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1264
1319
|
function buildMoveSelection(srcFile, source, target, sameFile, dstLabel) {
|
|
1265
1320
|
if (sameFile && rangesOverlap(source, target.range)) {
|
|
1266
1321
|
fail("target_overlaps_source", `Target anchor for ${dstLabel} overlaps the source block`, {
|
|
@@ -1283,10 +1338,20 @@ async function commitMove(args) {
|
|
|
1283
1338
|
if (!args.dryRun) {
|
|
1284
1339
|
const writes = [];
|
|
1285
1340
|
if (dstChanged || sameFileAlias && srcChanged) {
|
|
1286
|
-
writes.push({
|
|
1341
|
+
writes.push({
|
|
1342
|
+
path: args.dstPath,
|
|
1343
|
+
bytes: next.dst,
|
|
1344
|
+
label: args.dstLabel,
|
|
1345
|
+
expected: args.dstSnapshot === undefined ? undefined : { kind: "file", label: args.dstLabel, snapshot: args.dstSnapshot }
|
|
1346
|
+
});
|
|
1287
1347
|
}
|
|
1288
1348
|
if (srcChanged) {
|
|
1289
|
-
writes.push({
|
|
1349
|
+
writes.push({
|
|
1350
|
+
path: args.srcPath,
|
|
1351
|
+
bytes: next.src,
|
|
1352
|
+
label: args.srcLabel,
|
|
1353
|
+
expected: args.srcSnapshot === undefined ? undefined : { kind: "file", label: args.srcLabel, snapshot: args.srcSnapshot }
|
|
1354
|
+
});
|
|
1290
1355
|
}
|
|
1291
1356
|
await writeAtomically(writes);
|
|
1292
1357
|
}
|
|
@@ -1328,17 +1393,18 @@ function applyCrossFileMove(srcFile, dstFile, selection) {
|
|
|
1328
1393
|
function findSourceRange(srcFile, dstFile, patch) {
|
|
1329
1394
|
const srcLabel = patch.src ?? devNull;
|
|
1330
1395
|
const fullSource = Buffer3.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1396
|
+
const fullMatchResult = indexesOfLimited(srcFile, fullSource);
|
|
1397
|
+
const fullMatches = fullMatchResult.matches;
|
|
1398
|
+
if (fullMatches.length === 1 && !fullMatchResult.truncated) {
|
|
1333
1399
|
const start = fullMatches[0] + patch.sourceBefore.length;
|
|
1334
1400
|
return { start, end: start + patch.sourcePayload.length };
|
|
1335
1401
|
}
|
|
1336
|
-
if (fullMatches.length > 1) {
|
|
1337
|
-
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel};
|
|
1402
|
+
if (fullMatches.length > 1 || fullMatchResult.truncated) {
|
|
1403
|
+
fail("source_ambiguous", `Source block is ambiguous in ${srcLabel}; ${matchedLocations(fullMatches.length, fullMatchResult.truncated)}`, {
|
|
1338
1404
|
path: srcLabel,
|
|
1339
1405
|
phase: "source",
|
|
1340
1406
|
anchor: "blockpatch-source",
|
|
1341
|
-
|
|
1407
|
+
...matchCountDetails(fullMatches.length, fullMatchResult.truncated),
|
|
1342
1408
|
ranges: boundedMatchRanges(fullMatches, fullSource.length),
|
|
1343
1409
|
line_ranges: boundedMatchLineRanges(srcFile, fullMatches, fullSource.length)
|
|
1344
1410
|
});
|
|
@@ -1348,21 +1414,21 @@ function findSourceRange(srcFile, dstFile, patch) {
|
|
|
1348
1414
|
return;
|
|
1349
1415
|
}
|
|
1350
1416
|
const envelopes = findSourceEnvelopes(srcFile, patch);
|
|
1351
|
-
if (envelopes.length === 1) {
|
|
1417
|
+
if (envelopes.ranges.length === 1 && !envelopes.truncated) {
|
|
1352
1418
|
fail("payload_mismatch", `Source payload does not match located source anchors in ${srcLabel}`, {
|
|
1353
1419
|
path: srcLabel,
|
|
1354
1420
|
phase: "payload",
|
|
1355
1421
|
anchor: "blockpatch-source"
|
|
1356
1422
|
});
|
|
1357
1423
|
}
|
|
1358
|
-
if (envelopes.length > 1) {
|
|
1359
|
-
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel};
|
|
1424
|
+
if (envelopes.ranges.length > 1 || envelopes.truncated) {
|
|
1425
|
+
fail("source_ambiguous", `Source anchors are ambiguous in ${srcLabel}; ${matchedLocations(envelopes.ranges.length, envelopes.truncated)}`, {
|
|
1360
1426
|
path: srcLabel,
|
|
1361
1427
|
phase: "source",
|
|
1362
1428
|
anchor: "blockpatch-source",
|
|
1363
|
-
|
|
1364
|
-
ranges: boundedRanges(envelopes),
|
|
1365
|
-
line_ranges: boundedLineRanges(srcFile, envelopes)
|
|
1429
|
+
...matchCountDetails(envelopes.ranges.length, envelopes.truncated),
|
|
1430
|
+
ranges: boundedRanges(envelopes.ranges),
|
|
1431
|
+
line_ranges: boundedLineRanges(srcFile, envelopes.ranges)
|
|
1366
1432
|
});
|
|
1367
1433
|
}
|
|
1368
1434
|
fail("source_not_found", `Source anchors were not found in ${srcLabel}`, {
|
|
@@ -1375,16 +1441,17 @@ function findSourceRange(srcFile, dstFile, patch) {
|
|
|
1375
1441
|
function findAlreadyAppliedTargetSelection(file, patch, dstLabel = patch.dst ?? devNull) {
|
|
1376
1442
|
const target = requireTarget(patch);
|
|
1377
1443
|
const alreadyApplied = Buffer3.concat([target.before, patch.sourcePayload, target.after]);
|
|
1378
|
-
const
|
|
1444
|
+
const matchResult = indexesOfLimited(file, alreadyApplied);
|
|
1445
|
+
const matches = matchResult.matches;
|
|
1379
1446
|
if (matches.length === 0) {
|
|
1380
1447
|
return;
|
|
1381
1448
|
}
|
|
1382
|
-
if (matches.length > 1) {
|
|
1383
|
-
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel};
|
|
1449
|
+
if (matches.length > 1 || matchResult.truncated) {
|
|
1450
|
+
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel}; ${matchedLocations(matches.length, matchResult.truncated)}`, {
|
|
1384
1451
|
path: dstLabel,
|
|
1385
1452
|
phase: "target",
|
|
1386
1453
|
anchor: "blockpatch-target",
|
|
1387
|
-
matches
|
|
1454
|
+
...matchCountDetails(matches.length, matchResult.truncated),
|
|
1388
1455
|
ranges: boundedMatchRanges(matches, alreadyApplied.length),
|
|
1389
1456
|
line_ranges: boundedMatchLineRanges(file, matches, alreadyApplied.length)
|
|
1390
1457
|
});
|
|
@@ -1395,22 +1462,30 @@ function findAlreadyAppliedTargetSelection(file, patch, dstLabel = patch.dst ??
|
|
|
1395
1462
|
insertIndex: start + target.before.length
|
|
1396
1463
|
};
|
|
1397
1464
|
}
|
|
1398
|
-
function findSourceEnvelopes(file, patch) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1465
|
+
function findSourceEnvelopes(file, patch, limit = 11) {
|
|
1466
|
+
if (patch.sourceBefore.length === 0 || patch.sourceAfter.length === 0) {
|
|
1467
|
+
return { ranges: [], truncated: false };
|
|
1468
|
+
}
|
|
1469
|
+
const maxMatches = normalizedLimit(limit);
|
|
1401
1470
|
const ranges = [];
|
|
1402
|
-
|
|
1471
|
+
let beforeStart = file.indexOf(patch.sourceBefore);
|
|
1472
|
+
while (beforeStart !== -1) {
|
|
1403
1473
|
const payloadStart = beforeStart + patch.sourceBefore.length;
|
|
1404
|
-
const afterStart =
|
|
1405
|
-
if (afterStart !==
|
|
1474
|
+
const afterStart = file.indexOf(patch.sourceAfter, payloadStart);
|
|
1475
|
+
if (afterStart !== -1) {
|
|
1476
|
+
if (ranges.length >= maxMatches) {
|
|
1477
|
+
return { ranges, truncated: true };
|
|
1478
|
+
}
|
|
1406
1479
|
ranges.push({ start: payloadStart, end: afterStart });
|
|
1407
1480
|
}
|
|
1481
|
+
beforeStart = file.indexOf(patch.sourceBefore, beforeStart + 1);
|
|
1408
1482
|
}
|
|
1409
|
-
return ranges;
|
|
1483
|
+
return { ranges, truncated: false };
|
|
1410
1484
|
}
|
|
1411
1485
|
function findTargetSelection(file, before, after, dstLabel, details = {}) {
|
|
1412
1486
|
const anchor = Buffer3.concat([before, after]);
|
|
1413
|
-
const
|
|
1487
|
+
const matchResult = indexesOfLimited(file, anchor);
|
|
1488
|
+
const matches = matchResult.matches;
|
|
1414
1489
|
if (matches.length === 0) {
|
|
1415
1490
|
fail("target_not_found", `Target anchor was not found in ${dstLabel}`, {
|
|
1416
1491
|
path: dstLabel,
|
|
@@ -1418,11 +1493,11 @@ function findTargetSelection(file, before, after, dstLabel, details = {}) {
|
|
|
1418
1493
|
matches: 0
|
|
1419
1494
|
});
|
|
1420
1495
|
}
|
|
1421
|
-
if (matches.length > 1) {
|
|
1422
|
-
fail("target_ambiguous", `Target anchor is ambiguous in ${dstLabel};
|
|
1496
|
+
if (matches.length > 1 || matchResult.truncated) {
|
|
1497
|
+
fail("target_ambiguous", `Target anchor is ambiguous in ${dstLabel}; ${matchedLocations(matches.length, matchResult.truncated)}`, {
|
|
1423
1498
|
path: dstLabel,
|
|
1424
1499
|
...details,
|
|
1425
|
-
matches
|
|
1500
|
+
...matchCountDetails(matches.length, matchResult.truncated),
|
|
1426
1501
|
ranges: boundedMatchRanges(matches, anchor.length),
|
|
1427
1502
|
line_ranges: boundedMatchLineRanges(file, matches, anchor.length)
|
|
1428
1503
|
});
|
|
@@ -1433,17 +1508,45 @@ function findTargetSelection(file, before, after, dstLabel, details = {}) {
|
|
|
1433
1508
|
insertIndex: start + before.length
|
|
1434
1509
|
};
|
|
1435
1510
|
}
|
|
1436
|
-
function
|
|
1511
|
+
function indexesOfLimited(haystack, needle, limit = 11) {
|
|
1512
|
+
if (needle.length === 0) {
|
|
1513
|
+
return { matches: [], truncated: false };
|
|
1514
|
+
}
|
|
1515
|
+
const maxMatches = normalizedLimit(limit);
|
|
1516
|
+
const matches = [];
|
|
1517
|
+
let index = haystack.indexOf(needle);
|
|
1518
|
+
while (index !== -1) {
|
|
1519
|
+
if (matches.length >= maxMatches) {
|
|
1520
|
+
return { matches, truncated: true };
|
|
1521
|
+
}
|
|
1522
|
+
matches.push(index);
|
|
1523
|
+
index = haystack.indexOf(needle, index + 1);
|
|
1524
|
+
}
|
|
1525
|
+
return { matches, truncated: false };
|
|
1526
|
+
}
|
|
1527
|
+
function indexesOfLimitedWhere(haystack, needle, predicate, limit = 11) {
|
|
1437
1528
|
if (needle.length === 0) {
|
|
1438
|
-
return [];
|
|
1529
|
+
return { matches: [], truncated: false };
|
|
1439
1530
|
}
|
|
1440
|
-
const
|
|
1531
|
+
const maxMatches = normalizedLimit(limit);
|
|
1532
|
+
const matches = [];
|
|
1441
1533
|
let index = haystack.indexOf(needle);
|
|
1442
1534
|
while (index !== -1) {
|
|
1443
|
-
|
|
1535
|
+
if (predicate(index)) {
|
|
1536
|
+
if (matches.length >= maxMatches) {
|
|
1537
|
+
return { matches, truncated: true };
|
|
1538
|
+
}
|
|
1539
|
+
matches.push(index);
|
|
1540
|
+
}
|
|
1444
1541
|
index = haystack.indexOf(needle, index + 1);
|
|
1445
1542
|
}
|
|
1446
|
-
return
|
|
1543
|
+
return { matches, truncated: false };
|
|
1544
|
+
}
|
|
1545
|
+
function normalizedLimit(limit) {
|
|
1546
|
+
if (!Number.isFinite(limit)) {
|
|
1547
|
+
return Number.POSITIVE_INFINITY;
|
|
1548
|
+
}
|
|
1549
|
+
return Math.max(0, Math.trunc(limit));
|
|
1447
1550
|
}
|
|
1448
1551
|
function rangesOverlap(left, right) {
|
|
1449
1552
|
return left.start < right.end && right.start < left.end;
|
|
@@ -1491,6 +1594,19 @@ function changedMoveLabels(srcLabel, dstLabel, sameFile, srcOriginal, dstOrigina
|
|
|
1491
1594
|
}
|
|
1492
1595
|
return unique(changed);
|
|
1493
1596
|
}
|
|
1597
|
+
function moveMutations(srcLabel, dstLabel, sameFile, srcOriginal, dstOriginal, next) {
|
|
1598
|
+
const srcChanged = !next.src.equals(srcOriginal);
|
|
1599
|
+
const dstChanged = !sameFile && !next.dst.equals(dstOriginal);
|
|
1600
|
+
const sameFileAlias = sameFile && srcLabel !== dstLabel;
|
|
1601
|
+
const mutations = [];
|
|
1602
|
+
if (dstChanged || sameFileAlias && srcChanged) {
|
|
1603
|
+
mutations.push({ kind: "write", label: dstLabel, bytes: next.dst });
|
|
1604
|
+
}
|
|
1605
|
+
if (srcChanged) {
|
|
1606
|
+
mutations.push({ kind: "write", label: srcLabel, bytes: next.src });
|
|
1607
|
+
}
|
|
1608
|
+
return mutations;
|
|
1609
|
+
}
|
|
1494
1610
|
function memoryFileMap(files) {
|
|
1495
1611
|
const mapped = new Map;
|
|
1496
1612
|
for (const file of files) {
|
|
@@ -1519,36 +1635,123 @@ function sameMemoryFile(files, left, right) {
|
|
|
1519
1635
|
const rightFile = files.get(right);
|
|
1520
1636
|
return leftFile !== undefined && rightFile !== undefined && leftFile.identity === rightFile.identity;
|
|
1521
1637
|
}
|
|
1522
|
-
async function writeAtomic(path, bytes) {
|
|
1523
|
-
await writeAtomically([
|
|
1638
|
+
async function writeAtomic(path, bytes, options = {}) {
|
|
1639
|
+
await writeAtomically([
|
|
1640
|
+
{
|
|
1641
|
+
path,
|
|
1642
|
+
bytes,
|
|
1643
|
+
create: options.create,
|
|
1644
|
+
expected: options.expected,
|
|
1645
|
+
label: options.label
|
|
1646
|
+
}
|
|
1647
|
+
]);
|
|
1524
1648
|
}
|
|
1525
|
-
async function writeAtomically(writes) {
|
|
1649
|
+
async function writeAtomically(writes, deletes = []) {
|
|
1526
1650
|
const staged = [];
|
|
1527
1651
|
try {
|
|
1528
1652
|
for (const write of writes) {
|
|
1529
|
-
staged.push(await stageAtomicWrite(write
|
|
1653
|
+
staged.push(await stageAtomicWrite(write));
|
|
1530
1654
|
}
|
|
1655
|
+
const decisions = [];
|
|
1531
1656
|
for (const write of staged) {
|
|
1657
|
+
decisions.push(await verifyStagedWrite(write));
|
|
1658
|
+
}
|
|
1659
|
+
for (const deletion of deletes) {
|
|
1660
|
+
await verifyAtomicDelete(deletion);
|
|
1661
|
+
}
|
|
1662
|
+
for (const [index, write] of staged.entries()) {
|
|
1663
|
+
if (decisions[index] === "skip") {
|
|
1664
|
+
await cleanupStagedWrite(write);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1532
1667
|
try {
|
|
1533
1668
|
await rename(write.temp, write.path);
|
|
1534
1669
|
} catch (error) {
|
|
1535
1670
|
failFileSystem(error, write.path, "Could not replace file");
|
|
1536
1671
|
}
|
|
1537
1672
|
}
|
|
1673
|
+
for (const deletion of deletes) {
|
|
1674
|
+
try {
|
|
1675
|
+
await unlink(deletion.path);
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
failFileSystem(error, deletion.path, "Could not remove file");
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
await cleanupStagedWrites(staged);
|
|
1682
|
+
throw error;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async function verifyStagedWrite(write) {
|
|
1686
|
+
const expected = write.request.expected;
|
|
1687
|
+
if (expected === undefined) {
|
|
1688
|
+
return "rename";
|
|
1689
|
+
}
|
|
1690
|
+
if (expected.kind === "missing") {
|
|
1691
|
+
const current2 = await readFileSnapshotOptional(write.path, expected.label);
|
|
1692
|
+
if (current2 === undefined) {
|
|
1693
|
+
return "rename";
|
|
1694
|
+
}
|
|
1695
|
+
if (expected.bytesIfExists !== undefined && current2.bytes.equals(expected.bytesIfExists)) {
|
|
1696
|
+
return "skip";
|
|
1697
|
+
}
|
|
1698
|
+
failConcurrentModification(expected.label);
|
|
1699
|
+
}
|
|
1700
|
+
const current = await readFileSnapshotOptional(write.path, expected.label);
|
|
1701
|
+
if (current === undefined || !sameFileSnapshot(expected.snapshot, current)) {
|
|
1702
|
+
failConcurrentModification(expected.label);
|
|
1703
|
+
}
|
|
1704
|
+
return "rename";
|
|
1705
|
+
}
|
|
1706
|
+
async function verifyAtomicDelete(deletion) {
|
|
1707
|
+
const expected = deletion.expected;
|
|
1708
|
+
if (expected === undefined) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (expected.kind === "missing") {
|
|
1712
|
+
const current2 = await readFileSnapshotOptional(deletion.path, expected.label);
|
|
1713
|
+
if (current2 === undefined) {
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
failConcurrentModification(expected.label);
|
|
1717
|
+
}
|
|
1718
|
+
const current = await readFileSnapshotOptional(deletion.path, expected.label);
|
|
1719
|
+
if (current === undefined || !sameFileSnapshot(expected.snapshot, current)) {
|
|
1720
|
+
failConcurrentModification(expected.label);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
async function readFileSnapshotOptional(path, label) {
|
|
1724
|
+
try {
|
|
1725
|
+
return await readFileSnapshot(path, "current file");
|
|
1538
1726
|
} catch (error) {
|
|
1539
|
-
|
|
1727
|
+
if (error instanceof BlockPatchError && error.code === "file_not_found") {
|
|
1540
1728
|
return;
|
|
1541
|
-
}
|
|
1729
|
+
}
|
|
1730
|
+
if (error instanceof BlockPatchError && error.code === "not_regular_file") {
|
|
1731
|
+
failConcurrentModification(label);
|
|
1732
|
+
}
|
|
1542
1733
|
throw error;
|
|
1543
1734
|
}
|
|
1544
1735
|
}
|
|
1545
|
-
|
|
1736
|
+
function sameFileSnapshot(left, right) {
|
|
1737
|
+
return left.sha256 === right.sha256 && sameStatSnapshot(left.stat, right.stat);
|
|
1738
|
+
}
|
|
1739
|
+
function sameStatSnapshot(left, right) {
|
|
1740
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
1741
|
+
}
|
|
1742
|
+
function failConcurrentModification(label) {
|
|
1743
|
+
fail("concurrent_modification", `File changed after blockpatch verified its input: ${label}`, {
|
|
1744
|
+
path: label,
|
|
1745
|
+
phase: "write"
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
async function stageAtomicWrite(write) {
|
|
1546
1749
|
let mode;
|
|
1547
1750
|
let createMissing = false;
|
|
1548
|
-
if (create) {
|
|
1549
|
-
const info = await statOptional(path);
|
|
1751
|
+
if (write.create === true) {
|
|
1752
|
+
const info = await statOptional(write.path);
|
|
1550
1753
|
if (info !== undefined) {
|
|
1551
|
-
|
|
1754
|
+
assertExpectedRegularFile(info, write.path, "output file", write.expected);
|
|
1552
1755
|
mode = info.mode;
|
|
1553
1756
|
}
|
|
1554
1757
|
if (mode === undefined) {
|
|
@@ -1556,28 +1759,105 @@ async function stageAtomicWrite(path, bytes, create = false) {
|
|
|
1556
1759
|
mode = 420;
|
|
1557
1760
|
}
|
|
1558
1761
|
} else {
|
|
1559
|
-
const info = await
|
|
1560
|
-
|
|
1762
|
+
const info = await statOptional(write.path);
|
|
1763
|
+
if (info === undefined) {
|
|
1764
|
+
if (write.expected !== undefined) {
|
|
1765
|
+
failConcurrentModification(write.expected.label);
|
|
1766
|
+
}
|
|
1767
|
+
fail("file_not_found", `Could not stat output file: ${write.path}`, { path: write.path, phase: "io" });
|
|
1768
|
+
}
|
|
1769
|
+
assertExpectedRegularFile(info, write.path, "output file", write.expected);
|
|
1561
1770
|
mode = info.mode;
|
|
1562
1771
|
}
|
|
1563
|
-
const dir = dirname(path);
|
|
1564
|
-
const base = basename(path);
|
|
1772
|
+
const dir = dirname(write.path);
|
|
1773
|
+
const base = basename(write.path);
|
|
1565
1774
|
const temp = join2(dir, `.${base}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
1775
|
+
let createdDirectory;
|
|
1566
1776
|
try {
|
|
1567
1777
|
if (createMissing) {
|
|
1568
|
-
await mkdir(dir, { recursive: true });
|
|
1778
|
+
const firstCreated = await mkdir(dir, { recursive: true });
|
|
1779
|
+
if (firstCreated !== undefined) {
|
|
1780
|
+
createdDirectory = { first: firstCreated, target: dir };
|
|
1781
|
+
}
|
|
1569
1782
|
}
|
|
1570
|
-
await
|
|
1783
|
+
await assertSafeOutputParentDirectory(dir, write.path);
|
|
1784
|
+
await writeFile(temp, write.bytes, { flag: "wx" });
|
|
1571
1785
|
if (mode !== undefined) {
|
|
1572
1786
|
await chmod(temp, mode);
|
|
1573
1787
|
}
|
|
1574
1788
|
} catch (error) {
|
|
1575
|
-
await
|
|
1576
|
-
|
|
1789
|
+
await cleanupStagedWrite({ temp, createdDirectory });
|
|
1790
|
+
failFileSystem(error, write.path, "Could not stage file replacement");
|
|
1791
|
+
}
|
|
1792
|
+
return { path: write.path, temp, request: write, createdDirectory };
|
|
1793
|
+
}
|
|
1794
|
+
async function assertSafeOutputParentDirectory(dir, outputPath) {
|
|
1795
|
+
let info;
|
|
1796
|
+
try {
|
|
1797
|
+
info = await lstat(dir);
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
failFileSystem(error, outputPath, "Could not stat output directory", "path");
|
|
1800
|
+
}
|
|
1801
|
+
if (info.isSymbolicLink()) {
|
|
1802
|
+
fail("symlink_path", `output directory must not be a symbolic link: ${outputPath}`, {
|
|
1803
|
+
path: outputPath,
|
|
1804
|
+
phase: "path"
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
if (!info.isDirectory()) {
|
|
1808
|
+
fail("not_regular_file", `output directory must be a directory: ${outputPath}`, {
|
|
1809
|
+
path: outputPath,
|
|
1810
|
+
phase: "path"
|
|
1577
1811
|
});
|
|
1578
|
-
failFileSystem(error, path, "Could not stage file replacement");
|
|
1579
1812
|
}
|
|
1580
|
-
|
|
1813
|
+
}
|
|
1814
|
+
async function cleanupStagedWrites(staged) {
|
|
1815
|
+
await Promise.all(staged.map((write) => unlink(write.temp).catch(() => {
|
|
1816
|
+
return;
|
|
1817
|
+
})));
|
|
1818
|
+
for (const write of [...staged].reverse()) {
|
|
1819
|
+
await cleanupCreatedDirectoryChain(write.createdDirectory);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async function cleanupStagedWrite(write) {
|
|
1823
|
+
await unlink(write.temp).catch(() => {
|
|
1824
|
+
return;
|
|
1825
|
+
});
|
|
1826
|
+
await cleanupCreatedDirectoryChain(write.createdDirectory);
|
|
1827
|
+
}
|
|
1828
|
+
async function cleanupCreatedDirectoryChain(chain) {
|
|
1829
|
+
if (chain === undefined || !isSameOrChildPath(chain.target, chain.first)) {
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
let current = chain.target;
|
|
1833
|
+
while (true) {
|
|
1834
|
+
try {
|
|
1835
|
+
await rmdir(current);
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
const code = error.code;
|
|
1838
|
+
if (code !== "ENOENT") {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (resolve2(current) === resolve2(chain.first)) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const parent = dirname(current);
|
|
1846
|
+
if (parent === current) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
current = parent;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function isSameOrChildPath(child, parent) {
|
|
1853
|
+
const childFromParent = relative2(resolve2(parent), resolve2(child));
|
|
1854
|
+
return childFromParent === "" || childFromParent !== ".." && !childFromParent.startsWith(`..${sep2}`) && !isAbsolute2(childFromParent);
|
|
1855
|
+
}
|
|
1856
|
+
function assertExpectedRegularFile(info, path, label, expected) {
|
|
1857
|
+
if (!info.isFile() && expected !== undefined) {
|
|
1858
|
+
failConcurrentModification(expected.label);
|
|
1859
|
+
}
|
|
1860
|
+
assertRegularFile(info, path, label);
|
|
1581
1861
|
}
|
|
1582
1862
|
async function statOptional(path) {
|
|
1583
1863
|
try {
|
|
@@ -1592,7 +1872,7 @@ async function statOptional(path) {
|
|
|
1592
1872
|
}
|
|
1593
1873
|
|
|
1594
1874
|
// src/move.ts
|
|
1595
|
-
import { createHash as
|
|
1875
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1596
1876
|
import { posix as posix2 } from "node:path";
|
|
1597
1877
|
import { TextDecoder } from "node:util";
|
|
1598
1878
|
var moveArgTypes = {
|
|
@@ -1604,14 +1884,22 @@ var moveArgTypes = {
|
|
|
1604
1884
|
target_before: "string",
|
|
1605
1885
|
target_after: "string",
|
|
1606
1886
|
expected_payload_sha256: "string",
|
|
1887
|
+
mode: "string",
|
|
1607
1888
|
dry_run: "boolean"
|
|
1608
1889
|
};
|
|
1890
|
+
var moveModes = new Set(["create_file", "remove_file"]);
|
|
1609
1891
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1610
1892
|
async function moveBlock(args, options = {}) {
|
|
1611
1893
|
const validated = validateMoveArgs(args);
|
|
1612
1894
|
const normalized = normalizeArgs(validated);
|
|
1613
1895
|
const cwd = options.cwd ?? process.cwd();
|
|
1614
1896
|
const dryRun = options.dryRun ?? validated.dry_run ?? false;
|
|
1897
|
+
if (normalized.kind === "create_file") {
|
|
1898
|
+
return createFile(normalized, validated, cwd, dryRun, options);
|
|
1899
|
+
}
|
|
1900
|
+
if (normalized.kind === "remove_file") {
|
|
1901
|
+
return removeFile(normalized, validated, cwd, dryRun, options);
|
|
1902
|
+
}
|
|
1615
1903
|
if (normalized.kind === "insertion") {
|
|
1616
1904
|
return insertPayload(normalized, validated, cwd, dryRun, options);
|
|
1617
1905
|
}
|
|
@@ -1621,15 +1909,17 @@ async function moveBlock(args, options = {}) {
|
|
|
1621
1909
|
const srcPath = resolvePath(cwd, normalized.src, "source path");
|
|
1622
1910
|
const dstPath = resolvePath(cwd, normalized.dst, "destination path");
|
|
1623
1911
|
const sameFile = await sameFileIdentity(srcPath, dstPath);
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1912
|
+
const srcSnapshot = await readFileSnapshot(srcPath, "source file");
|
|
1913
|
+
const dstSnapshot = sameFile ? srcSnapshot : await readFileSnapshot(dstPath, "destination file");
|
|
1914
|
+
const srcOriginal = srcSnapshot.bytes;
|
|
1915
|
+
const dstOriginal = dstSnapshot.bytes;
|
|
1626
1916
|
const source = findSource(srcOriginal, normalized);
|
|
1627
1917
|
const target = findTargetSelection(dstOriginal, normalized.targetBefore, normalized.targetAfter, normalized.dst, {
|
|
1628
1918
|
phase: "target",
|
|
1629
1919
|
anchor: targetAnchorName(normalized)
|
|
1630
1920
|
});
|
|
1631
1921
|
const selection = buildMoveSelection(srcOriginal, source, target, sameFile, normalized.dst);
|
|
1632
|
-
const payloadSha256 =
|
|
1922
|
+
const payloadSha256 = createHash3("sha256").update(selection.payload).digest("hex");
|
|
1633
1923
|
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1634
1924
|
const samePatchLabel = samePatchPath2(normalized.src, normalized.dst);
|
|
1635
1925
|
const patch = options.diff ? renderMovePatch(normalized, srcOriginal, dstOriginal, selection, sameFile && samePatchLabel, payloadSha256) : undefined;
|
|
@@ -1642,6 +1932,8 @@ async function moveBlock(args, options = {}) {
|
|
|
1642
1932
|
dryRun: writeSuppressed,
|
|
1643
1933
|
srcOriginal,
|
|
1644
1934
|
dstOriginal,
|
|
1935
|
+
srcSnapshot,
|
|
1936
|
+
dstSnapshot,
|
|
1645
1937
|
selection,
|
|
1646
1938
|
srcLabel: normalized.src,
|
|
1647
1939
|
dstLabel: normalized.dst
|
|
@@ -1661,7 +1953,8 @@ async function moveBlock(args, options = {}) {
|
|
|
1661
1953
|
selection
|
|
1662
1954
|
})
|
|
1663
1955
|
],
|
|
1664
|
-
patch
|
|
1956
|
+
patch,
|
|
1957
|
+
warnings: insertionBoundaryWarnings(normalized.dst, normalized.targetBefore, selection.payload, normalized.targetAfter)
|
|
1665
1958
|
};
|
|
1666
1959
|
}
|
|
1667
1960
|
function validateMoveArgs(value) {
|
|
@@ -1683,12 +1976,27 @@ function validateMoveArgs(value) {
|
|
|
1683
1976
|
field: "expected_payload_sha256"
|
|
1684
1977
|
});
|
|
1685
1978
|
}
|
|
1979
|
+
if (args.mode !== undefined && !moveModes.has(args.mode)) {
|
|
1980
|
+
fail("invalid_move_args", "mode must be create_file or remove_file", { field: "mode" });
|
|
1981
|
+
}
|
|
1686
1982
|
return args;
|
|
1687
1983
|
}
|
|
1688
1984
|
function normalizeArgs(args) {
|
|
1689
1985
|
if (!args.src) {
|
|
1690
1986
|
fail("invalid_move_args", "move requires src", { field: "src" });
|
|
1691
1987
|
}
|
|
1988
|
+
if (args.src !== devNull) {
|
|
1989
|
+
validateOperationPath(args.src, "source path");
|
|
1990
|
+
}
|
|
1991
|
+
if (args.dst !== undefined && args.dst !== devNull) {
|
|
1992
|
+
validateOperationPath(args.dst, "destination path");
|
|
1993
|
+
}
|
|
1994
|
+
if (args.mode === "create_file") {
|
|
1995
|
+
return normalizeFileCreationArgs(args);
|
|
1996
|
+
}
|
|
1997
|
+
if (args.mode === "remove_file") {
|
|
1998
|
+
return normalizeFileRemovalArgs(args);
|
|
1999
|
+
}
|
|
1692
2000
|
const dst = args.dst ?? args.src;
|
|
1693
2001
|
if (args.src === devNull && dst === devNull) {
|
|
1694
2002
|
fail("invalid_move_args", "move cannot use /dev/null for both src and dst", { field: "dst" });
|
|
@@ -1749,6 +2057,60 @@ function normalizeArgs(args) {
|
|
|
1749
2057
|
targetAfter: target.after
|
|
1750
2058
|
};
|
|
1751
2059
|
}
|
|
2060
|
+
function normalizeFileCreationArgs(args) {
|
|
2061
|
+
if (args.src !== devNull) {
|
|
2062
|
+
fail("invalid_move_args", "create_file mode requires src to be /dev/null", { field: "src" });
|
|
2063
|
+
}
|
|
2064
|
+
if (args.dst === undefined) {
|
|
2065
|
+
fail("invalid_move_args", "create_file mode requires dst", { field: "dst" });
|
|
2066
|
+
}
|
|
2067
|
+
if (args.dst === devNull) {
|
|
2068
|
+
fail("invalid_move_args", "create_file mode cannot target /dev/null", { field: "dst" });
|
|
2069
|
+
}
|
|
2070
|
+
if (args.payload === undefined) {
|
|
2071
|
+
fail("invalid_move_args", "create_file mode requires payload", { field: "payload" });
|
|
2072
|
+
}
|
|
2073
|
+
rejectSourceSelectionArgs(args, "create_file mode uses payload as the whole-file content");
|
|
2074
|
+
rejectTargetAnchorArgs(args, "create_file mode must not include target anchors");
|
|
2075
|
+
return {
|
|
2076
|
+
kind: "create_file",
|
|
2077
|
+
src: devNull,
|
|
2078
|
+
dst: args.dst,
|
|
2079
|
+
payload: Buffer.from(args.payload, "utf8")
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
function normalizeFileRemovalArgs(args) {
|
|
2083
|
+
if (args.src === devNull) {
|
|
2084
|
+
fail("invalid_move_args", "remove_file mode requires a real src path", { field: "src" });
|
|
2085
|
+
}
|
|
2086
|
+
if (args.dst !== devNull) {
|
|
2087
|
+
fail("invalid_move_args", "remove_file mode requires dst to be /dev/null", { field: "dst" });
|
|
2088
|
+
}
|
|
2089
|
+
if (args.payload !== undefined) {
|
|
2090
|
+
fail("invalid_move_args", "remove_file mode reads the whole-file payload from src", { field: "payload" });
|
|
2091
|
+
}
|
|
2092
|
+
rejectSourceSelectionArgs(args, "remove_file mode removes the whole src file");
|
|
2093
|
+
rejectTargetAnchorArgs(args, "remove_file mode must not include target anchors");
|
|
2094
|
+
return {
|
|
2095
|
+
kind: "remove_file",
|
|
2096
|
+
src: args.src,
|
|
2097
|
+
dst: devNull
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function rejectSourceSelectionArgs(args, message) {
|
|
2101
|
+
if (args.src_start !== undefined || args.src_end !== undefined) {
|
|
2102
|
+
fail("invalid_move_args", message, {
|
|
2103
|
+
field: args.src_start !== undefined ? "src_start" : "src_end"
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function rejectTargetAnchorArgs(args, message) {
|
|
2108
|
+
if (args.target_before !== undefined || args.target_after !== undefined) {
|
|
2109
|
+
fail("invalid_move_args", message, {
|
|
2110
|
+
field: args.target_before !== undefined ? "target_before" : "target_after"
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
1752
2114
|
function normalizeTargetArgs(args) {
|
|
1753
2115
|
const hasTargetBefore = args.target_before !== undefined;
|
|
1754
2116
|
const hasTargetAfter = args.target_after !== undefined;
|
|
@@ -1768,10 +2130,33 @@ function requiredBuffer(value, field) {
|
|
|
1768
2130
|
}
|
|
1769
2131
|
return Buffer.from(value, "utf8");
|
|
1770
2132
|
}
|
|
2133
|
+
async function createFile(args, validated, cwd, dryRun, options) {
|
|
2134
|
+
const payloadSha256 = createHash3("sha256").update(args.payload).digest("hex");
|
|
2135
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
2136
|
+
const patch = renderFileCreationPatch(args, payloadSha256);
|
|
2137
|
+
return runRenderedPatch(patch, cwd, dryRun, options);
|
|
2138
|
+
}
|
|
2139
|
+
async function removeFile(args, validated, cwd, dryRun, options) {
|
|
2140
|
+
const srcPath = resolvePath(cwd, args.src, "source path");
|
|
2141
|
+
const payload = await readFileChecked(srcPath, "source file");
|
|
2142
|
+
const payloadSha256 = createHash3("sha256").update(payload).digest("hex");
|
|
2143
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
2144
|
+
const patch = renderFileRemovalPatch(args, payload, payloadSha256);
|
|
2145
|
+
return runRenderedPatch(patch, cwd, dryRun, options);
|
|
2146
|
+
}
|
|
2147
|
+
async function runRenderedPatch(patch, cwd, dryRun, options) {
|
|
2148
|
+
const writeSuppressed = dryRun || options.diff === true;
|
|
2149
|
+
const result = await applyPatchBytes(Buffer.from(patch, "utf8"), { cwd, dryRun: writeSuppressed });
|
|
2150
|
+
return {
|
|
2151
|
+
...result,
|
|
2152
|
+
patch: options.diff === true ? patch : undefined
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
1771
2155
|
async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
1772
2156
|
const dstPath = resolvePath(cwd, args.dst, "destination path");
|
|
1773
|
-
const
|
|
1774
|
-
const
|
|
2157
|
+
const snapshot = await readFileSnapshot(dstPath, "destination file");
|
|
2158
|
+
const original = snapshot.bytes;
|
|
2159
|
+
const payloadSha256 = createHash3("sha256").update(args.payload).digest("hex");
|
|
1775
2160
|
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1776
2161
|
const alreadyApplied = findAlreadyAppliedTarget(original, args, args.dst);
|
|
1777
2162
|
if (alreadyApplied !== undefined) {
|
|
@@ -1812,7 +2197,9 @@ async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
|
1812
2197
|
]);
|
|
1813
2198
|
const changed = next.equals(original) ? [] : [args.dst];
|
|
1814
2199
|
if (!writeSuppressed && changed.length > 0) {
|
|
1815
|
-
await writeAtomic(dstPath, next
|
|
2200
|
+
await writeAtomic(dstPath, next, {
|
|
2201
|
+
expected: { kind: "file", label: args.dst, snapshot }
|
|
2202
|
+
});
|
|
1816
2203
|
}
|
|
1817
2204
|
return {
|
|
1818
2205
|
changed,
|
|
@@ -1832,9 +2219,40 @@ async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
|
1832
2219
|
insert_index: target.insertIndex
|
|
1833
2220
|
}
|
|
1834
2221
|
],
|
|
1835
|
-
patch: renderedPatch
|
|
2222
|
+
patch: renderedPatch,
|
|
2223
|
+
warnings: insertionBoundaryWarnings(args.dst, args.targetBefore, args.payload, args.targetAfter)
|
|
1836
2224
|
};
|
|
1837
2225
|
}
|
|
2226
|
+
function insertionBoundaryWarnings(path, targetBefore, payload, targetAfter) {
|
|
2227
|
+
const warnings = [];
|
|
2228
|
+
if (targetBefore.length > 0 && payload.length > 0 && !endsWithLf(targetBefore) && !startsWithLf(payload)) {
|
|
2229
|
+
warnings.push({
|
|
2230
|
+
code: "adjacent_bytes",
|
|
2231
|
+
message: "Insertion will place payload immediately after target_before with no newline or separator inserted by blockpatch",
|
|
2232
|
+
path,
|
|
2233
|
+
phase: "target",
|
|
2234
|
+
boundary: "target_before+payload",
|
|
2235
|
+
suggested_action: "include the intended newline in target_before or at the start of payload"
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
if (payload.length > 0 && targetAfter.length > 0 && !endsWithLf(payload) && !startsWithLf(targetAfter)) {
|
|
2239
|
+
warnings.push({
|
|
2240
|
+
code: "adjacent_bytes",
|
|
2241
|
+
message: "Insertion will place target_after immediately after payload with no newline or separator inserted by blockpatch",
|
|
2242
|
+
path,
|
|
2243
|
+
phase: "target",
|
|
2244
|
+
boundary: "payload+target_after",
|
|
2245
|
+
suggested_action: "include the intended newline at the end of payload or at the start of target_after"
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return warnings.length > 0 ? warnings : undefined;
|
|
2249
|
+
}
|
|
2250
|
+
function startsWithLf(bytes) {
|
|
2251
|
+
return bytes[0] === 10;
|
|
2252
|
+
}
|
|
2253
|
+
function endsWithLf(bytes) {
|
|
2254
|
+
return bytes[bytes.length - 1] === 10;
|
|
2255
|
+
}
|
|
1838
2256
|
function verifyExpectedPayloadHash(args, payloadSha256) {
|
|
1839
2257
|
if (args.expected_payload_sha256 !== undefined && args.expected_payload_sha256 !== payloadSha256) {
|
|
1840
2258
|
fail("hash_mismatch", "expected_payload_sha256 does not match selected source payload", {
|
|
@@ -1846,17 +2264,18 @@ function verifyExpectedPayloadHash(args, payloadSha256) {
|
|
|
1846
2264
|
}
|
|
1847
2265
|
function findAlreadyAppliedTarget(file, args, dstLabel) {
|
|
1848
2266
|
const alreadyApplied = Buffer.concat([args.targetBefore, args.payload, args.targetAfter]);
|
|
1849
|
-
const
|
|
2267
|
+
const matchResult = indexesOfLimited(file, alreadyApplied);
|
|
2268
|
+
const matches = matchResult.matches;
|
|
1850
2269
|
if (matches.length === 0) {
|
|
1851
2270
|
return;
|
|
1852
2271
|
}
|
|
1853
|
-
if (matches.length > 1) {
|
|
2272
|
+
if (matches.length > 1 || matchResult.truncated) {
|
|
1854
2273
|
const ranges = boundedRanges(matches.map((start2) => ({ start: start2, end: start2 + alreadyApplied.length })));
|
|
1855
|
-
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel};
|
|
2274
|
+
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel}; ${matchedLocations(matches.length, matchResult.truncated)}`, {
|
|
1856
2275
|
path: dstLabel,
|
|
1857
2276
|
phase: "target",
|
|
1858
2277
|
anchor: "target_before+payload+target_after",
|
|
1859
|
-
matches
|
|
2278
|
+
...matchCountDetails(matches.length, matchResult.truncated),
|
|
1860
2279
|
ranges,
|
|
1861
2280
|
line_ranges: boundedLineRanges(file, ranges)
|
|
1862
2281
|
});
|
|
@@ -1869,10 +2288,11 @@ function findAlreadyAppliedTarget(file, args, dstLabel) {
|
|
|
1869
2288
|
}
|
|
1870
2289
|
async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
1871
2290
|
const srcPath = resolvePath(cwd, args.src, "source path");
|
|
1872
|
-
const
|
|
2291
|
+
const snapshot = await readFileSnapshot(srcPath, "source file");
|
|
2292
|
+
const original = snapshot.bytes;
|
|
1873
2293
|
const source = findSource(original, args);
|
|
1874
2294
|
const payload = Buffer.from(original.subarray(source.start, source.end));
|
|
1875
|
-
const payloadSha256 =
|
|
2295
|
+
const payloadSha256 = createHash3("sha256").update(payload).digest("hex");
|
|
1876
2296
|
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1877
2297
|
const renderedPatch = options.diff ? renderDeletionPatch(args, original, source, payload, payloadSha256) : undefined;
|
|
1878
2298
|
await selfCheckRenderedPatch(renderedPatch, [{ path: args.src, bytes: original }]);
|
|
@@ -1880,7 +2300,9 @@ async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
|
1880
2300
|
const next = Buffer.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
1881
2301
|
const changed = next.equals(original) ? [] : [args.src];
|
|
1882
2302
|
if (!writeSuppressed && changed.length > 0) {
|
|
1883
|
-
await writeAtomic(srcPath, next
|
|
2303
|
+
await writeAtomic(srcPath, next, {
|
|
2304
|
+
expected: { kind: "file", label: args.src, snapshot }
|
|
2305
|
+
});
|
|
1884
2306
|
}
|
|
1885
2307
|
return {
|
|
1886
2308
|
changed,
|
|
@@ -1904,15 +2326,8 @@ async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
|
1904
2326
|
};
|
|
1905
2327
|
}
|
|
1906
2328
|
function findSource(file, args) {
|
|
1907
|
-
const
|
|
1908
|
-
const ranges =
|
|
1909
|
-
for (const start of startMatches) {
|
|
1910
|
-
const searchFrom = start + args.srcStart.length;
|
|
1911
|
-
const endStart = file.indexOf(args.srcEnd, searchFrom);
|
|
1912
|
-
if (endStart !== -1) {
|
|
1913
|
-
ranges.push({ start, end: endStart + args.srcEnd.length });
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
2329
|
+
const result = findDelimitedRanges(file, args.srcStart, args.srcEnd);
|
|
2330
|
+
const ranges = result.ranges;
|
|
1916
2331
|
if (ranges.length === 0) {
|
|
1917
2332
|
fail("source_not_found", `Source delimiters were not found in ${args.src}`, {
|
|
1918
2333
|
path: args.src,
|
|
@@ -1921,18 +2336,44 @@ function findSource(file, args) {
|
|
|
1921
2336
|
matches: 0
|
|
1922
2337
|
});
|
|
1923
2338
|
}
|
|
1924
|
-
if (ranges.length > 1) {
|
|
1925
|
-
fail("source_ambiguous", `Source delimiters are ambiguous in ${args.src};
|
|
2339
|
+
if (ranges.length > 1 || result.truncated) {
|
|
2340
|
+
fail("source_ambiguous", `Source delimiters are ambiguous in ${args.src}; ${matchedLocations(ranges.length, result.truncated)}`, {
|
|
1926
2341
|
path: args.src,
|
|
1927
2342
|
phase: "source",
|
|
1928
2343
|
anchor: "src_start/src_end",
|
|
1929
|
-
|
|
2344
|
+
...matchCountDetails(ranges.length, result.truncated),
|
|
1930
2345
|
ranges: boundedRanges(ranges),
|
|
1931
2346
|
line_ranges: boundedLineRanges(file, ranges)
|
|
1932
2347
|
});
|
|
1933
2348
|
}
|
|
1934
2349
|
return ranges[0];
|
|
1935
2350
|
}
|
|
2351
|
+
function findDelimitedRanges(file, startNeedle, endNeedle, limit = 11) {
|
|
2352
|
+
if (startNeedle.length === 0) {
|
|
2353
|
+
return { ranges: [], truncated: false };
|
|
2354
|
+
}
|
|
2355
|
+
const maxMatches = normalizedLimit2(limit);
|
|
2356
|
+
const ranges = [];
|
|
2357
|
+
let start = file.indexOf(startNeedle);
|
|
2358
|
+
while (start !== -1) {
|
|
2359
|
+
const searchFrom = start + startNeedle.length;
|
|
2360
|
+
const endStart = file.indexOf(endNeedle, searchFrom);
|
|
2361
|
+
if (endStart !== -1) {
|
|
2362
|
+
if (ranges.length >= maxMatches) {
|
|
2363
|
+
return { ranges, truncated: true };
|
|
2364
|
+
}
|
|
2365
|
+
ranges.push({ start, end: endStart + endNeedle.length });
|
|
2366
|
+
}
|
|
2367
|
+
start = file.indexOf(startNeedle, start + 1);
|
|
2368
|
+
}
|
|
2369
|
+
return { ranges, truncated: false };
|
|
2370
|
+
}
|
|
2371
|
+
function normalizedLimit2(limit) {
|
|
2372
|
+
if (!Number.isFinite(limit)) {
|
|
2373
|
+
return Number.POSITIVE_INFINITY;
|
|
2374
|
+
}
|
|
2375
|
+
return Math.max(0, Math.trunc(limit));
|
|
2376
|
+
}
|
|
1936
2377
|
function targetAnchorName(args) {
|
|
1937
2378
|
if (args.targetBefore.length > 0 && args.targetAfter.length > 0) {
|
|
1938
2379
|
return "target_before+target_after";
|
|
@@ -2078,6 +2519,40 @@ function renderDeletionPatch(args, srcOriginal, source, payload, payloadSha256)
|
|
|
2078
2519
|
`) + `
|
|
2079
2520
|
`;
|
|
2080
2521
|
}
|
|
2522
|
+
function renderFileCreationPatch(args, payloadSha256) {
|
|
2523
|
+
const id = "move-1";
|
|
2524
|
+
const payloadLines = countLines(args.payload);
|
|
2525
|
+
const targetBody = renderHunkBytes(args.payload, "+");
|
|
2526
|
+
return [
|
|
2527
|
+
`diff --blockpatch ${devNull} b/${args.dst}`,
|
|
2528
|
+
"blockpatch version 1",
|
|
2529
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2530
|
+
`--- ${devNull}`,
|
|
2531
|
+
`+++ b/${args.dst}`,
|
|
2532
|
+
"",
|
|
2533
|
+
`@@ -0,0 +${payloadLines === 0 ? "0,0" : `1,${payloadLines}`} @@ blockpatch-target id=${id}`,
|
|
2534
|
+
targetBody
|
|
2535
|
+
].join(`
|
|
2536
|
+
`) + `
|
|
2537
|
+
`;
|
|
2538
|
+
}
|
|
2539
|
+
function renderFileRemovalPatch(args, payload, payloadSha256) {
|
|
2540
|
+
const id = "move-1";
|
|
2541
|
+
const payloadLines = countLines(payload);
|
|
2542
|
+
const sourceBody = renderHunkBytes(payload, "-");
|
|
2543
|
+
return [
|
|
2544
|
+
`diff --blockpatch a/${args.src} ${devNull}`,
|
|
2545
|
+
"blockpatch version 1",
|
|
2546
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2547
|
+
`--- a/${args.src}`,
|
|
2548
|
+
`+++ ${devNull}`,
|
|
2549
|
+
"",
|
|
2550
|
+
`@@ -${payloadLines === 0 ? "0,0" : `1,${payloadLines}`} +0,0 @@ blockpatch-source id=${id}`,
|
|
2551
|
+
sourceBody
|
|
2552
|
+
].join(`
|
|
2553
|
+
`) + `
|
|
2554
|
+
`;
|
|
2555
|
+
}
|
|
2081
2556
|
function lineNumberAt2(file, byteIndex) {
|
|
2082
2557
|
let line = 1;
|
|
2083
2558
|
for (let index = 0;index < byteIndex; index += 1) {
|
|
@@ -2146,6 +2621,24 @@ function decodeUtf8(bytes) {
|
|
|
2146
2621
|
|
|
2147
2622
|
// src/cli.ts
|
|
2148
2623
|
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
2624
|
+
var OUTPUT_FLAGS = new Set(["--json-output", "--explain"]);
|
|
2625
|
+
var CWD_VALUE_FLAGS = new Set(["--cwd", "--directory", "-d"]);
|
|
2626
|
+
var PATCH_INPUT_VALUE_FLAGS = new Set(["-i", "--input"]);
|
|
2627
|
+
var STRIP_VALUE_FLAGS = new Set(["-p", "--strip"]);
|
|
2628
|
+
var PATCH_VALUE_FLAGS = new Set([...CWD_VALUE_FLAGS, ...PATCH_INPUT_VALUE_FLAGS, ...STRIP_VALUE_FLAGS]);
|
|
2629
|
+
var MOVE_JSON_VALUE_FLAGS = new Set(["--json"]);
|
|
2630
|
+
var MOVE_KEY_BY_FLAG = {
|
|
2631
|
+
"--src": "src",
|
|
2632
|
+
"--src-start": "src_start",
|
|
2633
|
+
"--src-end": "src_end",
|
|
2634
|
+
"--dst": "dst",
|
|
2635
|
+
"--payload": "payload",
|
|
2636
|
+
"--target-before": "target_before",
|
|
2637
|
+
"--target-after": "target_after",
|
|
2638
|
+
"--expected-payload-sha256": "expected_payload_sha256"
|
|
2639
|
+
};
|
|
2640
|
+
var MOVE_ARG_VALUE_FLAGS = new Set(Object.keys(MOVE_KEY_BY_FLAG));
|
|
2641
|
+
var MOVE_VALUE_FLAGS = new Set([...CWD_VALUE_FLAGS, ...MOVE_JSON_VALUE_FLAGS, ...MOVE_ARG_VALUE_FLAGS]);
|
|
2149
2642
|
async function main(argv) {
|
|
2150
2643
|
const options = parseArgs(argv);
|
|
2151
2644
|
if (options.command === "help") {
|
|
@@ -2177,12 +2670,13 @@ async function main(argv) {
|
|
|
2177
2670
|
}
|
|
2178
2671
|
async function runPatchCommand(options) {
|
|
2179
2672
|
const patchPath = options.patchPath ?? "-";
|
|
2673
|
+
const inputPatchPath = patchPath === "-" ? patchPath : resolve3(patchPath);
|
|
2180
2674
|
if (options.command === "check") {
|
|
2181
2675
|
return patchPath === "-" ? checkPatchBytes(await readStdin(), {
|
|
2182
2676
|
cwd: options.cwd,
|
|
2183
2677
|
reverse: options.reverse,
|
|
2184
2678
|
stripComponents: options.stripComponents
|
|
2185
|
-
}) : checkPatchFile(
|
|
2679
|
+
}) : checkPatchFile(inputPatchPath, {
|
|
2186
2680
|
cwd: options.cwd,
|
|
2187
2681
|
reverse: options.reverse,
|
|
2188
2682
|
stripComponents: options.stripComponents
|
|
@@ -2193,7 +2687,7 @@ async function runPatchCommand(options) {
|
|
|
2193
2687
|
dryRun: options.dryRun,
|
|
2194
2688
|
reverse: options.reverse,
|
|
2195
2689
|
stripComponents: options.stripComponents
|
|
2196
|
-
}) : applyPatchFile(
|
|
2690
|
+
}) : applyPatchFile(inputPatchPath, {
|
|
2197
2691
|
cwd: options.cwd,
|
|
2198
2692
|
dryRun: options.dryRun,
|
|
2199
2693
|
reverse: options.reverse,
|
|
@@ -2207,14 +2701,27 @@ async function loadMoveArgs(options) {
|
|
|
2207
2701
|
if (options.moveJsonPath === undefined) {
|
|
2208
2702
|
throw new BlockPatchError("missing_move_args", `${options.command} requires --json or --src flags`);
|
|
2209
2703
|
}
|
|
2210
|
-
const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.
|
|
2704
|
+
const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.moveJsonPath), "move JSON file");
|
|
2211
2705
|
try {
|
|
2212
|
-
|
|
2706
|
+
const parsed = JSON.parse(jsonBytes.toString("utf8"));
|
|
2707
|
+
if (isEmptyObject(parsed)) {
|
|
2708
|
+
throw new BlockPatchError("invalid_move_args", "Move JSON cannot be empty; provide src plus source selectors or payload and target anchors", {
|
|
2709
|
+
field: "src",
|
|
2710
|
+
suggested_action: "Use fields like src, src_start, src_end, dst, target_before, target_after, payload, or run blockpatch help"
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
return parsed;
|
|
2213
2714
|
} catch (error) {
|
|
2715
|
+
if (error instanceof BlockPatchError) {
|
|
2716
|
+
throw error;
|
|
2717
|
+
}
|
|
2214
2718
|
const message = error instanceof Error ? error.message : String(error);
|
|
2215
2719
|
throw new BlockPatchError("invalid_json", `Invalid move JSON: ${message}`);
|
|
2216
2720
|
}
|
|
2217
2721
|
}
|
|
2722
|
+
function isEmptyObject(value) {
|
|
2723
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
2724
|
+
}
|
|
2218
2725
|
function parseArgs(argv) {
|
|
2219
2726
|
const args = [...argv];
|
|
2220
2727
|
const outputFlags = takeLeadingOutputFlags(args);
|
|
@@ -2225,9 +2732,14 @@ function parseArgs(argv) {
|
|
|
2225
2732
|
if (first === "version" || first === "--version" || first === "-v") {
|
|
2226
2733
|
const options = base("version", outputFlags.jsonOutput);
|
|
2227
2734
|
for (const arg of args) {
|
|
2228
|
-
if (arg
|
|
2735
|
+
if (isOutputFlag(arg)) {
|
|
2229
2736
|
options.jsonOutput = true;
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
if (arg.startsWith("-")) {
|
|
2740
|
+
throw new BlockPatchError("unknown_option", `Unknown option: ${arg}`);
|
|
2230
2741
|
}
|
|
2742
|
+
throw new BlockPatchError("too_many_args", `Unexpected argument: ${arg}`);
|
|
2231
2743
|
}
|
|
2232
2744
|
return options;
|
|
2233
2745
|
}
|
|
@@ -2257,7 +2769,7 @@ function parsePatchArgs(command, args, jsonOutput, explain) {
|
|
|
2257
2769
|
options.reverse = true;
|
|
2258
2770
|
continue;
|
|
2259
2771
|
}
|
|
2260
|
-
if (arg
|
|
2772
|
+
if (isValueFlag(arg, CWD_VALUE_FLAGS)) {
|
|
2261
2773
|
options.cwd = requireValue(args, arg, true);
|
|
2262
2774
|
continue;
|
|
2263
2775
|
}
|
|
@@ -2269,7 +2781,7 @@ function parsePatchArgs(command, args, jsonOutput, explain) {
|
|
|
2269
2781
|
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
2270
2782
|
continue;
|
|
2271
2783
|
}
|
|
2272
|
-
if (arg
|
|
2784
|
+
if (isValueFlag(arg, PATCH_INPUT_VALUE_FLAGS)) {
|
|
2273
2785
|
setPatchPath(options, requireValue(args, arg, false));
|
|
2274
2786
|
continue;
|
|
2275
2787
|
}
|
|
@@ -2317,7 +2829,7 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2317
2829
|
options.diff = true;
|
|
2318
2830
|
continue;
|
|
2319
2831
|
}
|
|
2320
|
-
if (arg
|
|
2832
|
+
if (isValueFlag(arg, CWD_VALUE_FLAGS)) {
|
|
2321
2833
|
options.cwd = requireValue(args, arg, true);
|
|
2322
2834
|
continue;
|
|
2323
2835
|
}
|
|
@@ -2329,8 +2841,8 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2329
2841
|
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
2330
2842
|
continue;
|
|
2331
2843
|
}
|
|
2332
|
-
if (arg
|
|
2333
|
-
options.moveJsonPath = requireValue(args,
|
|
2844
|
+
if (isValueFlag(arg, MOVE_JSON_VALUE_FLAGS)) {
|
|
2845
|
+
options.moveJsonPath = requireValue(args, arg, false);
|
|
2334
2846
|
continue;
|
|
2335
2847
|
}
|
|
2336
2848
|
if (arg?.startsWith("--json=")) {
|
|
@@ -2354,26 +2866,7 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2354
2866
|
return options;
|
|
2355
2867
|
}
|
|
2356
2868
|
function argToMoveKey(arg) {
|
|
2357
|
-
|
|
2358
|
-
case "--src":
|
|
2359
|
-
return "src";
|
|
2360
|
-
case "--src-start":
|
|
2361
|
-
return "src_start";
|
|
2362
|
-
case "--src-end":
|
|
2363
|
-
return "src_end";
|
|
2364
|
-
case "--dst":
|
|
2365
|
-
return "dst";
|
|
2366
|
-
case "--payload":
|
|
2367
|
-
return "payload";
|
|
2368
|
-
case "--target-before":
|
|
2369
|
-
return "target_before";
|
|
2370
|
-
case "--target-after":
|
|
2371
|
-
return "target_after";
|
|
2372
|
-
case "--expected-payload-sha256":
|
|
2373
|
-
return "expected_payload_sha256";
|
|
2374
|
-
default:
|
|
2375
|
-
return;
|
|
2376
|
-
}
|
|
2869
|
+
return MOVE_KEY_BY_FLAG[arg];
|
|
2377
2870
|
}
|
|
2378
2871
|
function base(command, jsonOutput) {
|
|
2379
2872
|
return {
|
|
@@ -2393,7 +2886,7 @@ function setPatchPath(options, path) {
|
|
|
2393
2886
|
options.patchPath = path;
|
|
2394
2887
|
}
|
|
2395
2888
|
function parseStripOption(arg, args) {
|
|
2396
|
-
if (arg
|
|
2889
|
+
if (isValueFlag(arg, STRIP_VALUE_FLAGS)) {
|
|
2397
2890
|
return parseStripComponents(requireValue(args, arg, false), arg);
|
|
2398
2891
|
}
|
|
2399
2892
|
if (arg?.startsWith("-p") === true && arg.length > 2) {
|
|
@@ -2417,7 +2910,7 @@ function parseStripComponents(value, option) {
|
|
|
2417
2910
|
function takeLeadingOutputFlags(args) {
|
|
2418
2911
|
let jsonOutput = false;
|
|
2419
2912
|
let explain = false;
|
|
2420
|
-
while (args[0]
|
|
2913
|
+
while (isOutputFlag(args[0])) {
|
|
2421
2914
|
const arg = args.shift();
|
|
2422
2915
|
jsonOutput = true;
|
|
2423
2916
|
explain = explain || arg === "--explain";
|
|
@@ -2425,10 +2918,6 @@ function takeLeadingOutputFlags(args) {
|
|
|
2425
2918
|
return { jsonOutput, explain };
|
|
2426
2919
|
}
|
|
2427
2920
|
function takeOutputFlag(options, arg) {
|
|
2428
|
-
if (arg === "--json-output") {
|
|
2429
|
-
options.jsonOutput = true;
|
|
2430
|
-
return true;
|
|
2431
|
-
}
|
|
2432
2921
|
if (arg === "--explain") {
|
|
2433
2922
|
options.jsonOutput = true;
|
|
2434
2923
|
if (options.command === "apply" || options.command === "move" || options.command === "plan") {
|
|
@@ -2436,8 +2925,18 @@ function takeOutputFlag(options, arg) {
|
|
|
2436
2925
|
}
|
|
2437
2926
|
return true;
|
|
2438
2927
|
}
|
|
2928
|
+
if (arg === "--json-output") {
|
|
2929
|
+
options.jsonOutput = true;
|
|
2930
|
+
return true;
|
|
2931
|
+
}
|
|
2439
2932
|
return false;
|
|
2440
2933
|
}
|
|
2934
|
+
function isOutputFlag(arg) {
|
|
2935
|
+
return arg !== undefined && OUTPUT_FLAGS.has(arg);
|
|
2936
|
+
}
|
|
2937
|
+
function isValueFlag(arg, flags) {
|
|
2938
|
+
return arg !== undefined && flags.has(arg);
|
|
2939
|
+
}
|
|
2441
2940
|
function requireValue(args, option, pathValue) {
|
|
2442
2941
|
const value = args.shift();
|
|
2443
2942
|
if (value === undefined) {
|
|
@@ -2500,6 +2999,29 @@ Usage:
|
|
|
2500
2999
|
blockpatch move --src /dev/null --dst <path> --payload <text> --target-before <text>
|
|
2501
3000
|
blockpatch move --src <path> --src-start <text> --src-end <text> --dst /dev/null
|
|
2502
3001
|
blockpatch version
|
|
3002
|
+
|
|
3003
|
+
Move JSON fields:
|
|
3004
|
+
src, dst, src_start, src_end, payload, target_before, target_after,
|
|
3005
|
+
expected_payload_sha256, mode, dry_run
|
|
3006
|
+
|
|
3007
|
+
Move selection:
|
|
3008
|
+
src_start/src_end are byte-exact, newline-sensitive delimiters. The selected
|
|
3009
|
+
payload starts at src_start and ends after the first following src_end.
|
|
3010
|
+
|
|
3011
|
+
Target anchors:
|
|
3012
|
+
target_before is the exact context immediately before the insertion point,
|
|
3013
|
+
so insertion occurs after it. target_after is the exact context immediately
|
|
3014
|
+
after the insertion point, so insertion occurs before it. With both anchors,
|
|
3015
|
+
insertion occurs between them.
|
|
3016
|
+
|
|
3017
|
+
Newlines:
|
|
3018
|
+
blockpatch never adds separators. Include every intended newline in
|
|
3019
|
+
src_start/src_end, payload, target_before, or target_after.
|
|
3020
|
+
|
|
3021
|
+
Examples:
|
|
3022
|
+
blockpatch move --json - --diff <<'JSON'
|
|
3023
|
+
{"src":"src/a.ts","src_start":"function x() {\\n","src_end":"}\\n","dst":"src/b.ts","target_before":"class B {\\n"}
|
|
3024
|
+
JSON
|
|
2503
3025
|
`);
|
|
2504
3026
|
}
|
|
2505
3027
|
main(process.argv.slice(2)).then((code) => {
|
|
@@ -2531,31 +3053,21 @@ function hasJsonOutputFlag(argv) {
|
|
|
2531
3053
|
if (command === "plan") {
|
|
2532
3054
|
return true;
|
|
2533
3055
|
}
|
|
2534
|
-
return args.
|
|
3056
|
+
return args.some(isOutputFlag);
|
|
2535
3057
|
}
|
|
2536
3058
|
function hasPatchJsonOutputFlag(args) {
|
|
2537
|
-
|
|
2538
|
-
const arg = args[index];
|
|
2539
|
-
if (arg === "--json-output" || arg === "--explain") {
|
|
2540
|
-
return true;
|
|
2541
|
-
}
|
|
2542
|
-
if (arg === "--cwd" || arg === "--directory" || arg === "-d" || arg === "-i" || arg === "--input") {
|
|
2543
|
-
index += 1;
|
|
2544
|
-
continue;
|
|
2545
|
-
}
|
|
2546
|
-
if (arg === "-p" || arg === "--strip") {
|
|
2547
|
-
index += 1;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
return false;
|
|
3059
|
+
return hasJsonOutputAfterValueFlags(args, PATCH_VALUE_FLAGS);
|
|
2551
3060
|
}
|
|
2552
3061
|
function hasMoveJsonOutputFlag(args) {
|
|
3062
|
+
return hasJsonOutputAfterValueFlags(args, MOVE_VALUE_FLAGS);
|
|
3063
|
+
}
|
|
3064
|
+
function hasJsonOutputAfterValueFlags(args, valueFlags) {
|
|
2553
3065
|
for (let index = 0;index < args.length; index += 1) {
|
|
2554
3066
|
const arg = args[index];
|
|
2555
|
-
if (arg
|
|
3067
|
+
if (isOutputFlag(arg)) {
|
|
2556
3068
|
return true;
|
|
2557
3069
|
}
|
|
2558
|
-
if (
|
|
3070
|
+
if (isValueFlag(arg, valueFlags)) {
|
|
2559
3071
|
index += 1;
|
|
2560
3072
|
}
|
|
2561
3073
|
}
|