@adecore/merge 0.21.1 → 0.21.2
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/README.md +2 -2
- package/dist/blocks.js +1 -1
- package/dist/diff.js +7 -7
- package/dist/resolve.js +7 -11
- package/package.json +1 -1
- package/src/blocks.ts +1 -1
- package/src/diff.ts +8 -7
- package/src/resolve.ts +7 -11
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ for (const [index, block] of blocks.entries()) {
|
|
|
24
24
|
const text = joinLines(draftOf(blocks, answers).lines, shapeOf(ours));
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The
|
|
27
|
+
The answer in the example stands for a decision the caller already made. A draft takes ours, without markers, for every conflict that has no answer yet, so check for unresolved conflicts separately before saving. Keep the shape of the text, and check that the inputs are still the versions you read before writing. Authorization, file access and refusing a stale answer are the host's.
|
|
28
28
|
|
|
29
29
|
Read the [overview](https://adecore.dev/merge/), [lines and diffs](https://adecore.dev/merge/lines) and [three-way merge](https://adecore.dev/merge/three-way).
|
|
30
30
|
|
|
31
|
-
From the repository root, run `bun run --cwd packages/merge test`, `typecheck` or `build`.
|
|
31
|
+
From the repository root, run `bun run --cwd packages/merge test`, `typecheck` or `build`. `test:memory` runs on its own and checks that a diff of two large files stays under 20 MB. [examples/resolve-conflict.ts](./examples/resolve-conflict.ts) is a whole example that imports the source.
|
package/dist/blocks.js
CHANGED
|
@@ -22,7 +22,7 @@ function delta(change) {
|
|
|
22
22
|
* The three versions of a file as the stretches they agree and disagree on, in reading order. A
|
|
23
23
|
* stretch is taken up by both sides at once only where their changes overlap in the base, or where
|
|
24
24
|
* both added something in exactly the same place; anything else is one side's alone and merges
|
|
25
|
-
* without asking.
|
|
25
|
+
* without asking.
|
|
26
26
|
*/
|
|
27
27
|
export function splitBlocks(base, ours, theirs) {
|
|
28
28
|
const ourChanges = diffLines(base, ours);
|
package/dist/diff.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* How far apart two files may be before the exact walk costs more than the answer is worth. Past
|
|
3
|
-
* this the two are answered as one stretch that differs whole, which reads as a single conflict
|
|
4
|
-
* instead of a list nobody would work through anyway.
|
|
5
|
-
*/
|
|
1
|
+
/* Past this edit distance two files are answered as one stretch that differs whole. */
|
|
6
2
|
const MAX_DISTANCE = 4000;
|
|
7
3
|
/* The walk keeps every STRIDE-th state and walks the rounds between two again on the way back: a
|
|
8
4
|
state per round came to over a hundred megabytes at MAX_DISTANCE. */
|
|
9
5
|
const STRIDE = 64;
|
|
6
|
+
/* Whether a diagonal's furthest reach steps in from diagonal + 1 (an insert) rather than diagonal - 1 (a delete). */
|
|
7
|
+
function cameDown(reach, offset, diagonal, distance) {
|
|
8
|
+
return diagonal === -distance || (diagonal !== distance && reach[offset + diagonal - 1] < reach[offset + diagonal + 1]);
|
|
9
|
+
}
|
|
10
10
|
/* One round of the walk: every diagonal of this distance taken as far as its lines match. True once the far corner is reached. */
|
|
11
11
|
function extend(left, right, reach, offset, distance) {
|
|
12
12
|
for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
|
|
13
|
-
const down =
|
|
13
|
+
const down = cameDown(reach, offset, diagonal, distance);
|
|
14
14
|
let x = down ? reach[offset + diagonal + 1] : reach[offset + diagonal - 1] + 1;
|
|
15
15
|
let y = x - diagonal;
|
|
16
16
|
while (x < left.length && y < right.length && left[x] === right[y]) {
|
|
@@ -42,7 +42,7 @@ function backtrack(left, right, saved, distance, offset) {
|
|
|
42
42
|
for (; step > first; step -= 1) {
|
|
43
43
|
const state = states[step - first];
|
|
44
44
|
const diagonal = x - y;
|
|
45
|
-
const down =
|
|
45
|
+
const down = cameDown(state, offset, diagonal, step);
|
|
46
46
|
const previous = down ? diagonal + 1 : diagonal - 1;
|
|
47
47
|
const prevX = state[offset + previous];
|
|
48
48
|
const prevY = prevX - previous;
|
package/dist/resolve.js
CHANGED
|
@@ -50,10 +50,9 @@ function covers(base, outer, inner) {
|
|
|
50
50
|
return added.every((line) => !base.includes(line) || inner.includes(line));
|
|
51
51
|
}
|
|
52
52
|
/*
|
|
53
|
-
* What the wand makes of a conflict, or null for one that needs a person after all. It closes
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* deleted. Anything else is a choice, and a wand that guesses at those is a wand nobody trusts twice.
|
|
53
|
+
* What the wand makes of a conflict, or null for one that needs a person after all. It closes only
|
|
54
|
+
* sides that differ in whitespace inside or after a line, and a side that already holds every line
|
|
55
|
+
* of the other without putting back what the other deleted. Anything else is a choice it never guesses.
|
|
57
56
|
*/
|
|
58
57
|
export function wandLines(block) {
|
|
59
58
|
if (block.kind !== 'conflict') {
|
|
@@ -72,9 +71,8 @@ export function wandLines(block) {
|
|
|
72
71
|
}
|
|
73
72
|
/*
|
|
74
73
|
* A short name for the two sides of one block, so an answer written for it can be checked against
|
|
75
|
-
* the block it lands on.
|
|
76
|
-
*
|
|
77
|
-
* one hash a browser cannot take synchronously.
|
|
74
|
+
* the block it lands on. FNV-1a, not a digest: it has to read the same on a server and in a browser,
|
|
75
|
+
* and a browser has no synchronous crypto hash.
|
|
78
76
|
*/
|
|
79
77
|
export function fingerprint(block) {
|
|
80
78
|
let hash = 0x811c9dc5;
|
|
@@ -88,10 +86,8 @@ export function fingerprint(block) {
|
|
|
88
86
|
}
|
|
89
87
|
/*
|
|
90
88
|
* The merged file to start from: everything that merges by itself is merged, and every conflict
|
|
91
|
-
* holds our side until someone says otherwise. Never a conflict marker
|
|
92
|
-
*
|
|
93
|
-
* that have already been answered are passed in, which is how a proposal lands in a file nobody has
|
|
94
|
-
* open.
|
|
89
|
+
* holds our side until someone says otherwise. Never a conflict marker, so what a person edits is a
|
|
90
|
+
* file that would compile. `answered` holds the lines of blocks already resolved, by block index.
|
|
95
91
|
*/
|
|
96
92
|
export function draftOf(blocks, answered = new Map()) {
|
|
97
93
|
const lines = [];
|
package/package.json
CHANGED
package/src/blocks.ts
CHANGED
|
@@ -40,7 +40,7 @@ function delta(change: Change): number {
|
|
|
40
40
|
* The three versions of a file as the stretches they agree and disagree on, in reading order. A
|
|
41
41
|
* stretch is taken up by both sides at once only where their changes overlap in the base, or where
|
|
42
42
|
* both added something in exactly the same place; anything else is one side's alone and merges
|
|
43
|
-
* without asking.
|
|
43
|
+
* without asking.
|
|
44
44
|
*/
|
|
45
45
|
export function splitBlocks(base: readonly string[], ours: readonly string[], theirs: readonly string[]): MergeBlock[] {
|
|
46
46
|
const ourChanges = diffLines(base, ours);
|
package/src/diff.ts
CHANGED
|
@@ -9,11 +9,7 @@ export interface Change {
|
|
|
9
9
|
otherEnd: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
* How far apart two files may be before the exact walk costs more than the answer is worth. Past
|
|
14
|
-
* this the two are answered as one stretch that differs whole, which reads as a single conflict
|
|
15
|
-
* instead of a list nobody would work through anyway.
|
|
16
|
-
*/
|
|
12
|
+
/* Past this edit distance two files are answered as one stretch that differs whole. */
|
|
17
13
|
const MAX_DISTANCE = 4000;
|
|
18
14
|
|
|
19
15
|
/* The walk keeps every STRIDE-th state and walks the rounds between two again on the way back: a
|
|
@@ -28,10 +24,15 @@ interface Move {
|
|
|
28
24
|
y: number;
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
/* Whether a diagonal's furthest reach steps in from diagonal + 1 (an insert) rather than diagonal - 1 (a delete). */
|
|
28
|
+
function cameDown(reach: Int32Array, offset: number, diagonal: number, distance: number): boolean {
|
|
29
|
+
return diagonal === -distance || (diagonal !== distance && reach[offset + diagonal - 1]! < reach[offset + diagonal + 1]!);
|
|
30
|
+
}
|
|
31
|
+
|
|
31
32
|
/* One round of the walk: every diagonal of this distance taken as far as its lines match. True once the far corner is reached. */
|
|
32
33
|
function extend(left: readonly string[], right: readonly string[], reach: Int32Array, offset: number, distance: number): boolean {
|
|
33
34
|
for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
|
|
34
|
-
const down =
|
|
35
|
+
const down = cameDown(reach, offset, diagonal, distance);
|
|
35
36
|
let x = down ? reach[offset + diagonal + 1]! : reach[offset + diagonal - 1]! + 1;
|
|
36
37
|
let y = x - diagonal;
|
|
37
38
|
while (x < left.length && y < right.length && left[x] === right[y]) {
|
|
@@ -64,7 +65,7 @@ function backtrack(left: readonly string[], right: readonly string[], saved: rea
|
|
|
64
65
|
for (; step > first; step -= 1) {
|
|
65
66
|
const state = states[step - first]!;
|
|
66
67
|
const diagonal = x - y;
|
|
67
|
-
const down =
|
|
68
|
+
const down = cameDown(state, offset, diagonal, step);
|
|
68
69
|
const previous = down ? diagonal + 1 : diagonal - 1;
|
|
69
70
|
const prevX = state[offset + previous]!;
|
|
70
71
|
const prevY = prevX - previous;
|
package/src/resolve.ts
CHANGED
|
@@ -60,10 +60,9 @@ function covers(base: readonly string[], outer: readonly string[], inner: readon
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/*
|
|
63
|
-
* What the wand makes of a conflict, or null for one that needs a person after all. It closes
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* deleted. Anything else is a choice, and a wand that guesses at those is a wand nobody trusts twice.
|
|
63
|
+
* What the wand makes of a conflict, or null for one that needs a person after all. It closes only
|
|
64
|
+
* sides that differ in whitespace inside or after a line, and a side that already holds every line
|
|
65
|
+
* of the other without putting back what the other deleted. Anything else is a choice it never guesses.
|
|
67
66
|
*/
|
|
68
67
|
export function wandLines(block: MergeBlock): string[] | null {
|
|
69
68
|
if (block.kind !== 'conflict') {
|
|
@@ -83,9 +82,8 @@ export function wandLines(block: MergeBlock): string[] | null {
|
|
|
83
82
|
|
|
84
83
|
/*
|
|
85
84
|
* A short name for the two sides of one block, so an answer written for it can be checked against
|
|
86
|
-
* the block it lands on.
|
|
87
|
-
*
|
|
88
|
-
* one hash a browser cannot take synchronously.
|
|
85
|
+
* the block it lands on. FNV-1a, not a digest: it has to read the same on a server and in a browser,
|
|
86
|
+
* and a browser has no synchronous crypto hash.
|
|
89
87
|
*/
|
|
90
88
|
export function fingerprint(block: MergeBlock): string {
|
|
91
89
|
let hash = 0x811c9dc5;
|
|
@@ -113,10 +111,8 @@ export interface MergeDraft {
|
|
|
113
111
|
|
|
114
112
|
/*
|
|
115
113
|
* The merged file to start from: everything that merges by itself is merged, and every conflict
|
|
116
|
-
* holds our side until someone says otherwise. Never a conflict marker
|
|
117
|
-
*
|
|
118
|
-
* that have already been answered are passed in, which is how a proposal lands in a file nobody has
|
|
119
|
-
* open.
|
|
114
|
+
* holds our side until someone says otherwise. Never a conflict marker, so what a person edits is a
|
|
115
|
+
* file that would compile. `answered` holds the lines of blocks already resolved, by block index.
|
|
120
116
|
*/
|
|
121
117
|
export function draftOf(blocks: readonly MergeBlock[], answered: ReadonlyMap<number, readonly string[]> = new Map()): MergeDraft {
|
|
122
118
|
const lines: string[] = [];
|