├── .eslintrc
├── .github
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── RELEASE.md
├── index.d.ts
├── index.mjs
├── package.json
└── test
├── LCS.test.js
├── diff3Merge.test.js
├── diff3MergeRegions.test.js
├── diffComm.test.js
├── diffIndices.test.js
├── diffPatch.test.js
├── merge.test.js
├── mergeDiff3.test.js
└── mergeDigIn.test.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 6,
8 | "sourceType": "module"
9 | },
10 | "extends": [
11 | "eslint:recommended"
12 | ],
13 | "rules": {
14 | "dot-notation": "error",
15 | "eqeqeq": ["error", "smart"],
16 | "indent": ["off", 4],
17 | "keyword-spacing": "error",
18 | "linebreak-style": ["error", "unix"],
19 | "no-caller": "error",
20 | "no-catch-shadow": "error",
21 | "no-console": "warn",
22 | "no-div-regex": "error",
23 | "no-extend-native": "error",
24 | "no-extra-bind": "error",
25 | "no-floating-decimal": "error",
26 | "no-implied-eval": "error",
27 | "no-invalid-this": "error",
28 | "no-iterator": "error",
29 | "no-labels": "error",
30 | "no-label-var": "error",
31 | "no-lone-blocks": "error",
32 | "no-loop-func": "error",
33 | "no-multi-str": "error",
34 | "no-native-reassign": "error",
35 | "no-new": "error",
36 | "no-new-func": "error",
37 | "no-new-wrappers": "error",
38 | "no-octal": "error",
39 | "no-octal-escape": "error",
40 | "no-process-env": "error",
41 | "no-proto": "error",
42 | "no-return-assign": "off",
43 | "no-script-url": "error",
44 | "no-self-compare": "error",
45 | "no-sequences": "error",
46 | "no-shadow": "off",
47 | "no-shadow-restricted-names": "error",
48 | "no-throw-literal": "error",
49 | "no-unneeded-ternary": "error",
50 | "no-unused-expressions": "error",
51 | "no-unexpected-multiline": "error",
52 | "no-unused-vars": "warn",
53 | "no-void": "error",
54 | "no-warning-comments": "warn",
55 | "no-with": "error",
56 | "no-use-before-define": ["off", "nofunc"],
57 | "semi": ["error", "always"],
58 | "semi-spacing": "error",
59 | "space-unary-ops": "error",
60 | "wrap-regex": "off",
61 | "quotes": ["error", "single"]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm"
7 | directory: "/"
8 | schedule:
9 | interval: "monthly"
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "monthly"
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | FORCE_COLOR: 2
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | node-version: ['18', '20', '22']
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm install
28 | - run: npm run all
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .esm-cache
3 | .vscode
4 | .coverage
5 |
6 | .nyc_output/
7 | built/
8 | coverage/
9 | dist/
10 | node_modules/
11 |
12 | npm-debug.log
13 | lerna-debug.log
14 | package-lock.json
15 | yarn.lock
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # What's New
2 |
3 | **node-diff3** is an open source project. You can submit bug reports, help out,
4 | or learn more by visiting our project page on GitHub: :octocat: https://github.com/bhousel/node-diff3
5 |
6 | Please star our project on GitHub to show your support! ⭐️
7 |
8 | _Breaking changes, which may affect downstream projects, are marked with a_ ⚠️
9 |
10 |
17 |
18 | ## 3.1.2
19 | ##### 2022-Jun-06
20 |
21 | * Rearrange "default" export to end of list for Webpack ([#58])
22 |
23 | [#58]: https://github.com/bhousel/node-diff3/issues/58
24 |
25 |
26 | ## 3.1.1
27 | ##### 2022-Jun-03
28 |
29 | * Fix exports property for Typescript declaration file ([#57])
30 |
31 | [#57]: https://github.com/bhousel/node-diff3/issues/57
32 |
33 |
34 | ## 3.1.0
35 | ##### 2021-Sep-24
36 |
37 | * Add `sideEffects: false` to `package.json` so bundlers like webpack can treeshake
38 | * Remove the hardcoded `\n` from conflict boundaries ([#46], [#48])
39 | * Users who want to view the results of a merge will probably do something like `console.log(result.join('\n'));`, so having extra `\n` in there is unhelpful.
40 |
41 | [#46]: https://github.com/bhousel/node-diff3/issues/46
42 | [#48]: https://github.com/bhousel/node-diff3/issues/48
43 |
44 |
45 | ## 3.0.0
46 | ##### 2021-Jun-26
47 |
48 | * ⚠️ Replace rollup with [esbuild](https://esbuild.github.io/) for super fast build speed. Package outputs are now:
49 | * `"module": "./index.mjs"` - ESM, modern JavaScript, works with `import`
50 | * `"main": "./dist/index.cjs"` - CJS bundle, modern JavaScript, works with `require()`
51 | * `"browser": "./dist/index.iife.js"` - IIFE bundle, modern JavaScript, works in browser `
43 |
44 | …
45 |
51 | ```
52 |
53 | 👉 This project uses modern JavaScript syntax for use in supported node versions and modern browsers. If you need support for legacy environments like ES5 or Internet Explorer, you'll need to build your own bundle with something like [Babel](https://babeljs.io/docs/en/index.html).
54 |
55 |
56 |
57 | ## API Reference
58 |
59 | * [3-way diff and merging](#3-way-diff-and-merging)
60 | * [diff3Merge](#diff3Merge)
61 | * [merge](#merge)
62 | * [mergeDiff3](#mergeDiff3)
63 | * [mergeDigIn](#mergeDigIn)
64 | * [diff3MergeRegions](#diff3MergeRegions)
65 | * [2-way diff and patching](#2-way-diff-and-patching)
66 | * [diffPatch](#diffPatch)
67 | * [patch](#patch)
68 | * [stripPatch](#stripPatch)
69 | * [invertPatch](#invertPatch)
70 | * [diffComm](#diffComm)
71 | * [diffIndices](#diffIndices)
72 | * [Longest Common Sequence (LCS)](#longest-common-sequence-lcs)
73 | * [LCS](#LCS)
74 |
75 |
76 |
77 | ### 3-way diff and merging
78 |
79 |
80 |
81 | # Diff3.diff3Merge(a, o, b, options)
82 |
83 | Performs a 3-way diff on buffers `o` (original), and `a` and `b` (changed).
84 | The buffers may be arrays or strings. If strings, they will be split into arrays on whitespace `/\s+/` by default.
85 | The returned result alternates between "ok" and "conflict" blocks.
86 |
87 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diff3Merge.test.js
88 |
89 | ```js
90 | const o = ['AA', 'ZZ', '00', 'M', '99'];
91 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
92 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
93 | const result = Diff3.diff3Merge(a, o, b);
94 | ```
95 |
96 | Options may passed as an object:
97 | ```js
98 | {
99 | excludeFalseConflicts: true,
100 | stringSeparator: /\s+/
101 | }
102 | ```
103 |
104 | * `excludeFalseConflicts` - If both `a` and `b` contain an identical change from `o`, this is considered a "false" conflict.
105 | * `stringSeparator` - If inputs buffers are strings, this controls how to split the strings into arrays. The separator value may be a string or a regular expression, as it is just passed to [String.split()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split).
106 |
107 |
108 |
109 | # Diff3.merge(a, o, b, options)
110 |
111 | Passes arguments to [diff3Merge](#diff3Merge) to generate a diff3-style merge result.
112 |
113 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/merge.test.js
114 |
115 | ```js
116 | const r = Diff3.merge(a, o, b);
117 | const result = r.result;
118 | // [
119 | // 'AA',
120 | // '<<<<<<<',
121 | // 'a',
122 | // 'b',
123 | // 'c',
124 | // '=======',
125 | // 'a',
126 | // 'd',
127 | // 'c',
128 | // '>>>>>>>',
129 | // 'ZZ',
130 | // '<<<<<<<',
131 | // 'new',
132 | // '00',
133 | // 'a',
134 | // 'a',
135 | // '=======',
136 | // '11',
137 | // '>>>>>>>',
138 | // 'M',
139 | // 'z',
140 | // 'z',
141 | // '99'
142 | // ]
143 | ```
144 |
145 |
146 |
147 | # Diff3.mergeDiff3(a, o, b, options)
148 |
149 | Passes arguments to [diff3Merge](#diff3Merge) to generate a diff3-style merge result with original (similar to [git-diff3](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging)).
150 |
151 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/mergeDiff3.test.js
152 |
153 | ```js
154 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' } });
155 | const result = r.result;
156 | // [
157 | // 'AA',
158 | // '<<<<<<< a',
159 | // 'a',
160 | // 'b',
161 | // 'c',
162 | // '||||||| o',
163 | // '=======',
164 | // 'a',
165 | // 'd',
166 | // 'c',
167 | // '>>>>>>> b',
168 | // 'ZZ',
169 | // '<<<<<<< a',
170 | // 'new',
171 | // '00',
172 | // 'a',
173 | // 'a',
174 | // '||||||| o',
175 | // '00',
176 | // '=======',
177 | // '11',
178 | // '>>>>>>> b',
179 | // 'M',
180 | // 'z',
181 | // 'z',
182 | // '99'
183 | // ]
184 | ```
185 |
186 | Extra options:
187 | ```js
188 | {
189 | // labels for conflict marker lines
190 | label: {
191 | a: 'a',
192 | o: 'o',
193 | b: 'b'
194 | },
195 | }
196 | ```
197 |
198 |
199 |
200 | # Diff3.mergeDigIn(a, o, b, options)
201 |
202 | Passes arguments to [diff3Merge](#diff3Merge) to generate a digin-style merge result.
203 |
204 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/mergeDigIn.test.js
205 |
206 |
207 |
208 | # Diff3.diff3MergeRegions(a, o, b)
209 |
210 | Low-level function used by [diff3Merge](#diff3Merge) to determine the stable and unstable regions between `a`, `o`, `b`.
211 |
212 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diff3MergeRegions.test.js
213 |
214 |
215 |
216 |
217 | ### 2-way diff and patching
218 |
219 |
220 |
221 | # Diff3.diffPatch(buffer1, buffer2)
222 |
223 | Performs a diff between arrays `buffer1` and `buffer2`.
224 | The returned `patch` result contains the information about the differing regions and can be applied to `buffer1` to yield `buffer2`.
225 |
226 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js
227 |
228 | ```js
229 | const buffer1 = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
230 | const buffer2 = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
231 | const patch = Diff3.diffPatch(buffer1, buffer2);
232 | // `patch` contains the information needed to turn `buffer1` into `buffer2`
233 | ```
234 |
235 |
236 |
237 | # Diff3.patch(buffer1, patch)
238 |
239 | Applies a patch to a buffer, returning a new buffer without modifying the original.
240 |
241 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js
242 |
243 | ```js
244 | const result = Diff3.patch(buffer1, patch);
245 | // `result` contains a new arrray which is a copy of `buffer2`
246 | ```
247 |
248 |
249 |
250 | # Diff3.stripPatch(patch)
251 |
252 | Strips some extra information from the patch, returning a new patch without modifying the original.
253 | The "stripped" patch can still patch `buffer1` -> `buffer2`, but can no longer be inverted.
254 |
255 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js
256 |
257 | ```js
258 | const stripped = Diff3.stripPatch(patch);
259 | // `stripped` contains a copy of a patch but with the extra information removed
260 | ```
261 |
262 |
263 |
264 | # Diff3.invertPatch(patch)
265 |
266 | Inverts the patch (for example to turn `buffer2` back into `buffer1`), returning a new patch without modifying the original.
267 |
268 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffPatch.test.js
269 |
270 | ```js
271 | const inverted = Diff3.invertPatch(patch);
272 | // `inverted` contains a copy of a patch to turn `buffer2` back into `buffer1`
273 | ```
274 |
275 |
276 |
277 | # Diff3.diffComm(buffer1, buffer2)
278 |
279 | Returns a comm-style result of the differences between `buffer1` and `buffer2`.
280 |
281 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffComm.test.js
282 |
283 |
284 |
285 | # Diff3.diffIndices(buffer1, buffer2)
286 |
287 | Low-level function used by [diff3MergeRegions](#diff3MergeRegions) to determine differing regions between `buffer1` and `buffer2`.
288 |
289 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/diffIndices.test.js
290 |
291 |
292 |
293 |
294 | ### Longest Common Sequence (LCS)
295 |
296 |
297 |
298 | # Diff3.LCS(buffer1, buffer2)
299 |
300 | Low-level function used by other functions to find the LCS between `buffer1` and `buffer2`.
301 | Returns a result linked list chain containing the common sequence path.
302 |
303 | See also:
304 | * http://www.cs.dartmouth.edu/~doug/
305 | * https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
306 |
307 | See examples: https://github.com/bhousel/node-diff3/blob/main/test/LCS.test.js
308 |
309 |
310 |
311 |
312 | ## License
313 |
314 | This project is available under the [MIT License](https://opensource.org/licenses/MIT).
315 | See the [LICENSE.md](LICENSE.md) file for more details.
316 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | ## Release Checklist
2 |
3 | #### Update version, tag, and publish
4 | ```bash
5 | $ git checkout main
6 | $ npm install
7 | $ npm run test
8 | $ Update CHANGELOG
9 | $ Update version number in `package.json`
10 | $ git add .
11 | $ git commit -m 'vA.B.C'
12 | $ git tag vA.B.C
13 | $ git push origin main vA.B.C
14 | $ npm publish
15 |
16 | ```
17 | * Open https://github.com/bhousel/node-diff3/tags
18 | * Click "Add Release Notes" and link to the CHANGELOG#version
19 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface ILCSResult {
2 | buffer1index: number;
3 | buffer2index: number;
4 | chain: null | ILCSResult;
5 | }
6 |
7 | /**
8 | * Expects two arrays, finds longest common sequence
9 | * @param {T[]} buffer1
10 | * @param {T[]} buffer2
11 | * @returns {ILCSResult}
12 | * @constructor
13 | */
14 | export function LCS(buffer1: T[], buffer2: T[]): ILCSResult;
15 |
16 | export interface ICommResult {
17 | buffer1: T[];
18 | buffer2: T[];
19 | }
20 |
21 | /**
22 | * We apply the LCS to build a 'comm'-style picture of the
23 | * differences between buffer1 and buffer2.
24 | * @param {T[]} buffer1
25 | * @param {T[]} buffer2
26 | * @returns {Array>}
27 | */
28 | export function diffComm(buffer1: T[], buffer2: T[]): ICommResult[];
29 |
30 | export interface IDiffIndicesResult {
31 | buffer1: [number, number];
32 | buffer1Content: T[];
33 | buffer2: [number, number];
34 | buffer2Content: T[];
35 | }
36 |
37 | /**
38 | * We apply the LCS to give a simple representation of the
39 | * offsets and lengths of mismatched chunks in the input
40 | * buffers. This is used by diff3MergeRegions.
41 | * @param {T[]} buffer1
42 | * @param {T[]} buffer2
43 | * @returns {IDiffIndicesResult[]}
44 | */
45 | export function diffIndices(
46 | buffer1: T[],
47 | buffer2: T[]
48 | ): IDiffIndicesResult[];
49 |
50 | export interface IChunk {
51 | offset: number;
52 | length: number;
53 | chunk: T[];
54 | }
55 |
56 | export interface IPatchRes {
57 | buffer1: IChunk;
58 | buffer2: IChunk;
59 | }
60 |
61 | /**
62 | * We apply the LCS to build a JSON representation of a
63 | * diff(1)-style patch.
64 | * @param {T[]} buffer1
65 | * @param {T[]} buffer2
66 | * @returns {IPatchRes[]}
67 | */
68 | export function diffPatch(buffer1: T[], buffer2: T[]): IPatchRes[];
69 |
70 | export function patch(buffer: T[], patch: IPatchRes[]): T[];
71 |
72 | export interface IStableRegion {
73 | stable: true;
74 | buffer: 'a' | 'o' | 'b';
75 | bufferStart: number;
76 | bufferLength: number;
77 | bufferContent: T[];
78 | }
79 |
80 | export interface IUnstableRegion {
81 | stable: false;
82 | aStart: number;
83 | aLength: number;
84 | aContent: T[];
85 | bStart: number;
86 | bLength: number;
87 | bContent: T[];
88 | oStart: number;
89 | oLength: number;
90 | oContent: T[];
91 | }
92 |
93 | export type IRegion = IStableRegion | IUnstableRegion;
94 |
95 | /**
96 | * Given three buffers, A, O, and B, where both A and B are
97 | * independently derived from O, returns a fairly complicated
98 | * internal representation of merge decisions it's taken. The
99 | * interested reader may wish to consult
100 | *
101 | * Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce.
102 | * 'A Formal Investigation of ' In Arvind and Prasad,
103 | * editors, Foundations of Software Technology and Theoretical
104 | * Computer Science (FSTTCS), December 2007.
105 | *
106 | * (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf)
107 | *
108 | * @param {T[]} a
109 | * @param {T[]} o
110 | * @param {T[]} b
111 | * @returns {IRegion[]}
112 | */
113 | export function diff3MergeRegions(a: T[], o: T[], b: T[]): IRegion[];
114 |
115 | export interface MergeRegion {
116 | ok?: T[];
117 | conflict?: {
118 | a: T[];
119 | aIndex: number;
120 | b: T[];
121 | bIndex: number;
122 | o: T[];
123 | oIndex: number;
124 | };
125 | }
126 |
127 | export interface MergeResult {
128 | conflict: boolean;
129 | result: string[];
130 | }
131 |
132 | export interface IMergeOptions {
133 | excludeFalseConflicts?: boolean;
134 | stringSeparator?: string | RegExp;
135 | }
136 |
137 | /**
138 | * Applies the output of diff3MergeRegions to actually
139 | * construct the merged buffer; the returned result alternates
140 | * between 'ok' and 'conflict' blocks.
141 | * A "false conflict" is where `a` and `b` both change the same from `o`
142 | *
143 | * @param {string | T[]} a
144 | * @param {string | T[]} o
145 | * @param {string | T[]} b
146 | * @param {{excludeFalseConflicts: boolean; stringSeparator: RegExp}} options
147 | * @returns {MergeRegion[]}
148 | */
149 | export function diff3Merge(
150 | a: string | T[],
151 | o: string | T[],
152 | b: string | T[],
153 | options?: IMergeOptions
154 | ): MergeRegion[];
155 |
156 | export function merge(
157 | a: string | T[],
158 | o: string | T[],
159 | b: string | T[],
160 | options?: IMergeOptions
161 | ): MergeResult;
162 |
163 | export function mergeDiff3(
164 | a: string | T[],
165 | o: string | T[],
166 | b: string | T[],
167 | options?: IMergeOptions & {
168 | label?: {
169 | a?: string;
170 | o?: string;
171 | b?: string;
172 | }
173 | }
174 | ): MergeResult;
175 |
176 | export function mergeDigIn(
177 | a: string | T[],
178 | o: string | T[],
179 | b: string | T[],
180 | options?: IMergeOptions
181 | ): MergeResult;
182 |
--------------------------------------------------------------------------------
/index.mjs:
--------------------------------------------------------------------------------
1 | export {
2 | LCS,
3 | diffComm,
4 | diffIndices,
5 | diffPatch,
6 | diff3MergeRegions,
7 | diff3Merge,
8 | mergeDiff3,
9 | merge,
10 | mergeDigIn,
11 | patch,
12 | stripPatch,
13 | invertPatch
14 | };
15 |
16 |
17 | // Text diff algorithm following Hunt and McIlroy 1976.
18 | // J. W. Hunt and M. D. McIlroy, An algorithm for differential buffer
19 | // comparison, Bell Telephone Laboratories CSTR #41 (1976)
20 | // http://www.cs.dartmouth.edu/~doug/
21 | // https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
22 | //
23 | // Expects two arrays, finds longest common sequence
24 | function LCS(buffer1, buffer2) {
25 |
26 | let equivalenceClasses = {};
27 | for (let j = 0; j < buffer2.length; j++) {
28 | const item = buffer2[j];
29 | if (equivalenceClasses[item]) {
30 | equivalenceClasses[item].push(j);
31 | } else {
32 | equivalenceClasses[item] = [j];
33 | }
34 | }
35 |
36 | const NULLRESULT = { buffer1index: -1, buffer2index: -1, chain: null };
37 | let candidates = [NULLRESULT];
38 |
39 | for (let i = 0; i < buffer1.length; i++) {
40 | const item = buffer1[i];
41 | const buffer2indices = equivalenceClasses[item] || [];
42 | let r = 0;
43 | let c = candidates[0];
44 |
45 | for (let jx = 0; jx < buffer2indices.length; jx++) {
46 | const j = buffer2indices[jx];
47 |
48 | let s;
49 | for (s = r; s < candidates.length; s++) {
50 | if ((candidates[s].buffer2index < j) && ((s === candidates.length - 1) || (candidates[s + 1].buffer2index > j))) {
51 | break;
52 | }
53 | }
54 |
55 | if (s < candidates.length) {
56 | const newCandidate = { buffer1index: i, buffer2index: j, chain: candidates[s] };
57 | if (r === candidates.length) {
58 | candidates.push(c);
59 | } else {
60 | candidates[r] = c;
61 | }
62 | r = s + 1;
63 | c = newCandidate;
64 | if (r === candidates.length) {
65 | break; // no point in examining further (j)s
66 | }
67 | }
68 | }
69 |
70 | candidates[r] = c;
71 | }
72 |
73 | // At this point, we know the LCS: it's in the reverse of the
74 | // linked-list through .chain of candidates[candidates.length - 1].
75 |
76 | return candidates[candidates.length - 1];
77 | }
78 |
79 |
80 | // We apply the LCS to build a 'comm'-style picture of the
81 | // differences between buffer1 and buffer2.
82 | function diffComm(buffer1, buffer2) {
83 | const lcs = LCS(buffer1, buffer2);
84 | let result = [];
85 | let tail1 = buffer1.length;
86 | let tail2 = buffer2.length;
87 | let common = {common: []};
88 |
89 | function processCommon() {
90 | if (common.common.length) {
91 | common.common.reverse();
92 | result.push(common);
93 | common = {common: []};
94 | }
95 | }
96 |
97 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {
98 | let different = {buffer1: [], buffer2: []};
99 |
100 | while (--tail1 > candidate.buffer1index) {
101 | different.buffer1.push(buffer1[tail1]);
102 | }
103 |
104 | while (--tail2 > candidate.buffer2index) {
105 | different.buffer2.push(buffer2[tail2]);
106 | }
107 |
108 | if (different.buffer1.length || different.buffer2.length) {
109 | processCommon();
110 | different.buffer1.reverse();
111 | different.buffer2.reverse();
112 | result.push(different);
113 | }
114 |
115 | if (tail1 >= 0) {
116 | common.common.push(buffer1[tail1]);
117 | }
118 | }
119 |
120 | processCommon();
121 |
122 | result.reverse();
123 | return result;
124 | }
125 |
126 |
127 | // We apply the LCS to give a simple representation of the
128 | // offsets and lengths of mismatched chunks in the input
129 | // buffers. This is used by diff3MergeRegions.
130 | function diffIndices(buffer1, buffer2) {
131 | const lcs = LCS(buffer1, buffer2);
132 | let result = [];
133 | let tail1 = buffer1.length;
134 | let tail2 = buffer2.length;
135 |
136 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {
137 | const mismatchLength1 = tail1 - candidate.buffer1index - 1;
138 | const mismatchLength2 = tail2 - candidate.buffer2index - 1;
139 | tail1 = candidate.buffer1index;
140 | tail2 = candidate.buffer2index;
141 |
142 | if (mismatchLength1 || mismatchLength2) {
143 | result.push({
144 | buffer1: [tail1 + 1, mismatchLength1],
145 | buffer1Content: buffer1.slice(tail1 + 1, tail1 + 1 + mismatchLength1),
146 | buffer2: [tail2 + 1, mismatchLength2],
147 | buffer2Content: buffer2.slice(tail2 + 1, tail2 + 1 + mismatchLength2)
148 | });
149 | }
150 | }
151 |
152 | result.reverse();
153 | return result;
154 | }
155 |
156 |
157 | // We apply the LCS to build a JSON representation of a
158 | // diff(1)-style patch.
159 | function diffPatch(buffer1, buffer2) {
160 | const lcs = LCS(buffer1, buffer2);
161 | let result = [];
162 | let tail1 = buffer1.length;
163 | let tail2 = buffer2.length;
164 |
165 | function chunkDescription(buffer, offset, length) {
166 | let chunk = [];
167 | for (let i = 0; i < length; i++) {
168 | chunk.push(buffer[offset + i]);
169 | }
170 | return {
171 | offset: offset,
172 | length: length,
173 | chunk: chunk
174 | };
175 | }
176 |
177 | for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {
178 | const mismatchLength1 = tail1 - candidate.buffer1index - 1;
179 | const mismatchLength2 = tail2 - candidate.buffer2index - 1;
180 | tail1 = candidate.buffer1index;
181 | tail2 = candidate.buffer2index;
182 |
183 | if (mismatchLength1 || mismatchLength2) {
184 | result.push({
185 | buffer1: chunkDescription(buffer1, candidate.buffer1index + 1, mismatchLength1),
186 | buffer2: chunkDescription(buffer2, candidate.buffer2index + 1, mismatchLength2)
187 | });
188 | }
189 | }
190 |
191 | result.reverse();
192 | return result;
193 | }
194 |
195 |
196 | // Given three buffers, A, O, and B, where both A and B are
197 | // independently derived from O, returns a fairly complicated
198 | // internal representation of merge decisions it's taken. The
199 | // interested reader may wish to consult
200 | //
201 | // Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce.
202 | // 'A Formal Investigation of ' In Arvind and Prasad,
203 | // editors, Foundations of Software Technology and Theoretical
204 | // Computer Science (FSTTCS), December 2007.
205 | //
206 | // (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf)
207 | //
208 | function diff3MergeRegions(a, o, b) {
209 |
210 | // "hunks" are array subsets where `a` or `b` are different from `o`
211 | // https://www.gnu.org/software/diffutils/manual/html_node/diff3-Hunks.html
212 | let hunks = [];
213 | function addHunk(h, ab) {
214 | hunks.push({
215 | ab: ab,
216 | oStart: h.buffer1[0],
217 | oLength: h.buffer1[1], // length of o to remove
218 | abStart: h.buffer2[0],
219 | abLength: h.buffer2[1] // length of a/b to insert
220 | // abContent: (ab === 'a' ? a : b).slice(h.buffer2[0], h.buffer2[0] + h.buffer2[1])
221 | });
222 | }
223 |
224 | diffIndices(o, a).forEach(item => addHunk(item, 'a'));
225 | diffIndices(o, b).forEach(item => addHunk(item, 'b'));
226 | hunks.sort((x,y) => x.oStart - y.oStart);
227 |
228 | let results = [];
229 | let currOffset = 0;
230 |
231 | function advanceTo(endOffset) {
232 | if (endOffset > currOffset) {
233 | results.push({
234 | stable: true,
235 | buffer: 'o',
236 | bufferStart: currOffset,
237 | bufferLength: endOffset - currOffset,
238 | bufferContent: o.slice(currOffset, endOffset)
239 | });
240 | currOffset = endOffset;
241 | }
242 | }
243 |
244 | while (hunks.length) {
245 | let hunk = hunks.shift();
246 | let regionStart = hunk.oStart;
247 | let regionEnd = hunk.oStart + hunk.oLength;
248 | let regionHunks = [hunk];
249 | advanceTo(regionStart);
250 |
251 | // Try to pull next overlapping hunk into this region
252 | while (hunks.length) {
253 | const nextHunk = hunks[0];
254 | const nextHunkStart = nextHunk.oStart;
255 | if (nextHunkStart > regionEnd) break; // no overlap
256 |
257 | regionEnd = Math.max(regionEnd, nextHunkStart + nextHunk.oLength);
258 | regionHunks.push(hunks.shift());
259 | }
260 |
261 | if (regionHunks.length === 1) {
262 | // Only one hunk touches this region, meaning that there is no conflict here.
263 | // Either `a` or `b` is inserting into a region of `o` unchanged by the other.
264 | if (hunk.abLength > 0) {
265 | const buffer = (hunk.ab === 'a' ? a : b);
266 | results.push({
267 | stable: true,
268 | buffer: hunk.ab,
269 | bufferStart: hunk.abStart,
270 | bufferLength: hunk.abLength,
271 | bufferContent: buffer.slice(hunk.abStart, hunk.abStart + hunk.abLength)
272 | });
273 | }
274 | } else {
275 | // A true a/b conflict. Determine the bounds involved from `a`, `o`, and `b`.
276 | // Effectively merge all the `a` hunks into one giant hunk, then do the
277 | // same for the `b` hunks; then, correct for skew in the regions of `o`
278 | // that each side changed, and report appropriate spans for the three sides.
279 | let bounds = {
280 | a: [a.length, -1, o.length, -1],
281 | b: [b.length, -1, o.length, -1]
282 | };
283 | while (regionHunks.length) {
284 | hunk = regionHunks.shift();
285 | const oStart = hunk.oStart;
286 | const oEnd = oStart + hunk.oLength;
287 | const abStart = hunk.abStart;
288 | const abEnd = abStart + hunk.abLength;
289 | let b = bounds[hunk.ab];
290 | b[0] = Math.min(abStart, b[0]);
291 | b[1] = Math.max(abEnd, b[1]);
292 | b[2] = Math.min(oStart, b[2]);
293 | b[3] = Math.max(oEnd, b[3]);
294 | }
295 |
296 | const aStart = bounds.a[0] + (regionStart - bounds.a[2]);
297 | const aEnd = bounds.a[1] + (regionEnd - bounds.a[3]);
298 | const bStart = bounds.b[0] + (regionStart - bounds.b[2]);
299 | const bEnd = bounds.b[1] + (regionEnd - bounds.b[3]);
300 |
301 | let result = {
302 | stable: false,
303 | aStart: aStart,
304 | aLength: aEnd - aStart,
305 | aContent: a.slice(aStart, aEnd),
306 | oStart: regionStart,
307 | oLength: regionEnd - regionStart,
308 | oContent: o.slice(regionStart, regionEnd),
309 | bStart: bStart,
310 | bLength: bEnd - bStart,
311 | bContent: b.slice(bStart, bEnd)
312 | };
313 | results.push(result);
314 | }
315 | currOffset = regionEnd;
316 | }
317 |
318 | advanceTo(o.length);
319 |
320 | return results;
321 | }
322 |
323 |
324 | // Applies the output of diff3MergeRegions to actually
325 | // construct the merged buffer; the returned result alternates
326 | // between 'ok' and 'conflict' blocks.
327 | // A "false conflict" is where `a` and `b` both change the same from `o`
328 | function diff3Merge(a, o, b, options) {
329 | let defaults = {
330 | excludeFalseConflicts: true,
331 | stringSeparator: /\s+/
332 | };
333 | options = Object.assign(defaults, options);
334 |
335 | if (typeof a === 'string') a = a.split(options.stringSeparator);
336 | if (typeof o === 'string') o = o.split(options.stringSeparator);
337 | if (typeof b === 'string') b = b.split(options.stringSeparator);
338 |
339 | let results = [];
340 | const regions = diff3MergeRegions(a, o, b);
341 |
342 | let okBuffer = [];
343 | function flushOk() {
344 | if (okBuffer.length) {
345 | results.push({ ok: okBuffer });
346 | }
347 | okBuffer = [];
348 | }
349 |
350 | function isFalseConflict(a, b) {
351 | if (a.length !== b.length) return false;
352 | for (let i = 0; i < a.length; i++) {
353 | if (a[i] !== b[i]) return false;
354 | }
355 | return true;
356 | }
357 |
358 | regions.forEach(region => {
359 | if (region.stable) {
360 | okBuffer.push(...region.bufferContent);
361 | } else {
362 | if (options.excludeFalseConflicts && isFalseConflict(region.aContent, region.bContent)) {
363 | okBuffer.push(...region.aContent);
364 | } else {
365 | flushOk();
366 | results.push({
367 | conflict: {
368 | a: region.aContent,
369 | aIndex: region.aStart,
370 | o: region.oContent,
371 | oIndex: region.oStart,
372 | b: region.bContent,
373 | bIndex: region.bStart
374 | }
375 | });
376 | }
377 | }
378 | });
379 |
380 | flushOk();
381 | return results;
382 | }
383 |
384 |
385 | function mergeDiff3(a, o, b, options) {
386 | const defaults = {
387 | excludeFalseConflicts: true,
388 | stringSeparator: /\s+/,
389 | label: {}
390 | };
391 | options = Object.assign(defaults, options);
392 |
393 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');
394 | const oSection = '|||||||' + (options.label.o ? ` ${options.label.o}` : '');
395 | const xSection = '=======';
396 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');
397 |
398 | const regions = diff3Merge(a, o, b, options);
399 | let conflict = false;
400 | let result = [];
401 |
402 | regions.forEach(region => {
403 | if (region.ok) {
404 | result = result.concat(region.ok);
405 | } else if (region.conflict) {
406 | conflict = true;
407 | result = result.concat(
408 | [aSection],
409 | region.conflict.a,
410 | [oSection],
411 | region.conflict.o,
412 | [xSection],
413 | region.conflict.b,
414 | [bSection]
415 | );
416 | }
417 | });
418 |
419 | return {
420 | conflict: conflict,
421 | result: result
422 | };
423 | }
424 |
425 |
426 | function merge(a, o, b, options) {
427 | const defaults = {
428 | excludeFalseConflicts: true,
429 | stringSeparator: /\s+/,
430 | label: {}
431 | };
432 | options = Object.assign(defaults, options);
433 |
434 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');
435 | const xSection = '=======';
436 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');
437 |
438 | const regions = diff3Merge(a, o, b, options);
439 | let conflict = false;
440 | let result = [];
441 |
442 | regions.forEach(region => {
443 | if (region.ok) {
444 | result = result.concat(region.ok);
445 | } else if (region.conflict) {
446 | conflict = true;
447 | result = result.concat(
448 | [aSection],
449 | region.conflict.a,
450 | [xSection],
451 | region.conflict.b,
452 | [bSection]
453 | );
454 | }
455 | });
456 |
457 | return {
458 | conflict: conflict,
459 | result: result
460 | };
461 | }
462 |
463 |
464 | function mergeDigIn(a, o, b, options) {
465 | const defaults = {
466 | excludeFalseConflicts: true,
467 | stringSeparator: /\s+/,
468 | label: {}
469 | };
470 | options = Object.assign(defaults, options);
471 |
472 | const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');
473 | const xSection = '=======';
474 | const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');
475 |
476 | const regions = diff3Merge(a, o, b, options);
477 | let conflict = false;
478 | let result = [];
479 |
480 | regions.forEach(region => {
481 | if (region.ok) {
482 | result = result.concat(region.ok);
483 | } else {
484 | const c = diffComm(region.conflict.a, region.conflict.b);
485 | for (let j = 0; j < c.length; j++) {
486 | let inner = c[j];
487 | if (inner.common) {
488 | result = result.concat(inner.common);
489 | } else {
490 | conflict = true;
491 | result = result.concat(
492 | [aSection],
493 | inner.buffer1,
494 | [xSection],
495 | inner.buffer2,
496 | [bSection]
497 | );
498 | }
499 | }
500 | }
501 | });
502 |
503 | return {
504 | conflict: conflict,
505 | result: result
506 | };
507 | }
508 |
509 |
510 | // Applies a patch to a buffer.
511 | // Given buffer1 and buffer2, `patch(buffer1, diffPatch(buffer1, buffer2))` should give buffer2.
512 | function patch(buffer, patch) {
513 | let result = [];
514 | let currOffset = 0;
515 |
516 | function advanceTo(targetOffset) {
517 | while (currOffset < targetOffset) {
518 | result.push(buffer[currOffset]);
519 | currOffset++;
520 | }
521 | }
522 |
523 | for (let chunkIndex = 0; chunkIndex < patch.length; chunkIndex++) {
524 | let chunk = patch[chunkIndex];
525 | advanceTo(chunk.buffer1.offset);
526 | for (let itemIndex = 0; itemIndex < chunk.buffer2.chunk.length; itemIndex++) {
527 | result.push(chunk.buffer2.chunk[itemIndex]);
528 | }
529 | currOffset += chunk.buffer1.length;
530 | }
531 |
532 | advanceTo(buffer.length);
533 | return result;
534 | }
535 |
536 |
537 | // Takes the output of diffPatch(), and removes extra information from it.
538 | // It can still be used by patch(), below, but can no longer be inverted.
539 | function stripPatch(patch) {
540 | return patch.map(chunk => ({
541 | buffer1: { offset: chunk.buffer1.offset, length: chunk.buffer1.length },
542 | buffer2: { chunk: chunk.buffer2.chunk }
543 | }));
544 | }
545 |
546 |
547 | // Takes the output of diffPatch(), and inverts the sense of it, so that it
548 | // can be applied to buffer2 to give buffer1 rather than the other way around.
549 | function invertPatch(patch) {
550 | return patch.map(chunk => ({
551 | buffer1: chunk.buffer2,
552 | buffer2: chunk.buffer1
553 | }));
554 | }
555 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-diff3",
3 | "version": "3.1.2",
4 | "license": "MIT",
5 | "repository": "github:bhousel/node-diff3",
6 | "description": "A node.js module for text diffing and three-way-merge.",
7 | "contributors": [
8 | "Bryan Housel (https://github.com/bhousel)"
9 | ],
10 | "keywords": [
11 | "diff",
12 | "diff3",
13 | "diffutils",
14 | "gnu",
15 | "javascript",
16 | "merge",
17 | "nodejs",
18 | "patch"
19 | ],
20 | "files": [
21 | "index.mjs",
22 | "index.d.ts",
23 | "dist/"
24 | ],
25 | "type": "module",
26 | "source": "./index.mjs",
27 | "types": "./index.d.ts",
28 | "main": "./dist/index.cjs",
29 | "module": "./index.mjs",
30 | "exports": {
31 | ".": {
32 | "import": {
33 | "types": "./index.d.ts",
34 | "default": "./index.mjs"
35 | },
36 | "require": "./dist/index.cjs"
37 | }
38 | },
39 | "scripts": {
40 | "all": "run-s clean test",
41 | "clean": "shx rm -rf dist",
42 | "build": "run-p build:**",
43 | "build:browser": "esbuild ./index.mjs --platform=browser --format=iife --global-name=Diff3 --bundle --sourcemap --outfile=./dist/index.iife.js",
44 | "build:cjs": "esbuild ./index.mjs --platform=node --format=cjs --sourcemap --outfile=./dist/index.cjs",
45 | "lint": "eslint index.mjs test/*.js",
46 | "test": "run-s build test:node",
47 | "test:node": "c8 node --test test/*.js"
48 | },
49 | "devDependencies": {
50 | "c8": "^10.1.2",
51 | "esbuild": "^0.23.1",
52 | "eslint": "^9.10.0",
53 | "npm-run-all": "^4.1.5",
54 | "shx": "^0.3.4"
55 | },
56 | "sideEffects": false,
57 | "publishConfig": {
58 | "access": "public"
59 | },
60 | "engines": {
61 | "node": ">=18"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/LCS.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('LCS', async t => {
6 |
7 | await t.test('returns the LCS of two arrays', t => {
8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
10 | const result = Diff3.LCS(a, b);
11 | const NULLRESULT = { buffer1index: -1, buffer2index: -1, chain: null };
12 |
13 | assert.deepEqual(result.buffer1index, 10); // '99'
14 | assert.deepEqual(result.buffer2index, 9);
15 |
16 | assert.deepEqual(result.chain.buffer1index, 9); // 'M'
17 | assert.deepEqual(result.chain.buffer2index, 6);
18 |
19 | assert.deepEqual(result.chain.chain.buffer1index, 4); // 'ZZ'
20 | assert.deepEqual(result.chain.chain.buffer2index, 4);
21 |
22 | assert.deepEqual(result.chain.chain.chain.buffer1index, 3); // 'c'
23 | assert.deepEqual(result.chain.chain.chain.buffer2index, 3);
24 |
25 | assert.deepEqual(result.chain.chain.chain.chain.buffer1index, 1); // 'a'
26 | assert.deepEqual(result.chain.chain.chain.chain.buffer2index, 1);
27 |
28 | assert.deepEqual(result.chain.chain.chain.chain.chain.buffer1index, 0); // 'AA'
29 | assert.deepEqual(result.chain.chain.chain.chain.chain.buffer2index, 0);
30 | assert.deepEqual(result.chain.chain.chain.chain.chain.chain, NULLRESULT);
31 | });
32 |
33 | });
34 |
--------------------------------------------------------------------------------
/test/diff3Merge.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('diff3Merge', async t => {
6 |
7 | await t.test('performs diff3 merge on arrays', t => {
8 | const o = ['AA', 'ZZ', '00', 'M', '99'];
9 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
10 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
11 | const result = Diff3.diff3Merge(a, o, b);
12 |
13 | /*
14 | AA
15 | <<<<<<< a
16 | a
17 | b
18 | c
19 | ||||||| o
20 | =======
21 | a
22 | d
23 | c
24 | >>>>>>> b
25 | ZZ
26 | <<<<<<< a
27 | new
28 | 00
29 | a
30 | a
31 | ||||||| o
32 | 00
33 | =======
34 | 11
35 | >>>>>>> b
36 | M
37 | z
38 | z
39 | 99
40 | */
41 |
42 | assert.deepEqual(result[0].ok, ['AA']);
43 | assert.deepEqual(result[0].conflict, undefined);
44 |
45 | assert.deepEqual(result[1].ok, undefined);
46 | assert.deepEqual(result[1].conflict.o, []);
47 | assert.deepEqual(result[1].conflict.a, ['a', 'b', 'c']);
48 | assert.deepEqual(result[1].conflict.b, ['a', 'd', 'c']);
49 |
50 | assert.deepEqual(result[2].ok, ['ZZ']);
51 | assert.deepEqual(result[2].conflict, undefined);
52 |
53 | assert.deepEqual(result[3].ok, undefined);
54 | assert.deepEqual(result[3].conflict.o, ['00']);
55 | assert.deepEqual(result[3].conflict.a, ['new', '00', 'a', 'a']);
56 | assert.deepEqual(result[3].conflict.b, ['11']);
57 |
58 | assert.deepEqual(result[4].ok, ['M', 'z', 'z', '99']);
59 | assert.deepEqual(result[4].conflict, undefined);
60 | });
61 |
62 |
63 | await t.test('strings split on whitespace by default to avoid surprises - issue #9', t => {
64 | const o = 'was touring';
65 | const a = 'was here touring';
66 | const b = 'was into touring';
67 | const result = Diff3.diff3Merge(a, o, b);
68 |
69 | assert.deepEqual(result[0].ok, ['was']);
70 | assert.deepEqual(result[0].conflict, undefined);
71 |
72 | assert.deepEqual(result[1].ok, undefined);
73 | assert.deepEqual(result[1].conflict.o, []);
74 | assert.deepEqual(result[1].conflict.a, ['here']);
75 | assert.deepEqual(result[1].conflict.b, ['into']);
76 |
77 | assert.deepEqual(result[2].ok, ['touring']);
78 | assert.deepEqual(result[2].conflict, undefined);
79 | });
80 |
81 | await t.test('strings can optionally split on given separator', t => {
82 | const o = 'new hampshire, new mexico, north carolina';
83 | const a = 'new hampshire, new jersey, north carolina';
84 | const b = 'new hampshire, new york, north carolina';
85 | const result = Diff3.diff3Merge(a, o, b, { stringSeparator: /,\s+/ });
86 |
87 | assert.deepEqual(result[0].ok, ['new hampshire']);
88 | assert.deepEqual(result[0].conflict, undefined);
89 |
90 | assert.deepEqual(result[1].ok, undefined);
91 | assert.deepEqual(result[1].conflict.o, ['new mexico']);
92 | assert.deepEqual(result[1].conflict.a, ['new jersey']);
93 | assert.deepEqual(result[1].conflict.b, ['new york']);
94 |
95 | assert.deepEqual(result[2].ok, ['north carolina']);
96 | assert.deepEqual(result[2].conflict, undefined);
97 | });
98 |
99 |
100 | await t.test('excludes false conflicts by default', t => {
101 | const o = 'AA ZZ';
102 | const a = 'AA a b c ZZ';
103 | const b = 'AA a b c ZZ';
104 | const result = Diff3.diff3Merge(a, o, b);
105 |
106 | assert.deepEqual(result[0].ok, ['AA', 'a', 'b', 'c', 'ZZ']);
107 | assert.deepEqual(result[0].conflict, undefined);
108 | });
109 |
110 |
111 | await t.test('can include false conflicts with option', t => {
112 | const o = 'AA ZZ';
113 | const a = 'AA a b c ZZ';
114 | const b = 'AA a b c ZZ';
115 | const result = Diff3.diff3Merge(a, o, b, { excludeFalseConflicts: false });
116 |
117 | assert.deepEqual(result[0].ok, ['AA']);
118 | assert.deepEqual(result[0].conflict, undefined);
119 |
120 | assert.deepEqual(result[1].ok, undefined);
121 | assert.deepEqual(result[1].conflict.o, []);
122 | assert.deepEqual(result[1].conflict.a, ['a', 'b', 'c']);
123 | assert.deepEqual(result[1].conflict.b, ['a', 'b', 'c']);
124 |
125 | assert.deepEqual(result[2].ok, ['ZZ']);
126 | assert.deepEqual(result[2].conflict, undefined);
127 | });
128 |
129 |
130 | await t.test('avoids improper hunk sorting - see openstreetmap/iD#3058', t => {
131 | const o = ['n4100522632', 'n4100697091', 'n4100697136', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4102677890', 'n4094374176'];
132 | const a = ['n4100522632', 'n4100697091', 'n4100697136', 'n-10000', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4102677890', 'n4094374176'];
133 | const b = ['n4100522632', 'n4100697091', 'n4100697136', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4105613618', 'n4102677890', 'n4105613617', 'n4094374176'];
134 | const expected = ['n4100522632', 'n4100697091', 'n4100697136', 'n-10000', 'n4102671583', 'n4102671584', 'n4102671585', 'n4102671586', 'n4102671587', 'n4102671588', 'n4102677889', 'n4105613618', 'n4102677890', 'n4105613617', 'n4094374176'];
135 | const result = Diff3.diff3Merge(a, o, b);
136 |
137 | assert.deepEqual(result[0].ok, expected);
138 | });
139 |
140 |
141 | await t.test('yaml comparison - issue #46', t => {
142 | const o = `title: "title"
143 | description: "description"`;
144 | const a = `title: "title"
145 | description: "description changed"`;
146 | const b = `title: "title changed"
147 | description: "description"`;
148 | const result = Diff3.diff3Merge(a, o, b, { stringSeparator: /[\r\n]+/ });
149 |
150 | assert.deepEqual(result[0].ok, undefined);
151 | assert.deepEqual(result[0].conflict.o, ['title: "title"', 'description: "description"']);
152 | assert.deepEqual(result[0].conflict.a, ['title: "title"', 'description: "description changed"']);
153 | assert.deepEqual(result[0].conflict.b, ['title: "title changed"', 'description: "description"']);
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/test/diff3MergeRegions.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('diff3MergeRegions', async t => {
6 |
7 | await t.test('returns results of 3-way diff from o,a,b arrays', t => {
8 | const o = ['AA', 'ZZ', '00', 'M', '99'];
9 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
10 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
11 | const result = Diff3.diff3MergeRegions(a, o, b);
12 |
13 | assert.deepEqual(result[0].stable, true);
14 | assert.deepEqual(result[0].buffer, 'o');
15 | assert.deepEqual(result[0].bufferStart, 0);
16 | assert.deepEqual(result[0].bufferLength, 1);
17 | assert.deepEqual(result[0].bufferContent, ['AA']);
18 |
19 | assert.deepEqual(result[1].stable, false);
20 | assert.deepEqual(result[1].aStart, 1);
21 | assert.deepEqual(result[1].aLength, 3);
22 | assert.deepEqual(result[1].aContent, ['a', 'b', 'c']);
23 | assert.deepEqual(result[1].oStart, 1);
24 | assert.deepEqual(result[1].oLength, 0);
25 | assert.deepEqual(result[1].oContent, []);
26 | assert.deepEqual(result[1].bStart, 1);
27 | assert.deepEqual(result[1].bLength, 3);
28 | assert.deepEqual(result[1].bContent, ['a', 'd', 'c']);
29 |
30 | assert.deepEqual(result[2].stable, true);
31 | assert.deepEqual(result[2].buffer, 'o');
32 | assert.deepEqual(result[2].bufferStart, 1);
33 | assert.deepEqual(result[2].bufferLength, 1);
34 | assert.deepEqual(result[2].bufferContent, ['ZZ']);
35 |
36 | assert.deepEqual(result[3].stable, false);
37 | assert.deepEqual(result[3].aStart, 5);
38 | assert.deepEqual(result[3].aLength, 4);
39 | assert.deepEqual(result[3].aContent, ['new', '00', 'a', 'a']);
40 | assert.deepEqual(result[3].oStart, 2);
41 | assert.deepEqual(result[3].oLength, 1);
42 | assert.deepEqual(result[3].oContent, ['00']);
43 | assert.deepEqual(result[3].bStart, 5);
44 | assert.deepEqual(result[3].bLength, 1);
45 | assert.deepEqual(result[3].bContent, ['11']);
46 |
47 | assert.deepEqual(result[4].stable, true);
48 | assert.deepEqual(result[4].buffer, 'o');
49 | assert.deepEqual(result[4].bufferStart, 3);
50 | assert.deepEqual(result[4].bufferLength, 1);
51 | assert.deepEqual(result[4].bufferContent, ['M']);
52 |
53 | assert.deepEqual(result[5].stable, true);
54 | assert.deepEqual(result[5].buffer, 'b');
55 | assert.deepEqual(result[5].bufferStart, 7);
56 | assert.deepEqual(result[5].bufferLength, 2);
57 | assert.deepEqual(result[5].bufferContent, ['z', 'z']);
58 |
59 | assert.deepEqual(result[6].stable, true);
60 | assert.deepEqual(result[6].buffer, 'o');
61 | assert.deepEqual(result[6].bufferStart, 4);
62 | assert.deepEqual(result[6].bufferLength, 1);
63 | assert.deepEqual(result[6].bufferContent, ['99']);
64 | });
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/test/diffComm.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('diffComm', async t => {
6 |
7 | await t.test('returns a comm-style diff of two arrays', t => {
8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
10 | const result = Diff3.diffComm(a, b);
11 |
12 | assert.deepEqual(result[0].common, ['AA', 'a']);
13 | assert.deepEqual(result[0].buffer1, undefined);
14 | assert.deepEqual(result[0].buffer2, undefined);
15 |
16 | assert.deepEqual(result[1].common, undefined);
17 | assert.deepEqual(result[1].buffer1, ['b']);
18 | assert.deepEqual(result[1].buffer2, ['d']);
19 |
20 | assert.deepEqual(result[2].common, ['c', 'ZZ']);
21 | assert.deepEqual(result[2].buffer1, undefined);
22 | assert.deepEqual(result[2].buffer2, undefined);
23 |
24 | assert.deepEqual(result[3].common, undefined);
25 | assert.deepEqual(result[3].buffer1, ['new', '00', 'a', 'a']);
26 | assert.deepEqual(result[3].buffer2, ['11']);
27 |
28 | assert.deepEqual(result[4].common, ['M']);
29 | assert.deepEqual(result[4].buffer1, undefined);
30 | assert.deepEqual(result[4].buffer2, undefined);
31 |
32 | assert.deepEqual(result[5].common, undefined);
33 | assert.deepEqual(result[5].buffer1, []);
34 | assert.deepEqual(result[5].buffer2, ['z', 'z']);
35 |
36 | assert.deepEqual(result[6].common, ['99']);
37 | assert.deepEqual(result[6].buffer1, undefined);
38 | assert.deepEqual(result[6].buffer2, undefined);
39 | });
40 |
41 | });
--------------------------------------------------------------------------------
/test/diffIndices.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('diffIndices', async t => {
6 |
7 | await t.test('returns array indices for differing regions of two arrays', t => {
8 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
9 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
10 | const result = Diff3.diffIndices(a, b);
11 |
12 | assert.deepEqual(result[0].buffer1, [2, 1]);
13 | assert.deepEqual(result[0].buffer1Content, ['b']);
14 | assert.deepEqual(result[0].buffer2, [2, 1]);
15 | assert.deepEqual(result[0].buffer2Content, ['d']);
16 |
17 | assert.deepEqual(result[1].buffer1, [5, 4]);
18 | assert.deepEqual(result[1].buffer1Content, ['new', '00', 'a', 'a']);
19 | assert.deepEqual(result[1].buffer2, [5, 1]);
20 | assert.deepEqual(result[1].buffer2Content, ['11']);
21 |
22 | assert.deepEqual(result[2].buffer1, [10, 0]);
23 | assert.deepEqual(result[2].buffer1Content, []);
24 | assert.deepEqual(result[2].buffer2, [7, 2]);
25 | assert.deepEqual(result[2].buffer2Content, ['z', 'z']);
26 | });
27 |
28 | });
--------------------------------------------------------------------------------
/test/diffPatch.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
6 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
7 | const a0 = a;
8 | const b0 = b;
9 |
10 | test('diffPatch', async t => {
11 | await t.test('returns a patch-style diff of two arrays', t => {
12 | const result = Diff3.diffPatch(a, b);
13 |
14 | assert.deepEqual(result[0].buffer1.offset, 2);
15 | assert.deepEqual(result[0].buffer1.length, 1);
16 | assert.deepEqual(result[0].buffer1.chunk, ['b']);
17 | assert.deepEqual(result[0].buffer2.offset, 2);
18 | assert.deepEqual(result[0].buffer2.length, 1);
19 | assert.deepEqual(result[0].buffer2.chunk, ['d']);
20 |
21 | assert.deepEqual(result[1].buffer1.offset, 5);
22 | assert.deepEqual(result[1].buffer1.length, 4);
23 | assert.deepEqual(result[1].buffer1.chunk, ['new', '00', 'a', 'a']);
24 | assert.deepEqual(result[1].buffer2.offset, 5);
25 | assert.deepEqual(result[1].buffer2.length, 1);
26 | assert.deepEqual(result[1].buffer2.chunk, ['11']);
27 |
28 | assert.deepEqual(result[2].buffer1.offset, 10);
29 | assert.deepEqual(result[2].buffer1.length, 0);
30 | assert.deepEqual(result[2].buffer1.chunk, []);
31 | assert.deepEqual(result[2].buffer2.offset, 7);
32 | assert.deepEqual(result[2].buffer2.length, 2);
33 | assert.deepEqual(result[2].buffer2.chunk, ['z', 'z']);
34 | });
35 |
36 | await t.test('did not modify buffer1 or buffer2', t => {
37 | assert.equal(a0, a);
38 | assert.equal(b0, b);
39 | });
40 | });
41 |
42 |
43 | test('patch', async t => {
44 | await t.test('applies a patch against buffer1 to get buffer2', t => {
45 | const patch = Diff3.diffPatch(a, b);
46 | const result = Diff3.patch(a, patch);
47 | assert.deepEqual(result, b);
48 | });
49 |
50 | await t.test('did not modify buffer1 or buffer2', t => {
51 | assert.equal(a0, a);
52 | assert.equal(b0, b);
53 | });
54 | });
55 |
56 |
57 | test('stripPatch', async t => {
58 | const patch = Diff3.diffPatch(a, b);
59 | const strip = Diff3.stripPatch(patch);
60 |
61 | await t.test('removes extra information from the diffPatch result', t => {
62 | assert.deepEqual(strip[0].buffer1.offset, 2);
63 | assert.deepEqual(strip[0].buffer1.length, 1);
64 | assert.deepEqual(strip[0].buffer1.chunk, undefined);
65 | assert.deepEqual(strip[0].buffer2.offset, undefined);
66 | assert.deepEqual(strip[0].buffer2.length, undefined);
67 | assert.deepEqual(strip[0].buffer2.chunk, ['d']);
68 |
69 | assert.deepEqual(strip[1].buffer1.offset, 5);
70 | assert.deepEqual(strip[1].buffer1.length, 4);
71 | assert.deepEqual(strip[1].buffer1.chunk, undefined);
72 | assert.deepEqual(strip[1].buffer2.offset, undefined);
73 | assert.deepEqual(strip[1].buffer2.length, undefined);
74 | assert.deepEqual(strip[1].buffer2.chunk, ['11']);
75 |
76 | assert.deepEqual(strip[2].buffer1.offset, 10);
77 | assert.deepEqual(strip[2].buffer1.length, 0);
78 | assert.deepEqual(strip[2].buffer1.chunk, undefined);
79 | assert.deepEqual(strip[2].buffer2.offset, undefined);
80 | assert.deepEqual(strip[2].buffer2.length, undefined);
81 | assert.deepEqual(strip[2].buffer2.chunk, ['z', 'z']);
82 | });
83 |
84 | await t.test('applies a stripped patch against buffer1 to get buffer2', t => {
85 | const result = Diff3.patch(a, strip);
86 | assert.deepEqual(result, b);
87 | });
88 |
89 | await t.test('did not modify the original patch', t => {
90 | assert.notEqual(patch, strip);
91 | assert.notEqual(patch[0], strip[0]);
92 | assert.notEqual(patch[1], strip[1]);
93 | assert.notEqual(patch[2], strip[2]);
94 | });
95 |
96 | });
97 |
98 |
99 | test('invertPatch', async t => {
100 | const patch = Diff3.diffPatch(a, b);
101 | const invert = Diff3.invertPatch(patch);
102 |
103 | await t.test('inverts the diffPatch result', t => {
104 | assert.deepEqual(invert[0].buffer2.offset, 2);
105 | assert.deepEqual(invert[0].buffer2.length, 1);
106 | assert.deepEqual(invert[0].buffer2.chunk, ['b']);
107 | assert.deepEqual(invert[0].buffer1.offset, 2);
108 | assert.deepEqual(invert[0].buffer1.length, 1);
109 | assert.deepEqual(invert[0].buffer1.chunk, ['d']);
110 |
111 | assert.deepEqual(invert[1].buffer2.offset, 5);
112 | assert.deepEqual(invert[1].buffer2.length, 4);
113 | assert.deepEqual(invert[1].buffer2.chunk, ['new', '00', 'a', 'a']);
114 | assert.deepEqual(invert[1].buffer1.offset, 5);
115 | assert.deepEqual(invert[1].buffer1.length, 1);
116 | assert.deepEqual(invert[1].buffer1.chunk, ['11']);
117 |
118 | assert.deepEqual(invert[2].buffer2.offset, 10);
119 | assert.deepEqual(invert[2].buffer2.length, 0);
120 | assert.deepEqual(invert[2].buffer2.chunk, []);
121 | assert.deepEqual(invert[2].buffer1.offset, 7);
122 | assert.deepEqual(invert[2].buffer1.length, 2);
123 | assert.deepEqual(invert[2].buffer1.chunk, ['z', 'z']);
124 | });
125 |
126 | await t.test('applies an inverted patch against buffer2 to get buffer1', t => {
127 | const result = Diff3.patch(b, invert);
128 | assert.deepEqual(result, a);
129 | });
130 |
131 | await t.test('did not modify the original patch', t => {
132 | assert.notEqual(patch, invert);
133 | assert.notEqual(patch[0], invert[0]);
134 | assert.notEqual(patch[1], invert[1]);
135 | assert.notEqual(patch[2], invert[2]);
136 | });
137 |
138 | });
--------------------------------------------------------------------------------
/test/merge.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('merge', async t => {
6 |
7 | await t.test('returns conflict: false if no conflicts', t => {
8 | const o = ['AA'];
9 | const a = ['AA'];
10 | const b = ['AA'];
11 | const expected = ['AA'];
12 |
13 | const r = Diff3.merge(a, o, b);
14 | assert.equal(r.conflict, false);
15 | assert.deepEqual(r.result, expected);
16 | });
17 |
18 |
19 | await t.test('returns a diff3-style merge result', t => {
20 | const o = ['AA', 'ZZ', '00', 'M', '99'];
21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
23 | const expected = [
24 | 'AA',
25 | '<<<<<<<',
26 | 'a',
27 | 'b',
28 | 'c',
29 | '=======',
30 | 'a',
31 | 'd',
32 | 'c',
33 | '>>>>>>>',
34 | 'ZZ',
35 | '<<<<<<<',
36 | 'new',
37 | '00',
38 | 'a',
39 | 'a',
40 | '=======',
41 | '11',
42 | '>>>>>>>',
43 | 'M',
44 | 'z',
45 | 'z',
46 | '99'
47 | ];
48 |
49 | const r = Diff3.merge(a, o, b);
50 | assert.equal(r.conflict, true);
51 | assert.deepEqual(r.result, expected);
52 | });
53 |
54 |
55 | await t.test('yaml comparison - issue #46', t => {
56 | const o = `title: "title"
57 | description: "description"`;
58 | const a = `title: "title"
59 | description: "description changed"`;
60 | const b = `title: "title changed"
61 | description: "description"`;
62 | const expected = [
63 | '<<<<<<<',
64 | 'title: "title"',
65 | 'description: "description changed"',
66 | '=======',
67 | 'title: "title changed"',
68 | 'description: "description"',
69 | '>>>>>>>'
70 | ];
71 |
72 | const r = Diff3.merge(a, o, b, { stringSeparator: /[\r\n]+/ });
73 | assert.equal(r.conflict, true);
74 | assert.deepEqual(r.result, expected);
75 | });
76 |
77 | });
78 |
--------------------------------------------------------------------------------
/test/mergeDiff3.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('mergeDiff3', async t => {
6 |
7 | await t.test('returns conflict: false if no conflicts', t => {
8 | const o = ['AA'];
9 | const a = ['AA'];
10 | const b = ['AA'];
11 | const expected = ['AA'];
12 |
13 | const r = Diff3.mergeDiff3(a, o, b);
14 | assert.equal(r.conflict, false);
15 | assert.deepEqual(r.result, expected);
16 | });
17 |
18 |
19 | await t.test('performs merge diff3 on arrays', t => {
20 | const o = ['AA', 'ZZ', '00', 'M', '99'];
21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
23 | const expected = [
24 | 'AA',
25 | '<<<<<<< a',
26 | 'a',
27 | 'b',
28 | 'c',
29 | '||||||| o',
30 | '=======',
31 | 'a',
32 | 'd',
33 | 'c',
34 | '>>>>>>> b',
35 | 'ZZ',
36 | '<<<<<<< a',
37 | 'new',
38 | '00',
39 | 'a',
40 | 'a',
41 | '||||||| o',
42 | '00',
43 | '=======',
44 | '11',
45 | '>>>>>>> b',
46 | 'M',
47 | 'z',
48 | 'z',
49 | '99'
50 | ];
51 |
52 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' } });
53 | assert.equal(r.conflict, true);
54 | assert.deepEqual(r.result, expected);
55 | });
56 |
57 |
58 | t.test('yaml comparison - issue #46', t => {
59 | const o = `title: "title"
60 | description: "description"`;
61 | const a = `title: "title"
62 | description: "description changed"`;
63 | const b = `title: "title changed"
64 | description: "description"`;
65 | const expected = [
66 | '<<<<<<< a',
67 | 'title: "title"',
68 | 'description: "description changed"',
69 | '||||||| o',
70 | 'title: "title"',
71 | 'description: "description"',
72 | '=======',
73 | 'title: "title changed"',
74 | 'description: "description"',
75 | '>>>>>>> b'
76 | ];
77 |
78 | const r = Diff3.mergeDiff3(a, o, b, { label: { a: 'a', o: 'o', b: 'b' }, stringSeparator: /[\r\n]+/ });
79 | assert.equal(r.conflict, true);
80 | assert.deepEqual(r.result, expected);
81 | });
82 |
83 | });
84 |
--------------------------------------------------------------------------------
/test/mergeDigIn.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import * as Diff3 from '../index.mjs';
4 |
5 | test('mergeDigIn', async t => {
6 |
7 | await t.test('returns conflict: false if no conflicts', t => {
8 | const o = ['AA'];
9 | const a = ['AA'];
10 | const b = ['AA'];
11 | const expected = ['AA'];
12 |
13 | const r = Diff3.mergeDigIn(a, o, b);
14 | assert.equal(r.conflict, false);
15 | assert.deepEqual(r.result, expected);
16 | });
17 |
18 |
19 | await t.test('returns a digin-style merge result', t => {
20 | const o = ['AA', 'ZZ', '00', 'M', '99'];
21 | const a = ['AA', 'a', 'b', 'c', 'ZZ', 'new', '00', 'a', 'a', 'M', '99'];
22 | const b = ['AA', 'a', 'd', 'c', 'ZZ', '11', 'M', 'z', 'z', '99'];
23 | const expected = [
24 | 'AA',
25 | 'a',
26 | '<<<<<<<',
27 | 'b',
28 | '=======',
29 | 'd',
30 | '>>>>>>>',
31 | 'c',
32 | 'ZZ',
33 | '<<<<<<<',
34 | 'new',
35 | '00',
36 | 'a',
37 | 'a',
38 | '=======',
39 | '11',
40 | '>>>>>>>',
41 | 'M',
42 | 'z',
43 | 'z',
44 | '99'
45 | ];
46 |
47 | const r = Diff3.mergeDigIn(a, o, b);
48 | assert.equal(r.conflict, true);
49 | assert.deepEqual(r.result, expected);
50 | });
51 |
52 | });
53 |
--------------------------------------------------------------------------------