blockpatch 1.0.0 → 1.1.0
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 +967 -523
- package/docs/behavior.md +207 -0
- package/docs/commands.md +84 -0
- package/docs/spec.md +356 -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,28 +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
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
phase: "target",
|
|
1040
|
-
anchor: "blockpatch-target"
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
return nullSourceResult(patch, dstLabel, true, "applied", { start: 0, end: 0 }, 0);
|
|
1044
|
-
}
|
|
1045
|
-
async function applyPathCreationMove(patch, dstLabel, cwd, dryRun) {
|
|
1046
|
-
const resolved = resolvePathAllowMissing(cwd, dstLabel, "destination path");
|
|
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);
|
|
1096
|
+
return {
|
|
1097
|
+
result: nullSourceResult(patch, dstLabel, "already_applied", { start: 0, end: original.length }, 0),
|
|
1098
|
+
mutations: []
|
|
1099
|
+
};
|
|
1052
1100
|
}
|
|
1053
1101
|
fail("destination_exists", `Destination path for file creation already exists with different bytes: ${dstLabel}`, {
|
|
1054
1102
|
path: dstLabel,
|
|
@@ -1056,36 +1104,19 @@ async function applyPathCreationMove(patch, dstLabel, cwd, dryRun) {
|
|
|
1056
1104
|
anchor: "blockpatch-target"
|
|
1057
1105
|
});
|
|
1058
1106
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
|
@@ -1683,12 +1975,27 @@ function validateMoveArgs(value) {
|
|
|
1683
1975
|
field: "expected_payload_sha256"
|
|
1684
1976
|
});
|
|
1685
1977
|
}
|
|
1978
|
+
if (args.mode !== undefined && !moveModes.has(args.mode)) {
|
|
1979
|
+
fail("invalid_move_args", "mode must be create_file or remove_file", { field: "mode" });
|
|
1980
|
+
}
|
|
1686
1981
|
return args;
|
|
1687
1982
|
}
|
|
1688
1983
|
function normalizeArgs(args) {
|
|
1689
1984
|
if (!args.src) {
|
|
1690
1985
|
fail("invalid_move_args", "move requires src", { field: "src" });
|
|
1691
1986
|
}
|
|
1987
|
+
if (args.src !== devNull) {
|
|
1988
|
+
validateOperationPath(args.src, "source path");
|
|
1989
|
+
}
|
|
1990
|
+
if (args.dst !== undefined && args.dst !== devNull) {
|
|
1991
|
+
validateOperationPath(args.dst, "destination path");
|
|
1992
|
+
}
|
|
1993
|
+
if (args.mode === "create_file") {
|
|
1994
|
+
return normalizeFileCreationArgs(args);
|
|
1995
|
+
}
|
|
1996
|
+
if (args.mode === "remove_file") {
|
|
1997
|
+
return normalizeFileRemovalArgs(args);
|
|
1998
|
+
}
|
|
1692
1999
|
const dst = args.dst ?? args.src;
|
|
1693
2000
|
if (args.src === devNull && dst === devNull) {
|
|
1694
2001
|
fail("invalid_move_args", "move cannot use /dev/null for both src and dst", { field: "dst" });
|
|
@@ -1749,6 +2056,60 @@ function normalizeArgs(args) {
|
|
|
1749
2056
|
targetAfter: target.after
|
|
1750
2057
|
};
|
|
1751
2058
|
}
|
|
2059
|
+
function normalizeFileCreationArgs(args) {
|
|
2060
|
+
if (args.src !== devNull) {
|
|
2061
|
+
fail("invalid_move_args", "create_file mode requires src to be /dev/null", { field: "src" });
|
|
2062
|
+
}
|
|
2063
|
+
if (args.dst === undefined) {
|
|
2064
|
+
fail("invalid_move_args", "create_file mode requires dst", { field: "dst" });
|
|
2065
|
+
}
|
|
2066
|
+
if (args.dst === devNull) {
|
|
2067
|
+
fail("invalid_move_args", "create_file mode cannot target /dev/null", { field: "dst" });
|
|
2068
|
+
}
|
|
2069
|
+
if (args.payload === undefined) {
|
|
2070
|
+
fail("invalid_move_args", "create_file mode requires payload", { field: "payload" });
|
|
2071
|
+
}
|
|
2072
|
+
rejectSourceSelectionArgs(args, "create_file mode uses payload as the whole-file content");
|
|
2073
|
+
rejectTargetAnchorArgs(args, "create_file mode must not include target anchors");
|
|
2074
|
+
return {
|
|
2075
|
+
kind: "create_file",
|
|
2076
|
+
src: devNull,
|
|
2077
|
+
dst: args.dst,
|
|
2078
|
+
payload: Buffer.from(args.payload, "utf8")
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function normalizeFileRemovalArgs(args) {
|
|
2082
|
+
if (args.src === devNull) {
|
|
2083
|
+
fail("invalid_move_args", "remove_file mode requires a real src path", { field: "src" });
|
|
2084
|
+
}
|
|
2085
|
+
if (args.dst !== devNull) {
|
|
2086
|
+
fail("invalid_move_args", "remove_file mode requires dst to be /dev/null", { field: "dst" });
|
|
2087
|
+
}
|
|
2088
|
+
if (args.payload !== undefined) {
|
|
2089
|
+
fail("invalid_move_args", "remove_file mode reads the whole-file payload from src", { field: "payload" });
|
|
2090
|
+
}
|
|
2091
|
+
rejectSourceSelectionArgs(args, "remove_file mode removes the whole src file");
|
|
2092
|
+
rejectTargetAnchorArgs(args, "remove_file mode must not include target anchors");
|
|
2093
|
+
return {
|
|
2094
|
+
kind: "remove_file",
|
|
2095
|
+
src: args.src,
|
|
2096
|
+
dst: devNull
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
function rejectSourceSelectionArgs(args, message) {
|
|
2100
|
+
if (args.src_start !== undefined || args.src_end !== undefined) {
|
|
2101
|
+
fail("invalid_move_args", message, {
|
|
2102
|
+
field: args.src_start !== undefined ? "src_start" : "src_end"
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function rejectTargetAnchorArgs(args, message) {
|
|
2107
|
+
if (args.target_before !== undefined || args.target_after !== undefined) {
|
|
2108
|
+
fail("invalid_move_args", message, {
|
|
2109
|
+
field: args.target_before !== undefined ? "target_before" : "target_after"
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
1752
2113
|
function normalizeTargetArgs(args) {
|
|
1753
2114
|
const hasTargetBefore = args.target_before !== undefined;
|
|
1754
2115
|
const hasTargetAfter = args.target_after !== undefined;
|
|
@@ -1768,10 +2129,33 @@ function requiredBuffer(value, field) {
|
|
|
1768
2129
|
}
|
|
1769
2130
|
return Buffer.from(value, "utf8");
|
|
1770
2131
|
}
|
|
2132
|
+
async function createFile(args, validated, cwd, dryRun, options) {
|
|
2133
|
+
const payloadSha256 = createHash3("sha256").update(args.payload).digest("hex");
|
|
2134
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
2135
|
+
const patch = renderFileCreationPatch(args, payloadSha256);
|
|
2136
|
+
return runRenderedPatch(patch, cwd, dryRun, options);
|
|
2137
|
+
}
|
|
2138
|
+
async function removeFile(args, validated, cwd, dryRun, options) {
|
|
2139
|
+
const srcPath = resolvePath(cwd, args.src, "source path");
|
|
2140
|
+
const payload = await readFileChecked(srcPath, "source file");
|
|
2141
|
+
const payloadSha256 = createHash3("sha256").update(payload).digest("hex");
|
|
2142
|
+
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
2143
|
+
const patch = renderFileRemovalPatch(args, payload, payloadSha256);
|
|
2144
|
+
return runRenderedPatch(patch, cwd, dryRun, options);
|
|
2145
|
+
}
|
|
2146
|
+
async function runRenderedPatch(patch, cwd, dryRun, options) {
|
|
2147
|
+
const writeSuppressed = dryRun || options.diff === true;
|
|
2148
|
+
const result = await applyPatchBytes(Buffer.from(patch, "utf8"), { cwd, dryRun: writeSuppressed });
|
|
2149
|
+
return {
|
|
2150
|
+
...result,
|
|
2151
|
+
patch: options.diff === true ? patch : undefined
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
1771
2154
|
async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
1772
2155
|
const dstPath = resolvePath(cwd, args.dst, "destination path");
|
|
1773
|
-
const
|
|
1774
|
-
const
|
|
2156
|
+
const snapshot = await readFileSnapshot(dstPath, "destination file");
|
|
2157
|
+
const original = snapshot.bytes;
|
|
2158
|
+
const payloadSha256 = createHash3("sha256").update(args.payload).digest("hex");
|
|
1775
2159
|
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1776
2160
|
const alreadyApplied = findAlreadyAppliedTarget(original, args, args.dst);
|
|
1777
2161
|
if (alreadyApplied !== undefined) {
|
|
@@ -1812,7 +2196,9 @@ async function insertPayload(args, validated, cwd, dryRun, options) {
|
|
|
1812
2196
|
]);
|
|
1813
2197
|
const changed = next.equals(original) ? [] : [args.dst];
|
|
1814
2198
|
if (!writeSuppressed && changed.length > 0) {
|
|
1815
|
-
await writeAtomic(dstPath, next
|
|
2199
|
+
await writeAtomic(dstPath, next, {
|
|
2200
|
+
expected: { kind: "file", label: args.dst, snapshot }
|
|
2201
|
+
});
|
|
1816
2202
|
}
|
|
1817
2203
|
return {
|
|
1818
2204
|
changed,
|
|
@@ -1846,17 +2232,18 @@ function verifyExpectedPayloadHash(args, payloadSha256) {
|
|
|
1846
2232
|
}
|
|
1847
2233
|
function findAlreadyAppliedTarget(file, args, dstLabel) {
|
|
1848
2234
|
const alreadyApplied = Buffer.concat([args.targetBefore, args.payload, args.targetAfter]);
|
|
1849
|
-
const
|
|
2235
|
+
const matchResult = indexesOfLimited(file, alreadyApplied);
|
|
2236
|
+
const matches = matchResult.matches;
|
|
1850
2237
|
if (matches.length === 0) {
|
|
1851
2238
|
return;
|
|
1852
2239
|
}
|
|
1853
|
-
if (matches.length > 1) {
|
|
2240
|
+
if (matches.length > 1 || matchResult.truncated) {
|
|
1854
2241
|
const ranges = boundedRanges(matches.map((start2) => ({ start: start2, end: start2 + alreadyApplied.length })));
|
|
1855
|
-
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel};
|
|
2242
|
+
fail("target_ambiguous", `Already-applied target is ambiguous in ${dstLabel}; ${matchedLocations(matches.length, matchResult.truncated)}`, {
|
|
1856
2243
|
path: dstLabel,
|
|
1857
2244
|
phase: "target",
|
|
1858
2245
|
anchor: "target_before+payload+target_after",
|
|
1859
|
-
matches
|
|
2246
|
+
...matchCountDetails(matches.length, matchResult.truncated),
|
|
1860
2247
|
ranges,
|
|
1861
2248
|
line_ranges: boundedLineRanges(file, ranges)
|
|
1862
2249
|
});
|
|
@@ -1869,10 +2256,11 @@ function findAlreadyAppliedTarget(file, args, dstLabel) {
|
|
|
1869
2256
|
}
|
|
1870
2257
|
async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
1871
2258
|
const srcPath = resolvePath(cwd, args.src, "source path");
|
|
1872
|
-
const
|
|
2259
|
+
const snapshot = await readFileSnapshot(srcPath, "source file");
|
|
2260
|
+
const original = snapshot.bytes;
|
|
1873
2261
|
const source = findSource(original, args);
|
|
1874
2262
|
const payload = Buffer.from(original.subarray(source.start, source.end));
|
|
1875
|
-
const payloadSha256 =
|
|
2263
|
+
const payloadSha256 = createHash3("sha256").update(payload).digest("hex");
|
|
1876
2264
|
verifyExpectedPayloadHash(validated, payloadSha256);
|
|
1877
2265
|
const renderedPatch = options.diff ? renderDeletionPatch(args, original, source, payload, payloadSha256) : undefined;
|
|
1878
2266
|
await selfCheckRenderedPatch(renderedPatch, [{ path: args.src, bytes: original }]);
|
|
@@ -1880,7 +2268,9 @@ async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
|
1880
2268
|
const next = Buffer.concat([original.subarray(0, source.start), original.subarray(source.end)]);
|
|
1881
2269
|
const changed = next.equals(original) ? [] : [args.src];
|
|
1882
2270
|
if (!writeSuppressed && changed.length > 0) {
|
|
1883
|
-
await writeAtomic(srcPath, next
|
|
2271
|
+
await writeAtomic(srcPath, next, {
|
|
2272
|
+
expected: { kind: "file", label: args.src, snapshot }
|
|
2273
|
+
});
|
|
1884
2274
|
}
|
|
1885
2275
|
return {
|
|
1886
2276
|
changed,
|
|
@@ -1904,15 +2294,8 @@ async function deletePayload(args, validated, cwd, dryRun, options) {
|
|
|
1904
2294
|
};
|
|
1905
2295
|
}
|
|
1906
2296
|
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
|
-
}
|
|
2297
|
+
const result = findDelimitedRanges(file, args.srcStart, args.srcEnd);
|
|
2298
|
+
const ranges = result.ranges;
|
|
1916
2299
|
if (ranges.length === 0) {
|
|
1917
2300
|
fail("source_not_found", `Source delimiters were not found in ${args.src}`, {
|
|
1918
2301
|
path: args.src,
|
|
@@ -1921,18 +2304,44 @@ function findSource(file, args) {
|
|
|
1921
2304
|
matches: 0
|
|
1922
2305
|
});
|
|
1923
2306
|
}
|
|
1924
|
-
if (ranges.length > 1) {
|
|
1925
|
-
fail("source_ambiguous", `Source delimiters are ambiguous in ${args.src};
|
|
2307
|
+
if (ranges.length > 1 || result.truncated) {
|
|
2308
|
+
fail("source_ambiguous", `Source delimiters are ambiguous in ${args.src}; ${matchedLocations(ranges.length, result.truncated)}`, {
|
|
1926
2309
|
path: args.src,
|
|
1927
2310
|
phase: "source",
|
|
1928
2311
|
anchor: "src_start/src_end",
|
|
1929
|
-
|
|
2312
|
+
...matchCountDetails(ranges.length, result.truncated),
|
|
1930
2313
|
ranges: boundedRanges(ranges),
|
|
1931
2314
|
line_ranges: boundedLineRanges(file, ranges)
|
|
1932
2315
|
});
|
|
1933
2316
|
}
|
|
1934
2317
|
return ranges[0];
|
|
1935
2318
|
}
|
|
2319
|
+
function findDelimitedRanges(file, startNeedle, endNeedle, limit = 11) {
|
|
2320
|
+
if (startNeedle.length === 0) {
|
|
2321
|
+
return { ranges: [], truncated: false };
|
|
2322
|
+
}
|
|
2323
|
+
const maxMatches = normalizedLimit2(limit);
|
|
2324
|
+
const ranges = [];
|
|
2325
|
+
let start = file.indexOf(startNeedle);
|
|
2326
|
+
while (start !== -1) {
|
|
2327
|
+
const searchFrom = start + startNeedle.length;
|
|
2328
|
+
const endStart = file.indexOf(endNeedle, searchFrom);
|
|
2329
|
+
if (endStart !== -1) {
|
|
2330
|
+
if (ranges.length >= maxMatches) {
|
|
2331
|
+
return { ranges, truncated: true };
|
|
2332
|
+
}
|
|
2333
|
+
ranges.push({ start, end: endStart + endNeedle.length });
|
|
2334
|
+
}
|
|
2335
|
+
start = file.indexOf(startNeedle, start + 1);
|
|
2336
|
+
}
|
|
2337
|
+
return { ranges, truncated: false };
|
|
2338
|
+
}
|
|
2339
|
+
function normalizedLimit2(limit) {
|
|
2340
|
+
if (!Number.isFinite(limit)) {
|
|
2341
|
+
return Number.POSITIVE_INFINITY;
|
|
2342
|
+
}
|
|
2343
|
+
return Math.max(0, Math.trunc(limit));
|
|
2344
|
+
}
|
|
1936
2345
|
function targetAnchorName(args) {
|
|
1937
2346
|
if (args.targetBefore.length > 0 && args.targetAfter.length > 0) {
|
|
1938
2347
|
return "target_before+target_after";
|
|
@@ -2078,6 +2487,40 @@ function renderDeletionPatch(args, srcOriginal, source, payload, payloadSha256)
|
|
|
2078
2487
|
`) + `
|
|
2079
2488
|
`;
|
|
2080
2489
|
}
|
|
2490
|
+
function renderFileCreationPatch(args, payloadSha256) {
|
|
2491
|
+
const id = "move-1";
|
|
2492
|
+
const payloadLines = countLines(args.payload);
|
|
2493
|
+
const targetBody = renderHunkBytes(args.payload, "+");
|
|
2494
|
+
return [
|
|
2495
|
+
`diff --blockpatch ${devNull} b/${args.dst}`,
|
|
2496
|
+
"blockpatch version 1",
|
|
2497
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2498
|
+
`--- ${devNull}`,
|
|
2499
|
+
`+++ b/${args.dst}`,
|
|
2500
|
+
"",
|
|
2501
|
+
`@@ -0,0 +${payloadLines === 0 ? "0,0" : `1,${payloadLines}`} @@ blockpatch-target id=${id}`,
|
|
2502
|
+
targetBody
|
|
2503
|
+
].join(`
|
|
2504
|
+
`) + `
|
|
2505
|
+
`;
|
|
2506
|
+
}
|
|
2507
|
+
function renderFileRemovalPatch(args, payload, payloadSha256) {
|
|
2508
|
+
const id = "move-1";
|
|
2509
|
+
const payloadLines = countLines(payload);
|
|
2510
|
+
const sourceBody = renderHunkBytes(payload, "-");
|
|
2511
|
+
return [
|
|
2512
|
+
`diff --blockpatch a/${args.src} ${devNull}`,
|
|
2513
|
+
"blockpatch version 1",
|
|
2514
|
+
`blockpatch move id=${id} payload-sha256=${payloadSha256}`,
|
|
2515
|
+
`--- a/${args.src}`,
|
|
2516
|
+
`+++ ${devNull}`,
|
|
2517
|
+
"",
|
|
2518
|
+
`@@ -${payloadLines === 0 ? "0,0" : `1,${payloadLines}`} +0,0 @@ blockpatch-source id=${id}`,
|
|
2519
|
+
sourceBody
|
|
2520
|
+
].join(`
|
|
2521
|
+
`) + `
|
|
2522
|
+
`;
|
|
2523
|
+
}
|
|
2081
2524
|
function lineNumberAt2(file, byteIndex) {
|
|
2082
2525
|
let line = 1;
|
|
2083
2526
|
for (let index = 0;index < byteIndex; index += 1) {
|
|
@@ -2146,6 +2589,24 @@ function decodeUtf8(bytes) {
|
|
|
2146
2589
|
|
|
2147
2590
|
// src/cli.ts
|
|
2148
2591
|
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
2592
|
+
var OUTPUT_FLAGS = new Set(["--json-output", "--explain"]);
|
|
2593
|
+
var CWD_VALUE_FLAGS = new Set(["--cwd", "--directory", "-d"]);
|
|
2594
|
+
var PATCH_INPUT_VALUE_FLAGS = new Set(["-i", "--input"]);
|
|
2595
|
+
var STRIP_VALUE_FLAGS = new Set(["-p", "--strip"]);
|
|
2596
|
+
var PATCH_VALUE_FLAGS = new Set([...CWD_VALUE_FLAGS, ...PATCH_INPUT_VALUE_FLAGS, ...STRIP_VALUE_FLAGS]);
|
|
2597
|
+
var MOVE_JSON_VALUE_FLAGS = new Set(["--json"]);
|
|
2598
|
+
var MOVE_KEY_BY_FLAG = {
|
|
2599
|
+
"--src": "src",
|
|
2600
|
+
"--src-start": "src_start",
|
|
2601
|
+
"--src-end": "src_end",
|
|
2602
|
+
"--dst": "dst",
|
|
2603
|
+
"--payload": "payload",
|
|
2604
|
+
"--target-before": "target_before",
|
|
2605
|
+
"--target-after": "target_after",
|
|
2606
|
+
"--expected-payload-sha256": "expected_payload_sha256"
|
|
2607
|
+
};
|
|
2608
|
+
var MOVE_ARG_VALUE_FLAGS = new Set(Object.keys(MOVE_KEY_BY_FLAG));
|
|
2609
|
+
var MOVE_VALUE_FLAGS = new Set([...CWD_VALUE_FLAGS, ...MOVE_JSON_VALUE_FLAGS, ...MOVE_ARG_VALUE_FLAGS]);
|
|
2149
2610
|
async function main(argv) {
|
|
2150
2611
|
const options = parseArgs(argv);
|
|
2151
2612
|
if (options.command === "help") {
|
|
@@ -2177,12 +2638,13 @@ async function main(argv) {
|
|
|
2177
2638
|
}
|
|
2178
2639
|
async function runPatchCommand(options) {
|
|
2179
2640
|
const patchPath = options.patchPath ?? "-";
|
|
2641
|
+
const inputPatchPath = patchPath === "-" ? patchPath : resolve3(patchPath);
|
|
2180
2642
|
if (options.command === "check") {
|
|
2181
2643
|
return patchPath === "-" ? checkPatchBytes(await readStdin(), {
|
|
2182
2644
|
cwd: options.cwd,
|
|
2183
2645
|
reverse: options.reverse,
|
|
2184
2646
|
stripComponents: options.stripComponents
|
|
2185
|
-
}) : checkPatchFile(
|
|
2647
|
+
}) : checkPatchFile(inputPatchPath, {
|
|
2186
2648
|
cwd: options.cwd,
|
|
2187
2649
|
reverse: options.reverse,
|
|
2188
2650
|
stripComponents: options.stripComponents
|
|
@@ -2193,7 +2655,7 @@ async function runPatchCommand(options) {
|
|
|
2193
2655
|
dryRun: options.dryRun,
|
|
2194
2656
|
reverse: options.reverse,
|
|
2195
2657
|
stripComponents: options.stripComponents
|
|
2196
|
-
}) : applyPatchFile(
|
|
2658
|
+
}) : applyPatchFile(inputPatchPath, {
|
|
2197
2659
|
cwd: options.cwd,
|
|
2198
2660
|
dryRun: options.dryRun,
|
|
2199
2661
|
reverse: options.reverse,
|
|
@@ -2207,7 +2669,7 @@ async function loadMoveArgs(options) {
|
|
|
2207
2669
|
if (options.moveJsonPath === undefined) {
|
|
2208
2670
|
throw new BlockPatchError("missing_move_args", `${options.command} requires --json or --src flags`);
|
|
2209
2671
|
}
|
|
2210
|
-
const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.
|
|
2672
|
+
const jsonBytes = options.moveJsonPath === "-" ? await readStdin() : await readFileChecked(resolve3(options.moveJsonPath), "move JSON file");
|
|
2211
2673
|
try {
|
|
2212
2674
|
return JSON.parse(jsonBytes.toString("utf8"));
|
|
2213
2675
|
} catch (error) {
|
|
@@ -2225,9 +2687,14 @@ function parseArgs(argv) {
|
|
|
2225
2687
|
if (first === "version" || first === "--version" || first === "-v") {
|
|
2226
2688
|
const options = base("version", outputFlags.jsonOutput);
|
|
2227
2689
|
for (const arg of args) {
|
|
2228
|
-
if (arg
|
|
2690
|
+
if (isOutputFlag(arg)) {
|
|
2229
2691
|
options.jsonOutput = true;
|
|
2692
|
+
continue;
|
|
2230
2693
|
}
|
|
2694
|
+
if (arg.startsWith("-")) {
|
|
2695
|
+
throw new BlockPatchError("unknown_option", `Unknown option: ${arg}`);
|
|
2696
|
+
}
|
|
2697
|
+
throw new BlockPatchError("too_many_args", `Unexpected argument: ${arg}`);
|
|
2231
2698
|
}
|
|
2232
2699
|
return options;
|
|
2233
2700
|
}
|
|
@@ -2257,7 +2724,7 @@ function parsePatchArgs(command, args, jsonOutput, explain) {
|
|
|
2257
2724
|
options.reverse = true;
|
|
2258
2725
|
continue;
|
|
2259
2726
|
}
|
|
2260
|
-
if (arg
|
|
2727
|
+
if (isValueFlag(arg, CWD_VALUE_FLAGS)) {
|
|
2261
2728
|
options.cwd = requireValue(args, arg, true);
|
|
2262
2729
|
continue;
|
|
2263
2730
|
}
|
|
@@ -2269,7 +2736,7 @@ function parsePatchArgs(command, args, jsonOutput, explain) {
|
|
|
2269
2736
|
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
2270
2737
|
continue;
|
|
2271
2738
|
}
|
|
2272
|
-
if (arg
|
|
2739
|
+
if (isValueFlag(arg, PATCH_INPUT_VALUE_FLAGS)) {
|
|
2273
2740
|
setPatchPath(options, requireValue(args, arg, false));
|
|
2274
2741
|
continue;
|
|
2275
2742
|
}
|
|
@@ -2317,7 +2784,7 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2317
2784
|
options.diff = true;
|
|
2318
2785
|
continue;
|
|
2319
2786
|
}
|
|
2320
|
-
if (arg
|
|
2787
|
+
if (isValueFlag(arg, CWD_VALUE_FLAGS)) {
|
|
2321
2788
|
options.cwd = requireValue(args, arg, true);
|
|
2322
2789
|
continue;
|
|
2323
2790
|
}
|
|
@@ -2329,8 +2796,8 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2329
2796
|
options.cwd = resolve3(arg.slice("--directory=".length));
|
|
2330
2797
|
continue;
|
|
2331
2798
|
}
|
|
2332
|
-
if (arg
|
|
2333
|
-
options.moveJsonPath = requireValue(args,
|
|
2799
|
+
if (isValueFlag(arg, MOVE_JSON_VALUE_FLAGS)) {
|
|
2800
|
+
options.moveJsonPath = requireValue(args, arg, false);
|
|
2334
2801
|
continue;
|
|
2335
2802
|
}
|
|
2336
2803
|
if (arg?.startsWith("--json=")) {
|
|
@@ -2354,26 +2821,7 @@ function parseMoveArgs(command, args, jsonOutput, explain) {
|
|
|
2354
2821
|
return options;
|
|
2355
2822
|
}
|
|
2356
2823
|
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
|
-
}
|
|
2824
|
+
return MOVE_KEY_BY_FLAG[arg];
|
|
2377
2825
|
}
|
|
2378
2826
|
function base(command, jsonOutput) {
|
|
2379
2827
|
return {
|
|
@@ -2393,7 +2841,7 @@ function setPatchPath(options, path) {
|
|
|
2393
2841
|
options.patchPath = path;
|
|
2394
2842
|
}
|
|
2395
2843
|
function parseStripOption(arg, args) {
|
|
2396
|
-
if (arg
|
|
2844
|
+
if (isValueFlag(arg, STRIP_VALUE_FLAGS)) {
|
|
2397
2845
|
return parseStripComponents(requireValue(args, arg, false), arg);
|
|
2398
2846
|
}
|
|
2399
2847
|
if (arg?.startsWith("-p") === true && arg.length > 2) {
|
|
@@ -2417,7 +2865,7 @@ function parseStripComponents(value, option) {
|
|
|
2417
2865
|
function takeLeadingOutputFlags(args) {
|
|
2418
2866
|
let jsonOutput = false;
|
|
2419
2867
|
let explain = false;
|
|
2420
|
-
while (args[0]
|
|
2868
|
+
while (isOutputFlag(args[0])) {
|
|
2421
2869
|
const arg = args.shift();
|
|
2422
2870
|
jsonOutput = true;
|
|
2423
2871
|
explain = explain || arg === "--explain";
|
|
@@ -2425,10 +2873,6 @@ function takeLeadingOutputFlags(args) {
|
|
|
2425
2873
|
return { jsonOutput, explain };
|
|
2426
2874
|
}
|
|
2427
2875
|
function takeOutputFlag(options, arg) {
|
|
2428
|
-
if (arg === "--json-output") {
|
|
2429
|
-
options.jsonOutput = true;
|
|
2430
|
-
return true;
|
|
2431
|
-
}
|
|
2432
2876
|
if (arg === "--explain") {
|
|
2433
2877
|
options.jsonOutput = true;
|
|
2434
2878
|
if (options.command === "apply" || options.command === "move" || options.command === "plan") {
|
|
@@ -2436,8 +2880,18 @@ function takeOutputFlag(options, arg) {
|
|
|
2436
2880
|
}
|
|
2437
2881
|
return true;
|
|
2438
2882
|
}
|
|
2883
|
+
if (arg === "--json-output") {
|
|
2884
|
+
options.jsonOutput = true;
|
|
2885
|
+
return true;
|
|
2886
|
+
}
|
|
2439
2887
|
return false;
|
|
2440
2888
|
}
|
|
2889
|
+
function isOutputFlag(arg) {
|
|
2890
|
+
return arg !== undefined && OUTPUT_FLAGS.has(arg);
|
|
2891
|
+
}
|
|
2892
|
+
function isValueFlag(arg, flags) {
|
|
2893
|
+
return arg !== undefined && flags.has(arg);
|
|
2894
|
+
}
|
|
2441
2895
|
function requireValue(args, option, pathValue) {
|
|
2442
2896
|
const value = args.shift();
|
|
2443
2897
|
if (value === undefined) {
|
|
@@ -2531,31 +2985,21 @@ function hasJsonOutputFlag(argv) {
|
|
|
2531
2985
|
if (command === "plan") {
|
|
2532
2986
|
return true;
|
|
2533
2987
|
}
|
|
2534
|
-
return args.
|
|
2988
|
+
return args.some(isOutputFlag);
|
|
2535
2989
|
}
|
|
2536
2990
|
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;
|
|
2991
|
+
return hasJsonOutputAfterValueFlags(args, PATCH_VALUE_FLAGS);
|
|
2551
2992
|
}
|
|
2552
2993
|
function hasMoveJsonOutputFlag(args) {
|
|
2994
|
+
return hasJsonOutputAfterValueFlags(args, MOVE_VALUE_FLAGS);
|
|
2995
|
+
}
|
|
2996
|
+
function hasJsonOutputAfterValueFlags(args, valueFlags) {
|
|
2553
2997
|
for (let index = 0;index < args.length; index += 1) {
|
|
2554
2998
|
const arg = args[index];
|
|
2555
|
-
if (arg
|
|
2999
|
+
if (isOutputFlag(arg)) {
|
|
2556
3000
|
return true;
|
|
2557
3001
|
}
|
|
2558
|
-
if (
|
|
3002
|
+
if (isValueFlag(arg, valueFlags)) {
|
|
2559
3003
|
index += 1;
|
|
2560
3004
|
}
|
|
2561
3005
|
}
|