blockpatch 0.1.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 +26 -0
- package/README.md +131 -74
- 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 +2908 -244
- 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 +33 -13
- package/dist/cli.d.ts +0 -2
- package/dist/engine.d.ts +0 -19
- package/dist/errors.d.ts +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -263
- package/dist/parser.d.ts +0 -2
- package/dist/types.d.ts +0 -20
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
diff --blockpatch /dev/null b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=072a398e62ad50e40613b98e57f3e3236a471dcbd7c2b9f190440c22adc16a0f
|
|
4
|
+
--- /dev/null
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -0,0 +1,1 @@ blockpatch-target id=move-1
|
|
8
|
+
+created file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This directory is intentionally empty before applying the patch.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Cross-File Relocation
|
|
2
|
+
|
|
3
|
+
Moves `movedThing` from `source.txt` to `target.txt`. Cross-file relocations use two sections tied by the same move id and payload hash.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
blockpatch check patch.blockpatch -d work
|
|
7
|
+
blockpatch apply patch.blockpatch -d work
|
|
8
|
+
diff -u expected/source.txt work/source.txt
|
|
9
|
+
diff -u expected/target.txt work/target.txt
|
|
10
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
diff --blockpatch a/source.txt b/source.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 role=source payload-sha256=a990c0da5571138b0e2363af883a399fe214a137ad809c67f6530c618967a4e6
|
|
4
|
+
--- a/source.txt
|
|
5
|
+
+++ b/source.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,5 +1,2 @@ blockpatch-source id=move-1
|
|
8
|
+
alpha()
|
|
9
|
+
-function movedThing() {
|
|
10
|
+
- return 42;
|
|
11
|
+
-}
|
|
12
|
+
omega()
|
|
13
|
+
|
|
14
|
+
diff --blockpatch a/target.txt b/target.txt
|
|
15
|
+
blockpatch version 1
|
|
16
|
+
blockpatch move id=move-1 role=target payload-sha256=a990c0da5571138b0e2363af883a399fe214a137ad809c67f6530c618967a4e6
|
|
17
|
+
--- a/target.txt
|
|
18
|
+
+++ b/target.txt
|
|
19
|
+
|
|
20
|
+
@@ -1,3 +1,6 @@ blockpatch-target id=move-1
|
|
21
|
+
class Target {
|
|
22
|
+
method() {}
|
|
23
|
+
+function movedThing() {
|
|
24
|
+
+ return 42;
|
|
25
|
+
+}
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=5dbe6af8d3a2a31f6696ebdae9e590977a19b827ec8e0a22ea1e3decf1a79b77
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
8
|
+
alpha
|
|
9
|
+
-remove me
|
|
10
|
+
omega
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
8
|
+
alpha
|
|
9
|
+
-move me
|
|
10
|
+
omega
|
|
11
|
+
|
|
12
|
+
@@ -4,1 +4,2 @@ blockpatch-target id=move-1
|
|
13
|
+
target
|
|
14
|
+
+move me
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=cbfeef0cd60ab2c01e036d23082a1f19ea3f5153d9cffde8790ad0152a3830f5
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,2 +1,3 @@ blockpatch-target id=move-1
|
|
8
|
+
alpha
|
|
9
|
+
+inserted line
|
|
10
|
+
omega
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
After applying the patch, file.txt should not exist.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt /dev/null
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=4da5a83256114db88b111676cf3b2f4a04f202d3d7fc99417cac1ba5145713e2
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ /dev/null
|
|
6
|
+
|
|
7
|
+
@@ -1,1 +0,0 @@ blockpatch-source id=move-1
|
|
8
|
+
-obsolete file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
obsolete file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Reverse
|
|
2
|
+
|
|
3
|
+
Applies the same relocation patch in reverse. The `work/` tree starts in the already-applied state and `-R` restores the original ordering.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
blockpatch check -R patch.blockpatch -d work
|
|
7
|
+
blockpatch apply -R patch.blockpatch -d work
|
|
8
|
+
diff -u expected/file.txt work/file.txt
|
|
9
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
8
|
+
alpha
|
|
9
|
+
-move me
|
|
10
|
+
omega
|
|
11
|
+
|
|
12
|
+
@@ -4,1 +4,2 @@ blockpatch-target id=move-1
|
|
13
|
+
target
|
|
14
|
+
+move me
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
diff --blockpatch a/file.txt b/file.txt
|
|
2
|
+
blockpatch version 1
|
|
3
|
+
blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
|
|
4
|
+
--- a/file.txt
|
|
5
|
+
+++ b/file.txt
|
|
6
|
+
|
|
7
|
+
@@ -1,3 +1,2 @@ blockpatch-source id=move-1
|
|
8
|
+
alpha
|
|
9
|
+
-move me
|
|
10
|
+
omega
|
|
11
|
+
|
|
12
|
+
@@ -4,1 +4,2 @@ blockpatch-target id=move-1
|
|
13
|
+
target
|
|
14
|
+
+move me
|
package/package.json
CHANGED
|
@@ -1,33 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blockpatch",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Hash-verified, reviewable block-move patches for coding agents",
|
|
5
|
+
"packageManager": "bun@1.3.14",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js"
|
|
13
|
-
}
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/cheese-melted/blockpatch.git"
|
|
14
11
|
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cheese-melted/blockpatch/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/cheese-melted/blockpatch#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"patch",
|
|
18
|
+
"diff",
|
|
19
|
+
"refactor",
|
|
20
|
+
"codemod",
|
|
21
|
+
"text",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
15
24
|
"bin": {
|
|
16
|
-
"blockpatch": "dist/cli.js"
|
|
25
|
+
"blockpatch": "dist/cli.js",
|
|
26
|
+
"blockpatch-conformance": "conformance/runner.mjs"
|
|
17
27
|
},
|
|
18
28
|
"files": [
|
|
19
29
|
"dist",
|
|
30
|
+
"docs",
|
|
31
|
+
"examples",
|
|
32
|
+
"conformance",
|
|
20
33
|
"README.md",
|
|
34
|
+
"CONTRIBUTING.md",
|
|
21
35
|
"LICENSE"
|
|
22
36
|
],
|
|
23
37
|
"scripts": {
|
|
24
38
|
"test": "bun test",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && bun build ./src/cli.ts --target=node --outdir=dist",
|
|
41
|
+
"smoke:dist": "node test/built-cli-smoke.mjs",
|
|
42
|
+
"smoke:pack": "node test/packed-cli-smoke.mjs",
|
|
43
|
+
"conformance": "node conformance/runner.mjs node dist/cli.js",
|
|
44
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build && npm run smoke:dist && npm run smoke:pack",
|
|
45
|
+
"pack:dry": "npm pack --dry-run",
|
|
27
46
|
"publish:dry": "npm publish --dry-run"
|
|
28
47
|
},
|
|
29
48
|
"devDependencies": {
|
|
30
|
-
"@types/bun": "
|
|
49
|
+
"@types/bun": "^1.3.14",
|
|
50
|
+
"@types/node": "^20",
|
|
31
51
|
"typescript": "^5.8.3"
|
|
32
52
|
},
|
|
33
53
|
"engines": {
|
package/dist/cli.d.ts
DELETED
package/dist/engine.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ApplyOptions, ApplyResult, BlockPatch } from "./types";
|
|
2
|
-
interface ByteRange {
|
|
3
|
-
start: number;
|
|
4
|
-
end: number;
|
|
5
|
-
}
|
|
6
|
-
interface TargetSelection {
|
|
7
|
-
range: ByteRange;
|
|
8
|
-
insertIndex: number;
|
|
9
|
-
}
|
|
10
|
-
interface MoveSelection {
|
|
11
|
-
source: ByteRange;
|
|
12
|
-
target: TargetSelection;
|
|
13
|
-
payload: Buffer;
|
|
14
|
-
}
|
|
15
|
-
export declare function checkPatchFile(patchPath: string, options?: ApplyOptions): Promise<ApplyResult>;
|
|
16
|
-
export declare function applyPatchFile(patchPath: string, options?: ApplyOptions): Promise<ApplyResult>;
|
|
17
|
-
export declare function selectMove(file: Buffer, patch: BlockPatch): MoveSelection;
|
|
18
|
-
export declare function applyMove(file: Buffer, selection: MoveSelection): Buffer;
|
|
19
|
-
export {};
|
package/dist/errors.d.ts
DELETED
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
// src/engine.ts
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { chmod, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
// src/errors.ts
|
|
7
|
-
class BlockPatchError extends Error {
|
|
8
|
-
code;
|
|
9
|
-
constructor(code, message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "BlockPatchError";
|
|
12
|
-
this.code = code;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function fail(code, message) {
|
|
16
|
-
throw new BlockPatchError(code, message);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// src/parser.ts
|
|
20
|
-
var beginLine = "*** Begin BlockPatch";
|
|
21
|
-
var sourceBeforeLine = "*** Source Before";
|
|
22
|
-
var sourcePayloadLine = "*** Source Payload";
|
|
23
|
-
var sourceAfterLine = "*** Source After";
|
|
24
|
-
var targetBeforeLine = "*** Target Before";
|
|
25
|
-
var targetAfterLine = "*** Target After";
|
|
26
|
-
var endLine = "*** End BlockPatch";
|
|
27
|
-
var moveFilePrefix = "*** Move File: ";
|
|
28
|
-
function parseBlockPatch(input) {
|
|
29
|
-
const markers = collectMarkers(input);
|
|
30
|
-
const sequence = markers.map((marker) => marker.name);
|
|
31
|
-
const targetIndex = sequence.findIndex((name) => name === "targetBefore" || name === "targetAfter");
|
|
32
|
-
if (targetIndex === -1) {
|
|
33
|
-
fail("parse_error", "Patch must contain *** Target Before or *** Target After");
|
|
34
|
-
}
|
|
35
|
-
const expectedPrefix = ["begin", "moveFile", "sourceBefore", "sourcePayload", "sourceAfter"];
|
|
36
|
-
const expectedSuffix = ["end"];
|
|
37
|
-
const expected = [...expectedPrefix, sequence[targetIndex], ...expectedSuffix];
|
|
38
|
-
if (sequence.length !== expected.length || sequence.some((name, index) => name !== expected[index])) {
|
|
39
|
-
fail("parse_error", "Patch markers must appear in the v0 order");
|
|
40
|
-
}
|
|
41
|
-
const moveFile = markers[1];
|
|
42
|
-
const path = moveFile.value?.trim();
|
|
43
|
-
if (!path) {
|
|
44
|
-
fail("parse_error", "*** Move File must include a path");
|
|
45
|
-
}
|
|
46
|
-
const sourceBefore = section(input, markers[2], markers[3]);
|
|
47
|
-
const sourcePayload = section(input, markers[3], markers[4]);
|
|
48
|
-
const sourceAfter = section(input, markers[4], markers[5]);
|
|
49
|
-
const targetAnchor = section(input, markers[5], markers[6]);
|
|
50
|
-
const target = {
|
|
51
|
-
kind: markers[5].name === "targetBefore" ? "before" : "after",
|
|
52
|
-
anchor: targetAnchor
|
|
53
|
-
};
|
|
54
|
-
if (sourceBefore.length === 0) {
|
|
55
|
-
fail("parse_error", "*** Source Before must not be empty");
|
|
56
|
-
}
|
|
57
|
-
if (sourcePayload.length === 0) {
|
|
58
|
-
fail("parse_error", "*** Source Payload must not be empty");
|
|
59
|
-
}
|
|
60
|
-
if (sourceAfter.length === 0) {
|
|
61
|
-
fail("parse_error", "*** Source After must not be empty");
|
|
62
|
-
}
|
|
63
|
-
if (target.anchor.length === 0) {
|
|
64
|
-
fail("parse_error", "*** Target Before/After must not be empty");
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
type: "move",
|
|
68
|
-
path,
|
|
69
|
-
sourceBefore,
|
|
70
|
-
sourcePayload,
|
|
71
|
-
sourceAfter,
|
|
72
|
-
target
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function collectMarkers(input) {
|
|
76
|
-
const markers = [];
|
|
77
|
-
let position = 0;
|
|
78
|
-
while (position <= input.length) {
|
|
79
|
-
const lineStart = position;
|
|
80
|
-
const lf = input.indexOf(10, position);
|
|
81
|
-
const lineEnd = lf === -1 ? input.length : lf;
|
|
82
|
-
const contentStart = lf === -1 ? input.length : lf + 1;
|
|
83
|
-
const textEnd = lineEnd > lineStart && input[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
|
|
84
|
-
const line = input.subarray(lineStart, textEnd).toString("utf8");
|
|
85
|
-
const marker = markerForLine(line, lineStart, contentStart);
|
|
86
|
-
if (marker !== undefined) {
|
|
87
|
-
markers.push(marker);
|
|
88
|
-
}
|
|
89
|
-
if (lf === -1) {
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
position = lf + 1;
|
|
93
|
-
}
|
|
94
|
-
if (markers.length === 0) {
|
|
95
|
-
fail("parse_error", "Patch does not contain BlockPatch markers");
|
|
96
|
-
}
|
|
97
|
-
return markers;
|
|
98
|
-
}
|
|
99
|
-
function markerForLine(line, lineStart, contentStart) {
|
|
100
|
-
if (line === beginLine)
|
|
101
|
-
return { name: "begin", lineStart, contentStart };
|
|
102
|
-
if (line.startsWith(moveFilePrefix)) {
|
|
103
|
-
return { name: "moveFile", value: line.slice(moveFilePrefix.length), lineStart, contentStart };
|
|
104
|
-
}
|
|
105
|
-
if (line === sourceBeforeLine)
|
|
106
|
-
return { name: "sourceBefore", lineStart, contentStart };
|
|
107
|
-
if (line === sourcePayloadLine)
|
|
108
|
-
return { name: "sourcePayload", lineStart, contentStart };
|
|
109
|
-
if (line === sourceAfterLine)
|
|
110
|
-
return { name: "sourceAfter", lineStart, contentStart };
|
|
111
|
-
if (line === targetBeforeLine)
|
|
112
|
-
return { name: "targetBefore", lineStart, contentStart };
|
|
113
|
-
if (line === targetAfterLine)
|
|
114
|
-
return { name: "targetAfter", lineStart, contentStart };
|
|
115
|
-
if (line === endLine)
|
|
116
|
-
return { name: "end", lineStart, contentStart };
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
function section(input, marker, nextMarker) {
|
|
120
|
-
let end = nextMarker.lineStart;
|
|
121
|
-
if (end > marker.contentStart && input[end - 1] === 10) {
|
|
122
|
-
end -= 1;
|
|
123
|
-
if (end > marker.contentStart && input[end - 1] === 13) {
|
|
124
|
-
end -= 1;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return Buffer.from(input.subarray(marker.contentStart, end));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// src/engine.ts
|
|
131
|
-
async function checkPatchFile(patchPath, options = {}) {
|
|
132
|
-
return runPatchFile(patchPath, { ...options, dryRun: true });
|
|
133
|
-
}
|
|
134
|
-
async function applyPatchFile(patchPath, options = {}) {
|
|
135
|
-
return runPatchFile(patchPath, options);
|
|
136
|
-
}
|
|
137
|
-
async function runPatchFile(patchPath, options) {
|
|
138
|
-
const cwd = options.cwd ?? process.cwd();
|
|
139
|
-
const patchBytes = await readFile(resolve(cwd, patchPath));
|
|
140
|
-
const patch = parseBlockPatch(patchBytes);
|
|
141
|
-
const changedPath = await applyMovePatch(patch, cwd, options.dryRun ?? false);
|
|
142
|
-
return { changed: [changedPath] };
|
|
143
|
-
}
|
|
144
|
-
async function applyMovePatch(patch, cwd, dryRun) {
|
|
145
|
-
const absolutePath = resolve(cwd, patch.path);
|
|
146
|
-
const original = await readFile(absolutePath);
|
|
147
|
-
const selection = selectMove(original, patch);
|
|
148
|
-
const next = applyMove(original, selection);
|
|
149
|
-
if (!dryRun && !next.equals(original)) {
|
|
150
|
-
await writeAtomic(absolutePath, next);
|
|
151
|
-
}
|
|
152
|
-
return patch.path;
|
|
153
|
-
}
|
|
154
|
-
function selectMove(file, patch) {
|
|
155
|
-
const source = findSourceRange(file, patch);
|
|
156
|
-
const target = findTarget(file, patch);
|
|
157
|
-
if (rangesOverlap(source, target.range)) {
|
|
158
|
-
fail("target_overlaps_source", `Target anchor for ${patch.path} overlaps the source block`);
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
source,
|
|
162
|
-
target,
|
|
163
|
-
payload: Buffer.from(file.subarray(source.start, source.end))
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function applyMove(file, selection) {
|
|
167
|
-
const withoutSource = Buffer.concat([
|
|
168
|
-
file.subarray(0, selection.source.start),
|
|
169
|
-
file.subarray(selection.source.end)
|
|
170
|
-
]);
|
|
171
|
-
const targetIndex = selection.target.insertIndex > selection.source.end ? selection.target.insertIndex - selection.payload.length : selection.target.insertIndex;
|
|
172
|
-
return Buffer.concat([
|
|
173
|
-
withoutSource.subarray(0, targetIndex),
|
|
174
|
-
selection.payload,
|
|
175
|
-
withoutSource.subarray(targetIndex)
|
|
176
|
-
]);
|
|
177
|
-
}
|
|
178
|
-
function findSourceRange(file, patch) {
|
|
179
|
-
const fullSource = Buffer.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
|
|
180
|
-
const fullMatches = indexesOf(file, fullSource);
|
|
181
|
-
if (fullMatches.length === 1) {
|
|
182
|
-
const start = fullMatches[0] + patch.sourceBefore.length;
|
|
183
|
-
return { start, end: start + patch.sourcePayload.length };
|
|
184
|
-
}
|
|
185
|
-
if (fullMatches.length > 1) {
|
|
186
|
-
fail("source_ambiguous", `Source block is ambiguous in ${patch.path}; matched ${fullMatches.length} locations`);
|
|
187
|
-
}
|
|
188
|
-
const envelopes = findSourceEnvelopes(file, patch);
|
|
189
|
-
if (envelopes.length === 1) {
|
|
190
|
-
fail("payload_mismatch", `Source payload does not match located source anchors in ${patch.path}`);
|
|
191
|
-
}
|
|
192
|
-
if (envelopes.length > 1) {
|
|
193
|
-
fail("source_ambiguous", `Source anchors are ambiguous in ${patch.path}; matched ${envelopes.length} locations`);
|
|
194
|
-
}
|
|
195
|
-
fail("source_not_found", `Source anchors were not found in ${patch.path}`);
|
|
196
|
-
}
|
|
197
|
-
function findSourceEnvelopes(file, patch) {
|
|
198
|
-
const beforeMatches = indexesOf(file, patch.sourceBefore);
|
|
199
|
-
const afterMatches = indexesOf(file, patch.sourceAfter);
|
|
200
|
-
const ranges = [];
|
|
201
|
-
for (const beforeStart of beforeMatches) {
|
|
202
|
-
const payloadStart = beforeStart + patch.sourceBefore.length;
|
|
203
|
-
const afterStart = afterMatches.find((candidate) => candidate >= payloadStart);
|
|
204
|
-
if (afterStart !== undefined) {
|
|
205
|
-
ranges.push({ start: payloadStart, end: afterStart });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return ranges;
|
|
209
|
-
}
|
|
210
|
-
function findTarget(file, patch) {
|
|
211
|
-
const matches = indexesOf(file, patch.target.anchor);
|
|
212
|
-
if (matches.length === 0) {
|
|
213
|
-
fail("target_not_found", `Target anchor was not found in ${patch.path}`);
|
|
214
|
-
}
|
|
215
|
-
if (matches.length > 1) {
|
|
216
|
-
fail("target_ambiguous", `Target anchor is ambiguous in ${patch.path}; matched ${matches.length} locations`);
|
|
217
|
-
}
|
|
218
|
-
const start = matches[0];
|
|
219
|
-
const end = start + patch.target.anchor.length;
|
|
220
|
-
return {
|
|
221
|
-
range: { start, end },
|
|
222
|
-
insertIndex: patch.target.kind === "before" ? start : end
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function indexesOf(haystack, needle) {
|
|
226
|
-
if (needle.length === 0) {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
const indexes = [];
|
|
230
|
-
let index = haystack.indexOf(needle);
|
|
231
|
-
while (index !== -1) {
|
|
232
|
-
indexes.push(index);
|
|
233
|
-
index = haystack.indexOf(needle, index + 1);
|
|
234
|
-
}
|
|
235
|
-
return indexes;
|
|
236
|
-
}
|
|
237
|
-
function rangesOverlap(left, right) {
|
|
238
|
-
return left.start < right.end && right.start < left.end;
|
|
239
|
-
}
|
|
240
|
-
async function writeAtomic(path, bytes) {
|
|
241
|
-
const info = await stat(path);
|
|
242
|
-
const dir = dirname(path);
|
|
243
|
-
const base = basename(path);
|
|
244
|
-
const temp = join(dir, `.${base}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
245
|
-
try {
|
|
246
|
-
await writeFile(temp, bytes, { flag: "wx" });
|
|
247
|
-
await chmod(temp, info.mode);
|
|
248
|
-
await rename(temp, path);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
await unlink(temp).catch(() => {
|
|
251
|
-
return;
|
|
252
|
-
});
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
export {
|
|
257
|
-
selectMove,
|
|
258
|
-
parseBlockPatch,
|
|
259
|
-
checkPatchFile,
|
|
260
|
-
applyPatchFile,
|
|
261
|
-
applyMove,
|
|
262
|
-
BlockPatchError
|
|
263
|
-
};
|
package/dist/parser.d.ts
DELETED
package/dist/types.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type TargetKind = "before" | "after";
|
|
2
|
-
export interface BlockPatch {
|
|
3
|
-
type: "move";
|
|
4
|
-
path: string;
|
|
5
|
-
sourceBefore: Buffer;
|
|
6
|
-
sourcePayload: Buffer;
|
|
7
|
-
sourceAfter: Buffer;
|
|
8
|
-
target: TargetAnchor;
|
|
9
|
-
}
|
|
10
|
-
export interface TargetAnchor {
|
|
11
|
-
kind: TargetKind;
|
|
12
|
-
anchor: Buffer;
|
|
13
|
-
}
|
|
14
|
-
export interface ApplyOptions {
|
|
15
|
-
cwd?: string;
|
|
16
|
-
dryRun?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export interface ApplyResult {
|
|
19
|
-
changed: string[];
|
|
20
|
-
}
|