@adecore/merge 0.21.1 → 0.22.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/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 fixed answer above represents a caller's completed decision. Unanswered draft conflicts use ours without markers; saving requires a separate unresolved check. Preserve text shape and verify input versions before writing. The host owns authorization, file access and stale-answer rejection.
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`. The isolated `test:memory` command checks bounded diff memory workloads. A source-relative example is in [examples/resolve-conflict.ts](./examples/resolve-conflict.ts).
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. This is what every other function here counts, draws and resolves.
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 = diagonal === -distance || (diagonal !== distance && reach[offset + diagonal - 1] < reach[offset + diagonal + 1]);
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 = diagonal === -step || (diagonal !== step && state[offset + diagonal - 1] < state[offset + diagonal + 1]);
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 the
54
- * two nobody would think twice about: the sides that differ only in whitespace inside or after a
55
- * line, and the side that already holds every line of the other without putting back what the other
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. Not a secret and not a digest anyone leans on: it only has to differ when
76
- * the stretch does, and it has to read the same on a daemon and in a browser, which rules out the
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: what a person edits here is
92
- * a file that would compile, and the other side is a column away rather than a line below. Blocks
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/merge",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Pure line diffs, three-way merge blocks and conflict resolution.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
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. This is what every other function here counts, draws and resolves.
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 = diagonal === -distance || (diagonal !== distance && reach[offset + diagonal - 1]! < reach[offset + diagonal + 1]!);
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 = diagonal === -step || (diagonal !== step && state[offset + diagonal - 1]! < state[offset + diagonal + 1]!);
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 the
64
- * two nobody would think twice about: the sides that differ only in whitespace inside or after a
65
- * line, and the side that already holds every line of the other without putting back what the other
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. Not a secret and not a digest anyone leans on: it only has to differ when
87
- * the stretch does, and it has to read the same on a daemon and in a browser, which rules out the
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: what a person edits here is
117
- * a file that would compile, and the other side is a column away rather than a line below. Blocks
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[] = [];