diff --git a/release-notes.md b/release-notes.md index bb10cdaa..7b949fb9 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ * the README now correctly documents the tokenization behaviour (it was wrong before) - [#581](https://github.com/kpdecker/jsdiff/pull/581) - **fixed some regex operations used for tokenization in `diffWords` taking O(n^2) time** in pathological cases - [#595](https://github.com/kpdecker/jsdiff/pull/595) - **fixed a crash in patch creation functions when handling a single hunk consisting of a very large number (e.g. >130k) of lines**. (This was caused by spreading indefinitely-large arrays to `.push()` using `.apply` or the spread operator and hitting the JS-implementation-specific limit on the maximum number of arguments to a function, as shown at https://stackoverflow.com/a/56809779/1709587; thus the exact threshold to hit the error will depend on the environment in which you were running JsDiff.) +- [#596](https://github.com/kpdecker/jsdiff/pull/596) - **removed the `merge` function**. Previously JsDiff included an undocumented function called `merge` that was meant to, in some sense, merge patches. It had at least a couple of serious bugs that could lead to it returning unambiguously wrong results, and it was difficult to simply "fix" because it was [unclear precisely what it was meant to do](https://github.com/kpdecker/jsdiff/issues/181#issuecomment-2198319542). For now, the fix is to remove it entirely. ## 7.0.0 diff --git a/src/index.js b/src/index.js index 44e43ace..aa679ce6 100644 --- a/src/index.js +++ b/src/index.js @@ -27,7 +27,6 @@ import {diffArrays} from './diff/array'; import {applyPatch, applyPatches} from './patch/apply'; import {parsePatch} from './patch/parse'; -import {merge} from './patch/merge'; import {reversePatch} from './patch/reverse'; import {structuredPatch, createTwoFilesPatch, createPatch, formatPatch} from './patch/create'; @@ -56,7 +55,6 @@ export { applyPatch, applyPatches, parsePatch, - merge, reversePatch, convertChangesToDMP, convertChangesToXML, diff --git a/src/patch/merge.js b/src/patch/merge.js deleted file mode 100644 index ea35d3c1..00000000 --- a/src/patch/merge.js +++ /dev/null @@ -1,378 +0,0 @@ -import {structuredPatch} from './create'; -import {parsePatch} from './parse'; - -import {arrayEqual, arrayStartsWith} from '../util/array'; - -export function calcLineCount(hunk) { - const {oldLines, newLines} = calcOldNewLineCount(hunk.lines); - - if (oldLines !== undefined) { - hunk.oldLines = oldLines; - } else { - delete hunk.oldLines; - } - - if (newLines !== undefined) { - hunk.newLines = newLines; - } else { - delete hunk.newLines; - } -} - -export function merge(mine, theirs, base) { - mine = loadPatch(mine, base); - theirs = loadPatch(theirs, base); - - let ret = {}; - - // For index we just let it pass through as it doesn't have any necessary meaning. - // Leaving sanity checks on this to the API consumer that may know more about the - // meaning in their own context. - if (mine.index || theirs.index) { - ret.index = mine.index || theirs.index; - } - - if (mine.newFileName || theirs.newFileName) { - if (!fileNameChanged(mine)) { - // No header or no change in ours, use theirs (and ours if theirs does not exist) - ret.oldFileName = theirs.oldFileName || mine.oldFileName; - ret.newFileName = theirs.newFileName || mine.newFileName; - ret.oldHeader = theirs.oldHeader || mine.oldHeader; - ret.newHeader = theirs.newHeader || mine.newHeader; - } else if (!fileNameChanged(theirs)) { - // No header or no change in theirs, use ours - ret.oldFileName = mine.oldFileName; - ret.newFileName = mine.newFileName; - ret.oldHeader = mine.oldHeader; - ret.newHeader = mine.newHeader; - } else { - // Both changed... figure it out - ret.oldFileName = selectField(ret, mine.oldFileName, theirs.oldFileName); - ret.newFileName = selectField(ret, mine.newFileName, theirs.newFileName); - ret.oldHeader = selectField(ret, mine.oldHeader, theirs.oldHeader); - ret.newHeader = selectField(ret, mine.newHeader, theirs.newHeader); - } - } - - ret.hunks = []; - - let mineIndex = 0, - theirsIndex = 0, - mineOffset = 0, - theirsOffset = 0; - - while (mineIndex < mine.hunks.length || theirsIndex < theirs.hunks.length) { - let mineCurrent = mine.hunks[mineIndex] || {oldStart: Infinity}, - theirsCurrent = theirs.hunks[theirsIndex] || {oldStart: Infinity}; - - if (hunkBefore(mineCurrent, theirsCurrent)) { - // This patch does not overlap with any of the others, yay. - ret.hunks.push(cloneHunk(mineCurrent, mineOffset)); - mineIndex++; - theirsOffset += mineCurrent.newLines - mineCurrent.oldLines; - } else if (hunkBefore(theirsCurrent, mineCurrent)) { - // This patch does not overlap with any of the others, yay. - ret.hunks.push(cloneHunk(theirsCurrent, theirsOffset)); - theirsIndex++; - mineOffset += theirsCurrent.newLines - theirsCurrent.oldLines; - } else { - // Overlap, merge as best we can - let mergedHunk = { - oldStart: Math.min(mineCurrent.oldStart, theirsCurrent.oldStart), - oldLines: 0, - newStart: Math.min(mineCurrent.newStart + mineOffset, theirsCurrent.oldStart + theirsOffset), - newLines: 0, - lines: [] - }; - mergeLines(mergedHunk, mineCurrent.oldStart, mineCurrent.lines, theirsCurrent.oldStart, theirsCurrent.lines); - theirsIndex++; - mineIndex++; - - ret.hunks.push(mergedHunk); - } - } - - return ret; -} - -function loadPatch(param, base) { - if (typeof param === 'string') { - if ((/^@@/m).test(param) || ((/^Index:/m).test(param))) { - return parsePatch(param)[0]; - } - - if (!base) { - throw new Error('Must provide a base reference or pass in a patch'); - } - return structuredPatch(undefined, undefined, base, param); - } - - return param; -} - -function fileNameChanged(patch) { - return patch.newFileName && patch.newFileName !== patch.oldFileName; -} - -function selectField(index, mine, theirs) { - if (mine === theirs) { - return mine; - } else { - index.conflict = true; - return {mine, theirs}; - } -} - -function hunkBefore(test, check) { - return test.oldStart < check.oldStart - && (test.oldStart + test.oldLines) < check.oldStart; -} - -function cloneHunk(hunk, offset) { - return { - oldStart: hunk.oldStart, oldLines: hunk.oldLines, - newStart: hunk.newStart + offset, newLines: hunk.newLines, - lines: hunk.lines - }; -} - -// TODO: Fix use of push(... ) pattern here which probably trigger an error for really big changes -// due to the maximum argument limit -function mergeLines(hunk, mineOffset, mineLines, theirOffset, theirLines) { - // This will generally result in a conflicted hunk, but there are cases where the context - // is the only overlap where we can successfully merge the content here. - let mine = {offset: mineOffset, lines: mineLines, index: 0}, - their = {offset: theirOffset, lines: theirLines, index: 0}; - - // Handle any leading content - insertLeading(hunk, mine, their); - insertLeading(hunk, their, mine); - - // Now in the overlap content. Scan through and select the best changes from each. - while (mine.index < mine.lines.length && their.index < their.lines.length) { - let mineCurrent = mine.lines[mine.index], - theirCurrent = their.lines[their.index]; - - if ((mineCurrent[0] === '-' || mineCurrent[0] === '+') - && (theirCurrent[0] === '-' || theirCurrent[0] === '+')) { - // Both modified ... - mutualChange(hunk, mine, their); - } else if (mineCurrent[0] === '+' && theirCurrent[0] === ' ') { - // Mine inserted - hunk.lines.push(... collectChange(mine)); - } else if (theirCurrent[0] === '+' && mineCurrent[0] === ' ') { - // Theirs inserted - hunk.lines.push(... collectChange(their)); - } else if (mineCurrent[0] === '-' && theirCurrent[0] === ' ') { - // Mine removed or edited - removal(hunk, mine, their); - } else if (theirCurrent[0] === '-' && mineCurrent[0] === ' ') { - // Their removed or edited - removal(hunk, their, mine, true); - } else if (mineCurrent === theirCurrent) { - // Context identity - hunk.lines.push(mineCurrent); - mine.index++; - their.index++; - } else { - // Context mismatch - conflict(hunk, collectChange(mine), collectChange(their)); - } - } - - // Now push anything that may be remaining - insertTrailing(hunk, mine); - insertTrailing(hunk, their); - - calcLineCount(hunk); -} - -function mutualChange(hunk, mine, their) { - let myChanges = collectChange(mine), - theirChanges = collectChange(their); - - if (allRemoves(myChanges) && allRemoves(theirChanges)) { - // Special case for remove changes that are supersets of one another - if (arrayStartsWith(myChanges, theirChanges) - && skipRemoveSuperset(their, myChanges, myChanges.length - theirChanges.length)) { - hunk.lines.push(... myChanges); - return; - } else if (arrayStartsWith(theirChanges, myChanges) - && skipRemoveSuperset(mine, theirChanges, theirChanges.length - myChanges.length)) { - hunk.lines.push(... theirChanges); - return; - } - } else if (arrayEqual(myChanges, theirChanges)) { - hunk.lines.push(... myChanges); - return; - } - - conflict(hunk, myChanges, theirChanges); -} - -function removal(hunk, mine, their, swap) { - let myChanges = collectChange(mine), - theirChanges = collectContext(their, myChanges); - if (theirChanges.merged) { - hunk.lines.push(... theirChanges.merged); - } else { - conflict(hunk, swap ? theirChanges : myChanges, swap ? myChanges : theirChanges); - } -} - -function conflict(hunk, mine, their) { - hunk.conflict = true; - hunk.lines.push({ - conflict: true, - mine: mine, - theirs: their - }); -} - -function insertLeading(hunk, insert, their) { - while (insert.offset < their.offset && insert.index < insert.lines.length) { - let line = insert.lines[insert.index++]; - hunk.lines.push(line); - insert.offset++; - } -} -function insertTrailing(hunk, insert) { - while (insert.index < insert.lines.length) { - let line = insert.lines[insert.index++]; - hunk.lines.push(line); - } -} - -function collectChange(state) { - let ret = [], - operation = state.lines[state.index][0]; - while (state.index < state.lines.length) { - let line = state.lines[state.index]; - - // Group additions that are immediately after subtractions and treat them as one "atomic" modify change. - if (operation === '-' && line[0] === '+') { - operation = '+'; - } - - if (operation === line[0]) { - ret.push(line); - state.index++; - } else { - break; - } - } - - return ret; -} -function collectContext(state, matchChanges) { - let changes = [], - merged = [], - matchIndex = 0, - contextChanges = false, - conflicted = false; - while (matchIndex < matchChanges.length - && state.index < state.lines.length) { - let change = state.lines[state.index], - match = matchChanges[matchIndex]; - - // Once we've hit our add, then we are done - if (match[0] === '+') { - break; - } - - contextChanges = contextChanges || change[0] !== ' '; - - merged.push(match); - matchIndex++; - - // Consume any additions in the other block as a conflict to attempt - // to pull in the remaining context after this - if (change[0] === '+') { - conflicted = true; - - while (change[0] === '+') { - changes.push(change); - change = state.lines[++state.index]; - } - } - - if (match.substr(1) === change.substr(1)) { - changes.push(change); - state.index++; - } else { - conflicted = true; - } - } - - if ((matchChanges[matchIndex] || '')[0] === '+' - && contextChanges) { - conflicted = true; - } - - if (conflicted) { - return changes; - } - - while (matchIndex < matchChanges.length) { - merged.push(matchChanges[matchIndex++]); - } - - return { - merged, - changes - }; -} - -function allRemoves(changes) { - return changes.reduce(function(prev, change) { - return prev && change[0] === '-'; - }, true); -} -function skipRemoveSuperset(state, removeChanges, delta) { - for (let i = 0; i < delta; i++) { - let changeContent = removeChanges[removeChanges.length - delta + i].substr(1); - if (state.lines[state.index + i] !== ' ' + changeContent) { - return false; - } - } - - state.index += delta; - return true; -} - -function calcOldNewLineCount(lines) { - let oldLines = 0; - let newLines = 0; - - lines.forEach(function(line) { - if (typeof line !== 'string') { - let myCount = calcOldNewLineCount(line.mine); - let theirCount = calcOldNewLineCount(line.theirs); - - if (oldLines !== undefined) { - if (myCount.oldLines === theirCount.oldLines) { - oldLines += myCount.oldLines; - } else { - oldLines = undefined; - } - } - - if (newLines !== undefined) { - if (myCount.newLines === theirCount.newLines) { - newLines += myCount.newLines; - } else { - newLines = undefined; - } - } - } else { - if (newLines !== undefined && (line[0] === '+' || line[0] === ' ')) { - newLines++; - } - if (oldLines !== undefined && (line[0] === '-' || line[0] === ' ')) { - oldLines++; - } - } - }); - - return {oldLines, newLines}; -} diff --git a/test/patch/merge.js b/test/patch/merge.js deleted file mode 100644 index 2e642274..00000000 --- a/test/patch/merge.js +++ /dev/null @@ -1,1328 +0,0 @@ -import {merge} from '../../lib/patch/merge'; -import {parsePatch} from '../../lib/patch/parse'; - -import {expect} from 'chai'; - -describe('patch/merge', function() { - describe('#merge', function() { - it('should update line numbers for no conflicts', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -25,3 +25,4 @@\n' - + ' foo2\n' - + ' foo3\n' - + '+foo4\n' - + ' foo5\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 4, - lines: [ - ' line2', - ' line3', - '+line4', - ' line5' - ] - }, - { - oldStart: 25, oldLines: 3, - newStart: 26, newLines: 4, - lines: [ - ' foo2', - ' foo3', - '+foo4', - ' foo5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should remove identical hunks', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 4, - lines: [ - ' line2', - ' line3', - '+line4', - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - describe('hunk merge', function() { - it('should merge adjacent additions', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,6 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4-1\n' - + '+line4-2\n' - + '+line4-3\n' - + ' line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -2,2 +2,3 @@\n' - + ' line3\n' - + ' line5\n' - + '+line4-4\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 7, - lines: [ - ' line2', - ' line3', - '+line4-1', - '+line4-2', - '+line4-3', - ' line5', - '+line4-4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should merge leading additions', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,2 +1,4 @@\n' - + '+line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -3,1 +3,2 @@\n' - + ' line5\n' - + '+line4\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 2, - newStart: 1, newLines: 5, - lines: [ - '+line2', - ' line3', - '+line4', - ' line5', - '+line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should merge adjacent removals', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,2 @@\n' - + '-line2\n' - + '-line3\n' - + '+line4\n' - + ' line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -2,2 +2,3 @@\n' - + ' line3\n' - + ' line5\n' - + '+line4\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 3, - lines: [ - '-line2', - '-line3', - '+line4', - ' line5', - '+line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should merge adjacent additions with context removal', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,5 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4-1\n' - + '+line4-2\n' - + '+line4-3\n' - + '-line5\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -2,2 +2,3 @@\n' - + ' line3\n' - + ' line5\n' - + '+line4-4\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 6, - lines: [ - ' line2', - ' line3', - '+line4-1', - '+line4-2', - '+line4-3', - '-line5', - '+line4-4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should merge removal supersets', function() { - const mine = - '@@ -1,5 +1,3 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + '-line4\n' - + ' line5\n'; - const theirs = - '@@ -1,5 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + ' line4\n' - + ' line5\n'; - - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 5, - newStart: 1, newLines: 3, - lines: [ - ' line2', - ' line3', - '-line4', - '-line4', - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should conflict removal disjoint sets', function() { - const mine = - '@@ -1,6 +1,3 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + '-line4\n' - + '-line4\n' - + ' line5\n'; - const theirs = - '@@ -1,6 +1,3 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + '-line4\n' - + '-line5\n' - + ' line5\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 6, - newStart: 1, - newLines: 3, - lines: [ - ' line2', - ' line3', - { - conflict: true, - mine: [ - '-line4', - '-line4', - '-line4' - ], - theirs: [ - '-line4', - '-line4', - '-line5' - ] - }, - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should conflict removal disjoint context', function() { - const mine = - '@@ -1,6 +1,3 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + '-line4\n' - + '-line4\n' - + ' line5\n'; - const theirs = - '@@ -1,6 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '-line4\n' - + '-line4\n' - + ' line5\n' - + ' line5\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - newStart: 1, - newLines: 4, - lines: [ - ' line2', - ' line3', - { - conflict: true, - mine: [ - '-line4', - '-line4', - '-line4' - ], - theirs: [ - '-line4', - '-line4' - ] - }, - ' line5', - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - // These are all conflicts. A conflict is anything that is on the same desired line that is not identical - it('should conflict two additions at the same line', function() { - const mine = - '@@ -1,3 +1,6 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4-1\n' - + '+line4-2\n' - + '+line4-3\n' - + ' line5\n'; - const theirs = - '@@ -2 +2,2 @@\n' - + ' line3\n' - + '+line4-4\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 3, - newStart: 1, - lines: [ - ' line2', - ' line3', - { - conflict: true, - mine: [ - '+line4-1', - '+line4-2', - '+line4-3' - ], - theirs: [ - '+line4-4' - ] - }, - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should conflict addition supersets', function() { - const mine = - '@@ -1,3 +1,5 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + '+line4\n' - + ' line5\n'; - const theirs = - '@@ -1,3 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 3, - newStart: 1, - lines: [ - ' line2', - ' line3', - { - conflict: true, - mine: [ - '+line4', - '+line4' - ], - theirs: [ - '+line4' - ] - }, - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle removal and edit (add+remove) at the same line', function() { - const mine = - '@@ -1,2 +1 @@\n' - + ' line2\n' - + '-line3\n'; - const theirs = - '@@ -2 +2 @@\n' - + '-line3\n' - + '+line4\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 2, - newStart: 1, - lines: [ - ' line2', - { - conflict: true, - mine: [ - '-line3' - ], - theirs: [ - '-line3', - '+line4' - ] - } - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) on multiple lines', function() { - const mine = - '@@ -1,4 +1,3 @@\n' - + '-line2\n' - + ' line3\n' - + ' line3\n' - + ' line5\n'; - const theirs = - '@@ -2,2 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 4, - newStart: 1, newLines: 3, - lines: [ - '-line2', - '-line3', - '-line3', - '+line4', - '+line4', - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) past extents', function() { - const mine = - '@@ -1,3 +1,2 @@\n' - + '-line2\n' - + ' line3\n' - + ' line3\n'; - const theirs = - '@@ -2,3 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '-line5\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 4, - newStart: 1, newLines: 2, - lines: [ - '-line2', - '-line3', - '-line3', - '-line5', - '+line4', - '+line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) past extents', function() { - const mine = - '@@ -1,3 +1,2 @@\n' - + '-line2\n' - + ' line3\n' - + ' line3\n'; - const theirs = - '@@ -2,3 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '-line5\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 4, - newStart: 1, newLines: 2, - lines: [ - '-line2', - '-line3', - '-line3', - '-line5', - '+line4', - '+line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) context mismatch', function() { - const mine = - '@@ -1,3 +1,2 @@\n' - + '-line2\n' - + ' line3\n' - + ' line4\n'; - const theirs = - '@@ -2,3 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '-line5\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - newStart: 1, - lines: [ - '-line2', - { - conflict: true, - mine: [ - ' line3' - ], - theirs: [ - '-line3', - '-line3', - '-line5', - '+line4', - '+line4' - ] - }, - ' line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) addition', function() { - const mine = - '@@ -1,3 +1,3 @@\n' - + '-line2\n' - + ' line3\n' - + '+line6\n' - + ' line3\n'; - const theirs = - '@@ -2,3 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '-line5\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - newStart: 1, - lines: [ - '-line2', - { - conflict: true, - mine: [ - ' line3', - '+line6', - ' line3' - ], - theirs: [ - '-line3', - '-line3', - '-line5', - '+line4', - '+line4' - ] - } - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit (add+remove) on multiple lines with context', function() { - const mine = - '@@ -1,4 +1,3 @@\n' - + ' line2\n' - + '-line3\n' - + ' line3\n' - + ' line5\n'; - const theirs = - '@@ -2,2 +2,2 @@\n' - + '-line3\n' - + '-line3\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - newStart: 1, - lines: [ - ' line2', - { - conflict: true, - mine: [ - '-line3' - ], - theirs: [ - '-line3', - '-line3', - '+line4', - '+line4' - ] - }, - ' line3', // TODO: Fix - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should conflict edit with remove in middle', function() { - const mine = - '@@ -1,4 +1,2 @@\n' - + '-line2\n' - + ' line3\n' - + '-line3\n' - + ' line5\n'; - const theirs = - '@@ -1,3 +1,3 @@\n' - + ' line2\n' - + '-line3\n' - + '-line3\n' - + '+line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 4, - newStart: 1, - lines: [ - '-line2', - { - conflict: true, - mine: [ - ' line3', - '-line3' - ], - theirs: [ - '-line3', - '-line3', - '+line4', - '+line4' - ] - }, - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle edit and addition with context connextion', function() { - const mine = - '@@ -1,3 +1 @@\n' - + ' line2\n' - + '-line3\n' - + '-line4\n'; - const theirs = - '@@ -2,2 +2,3 @@\n' - + ' line3\n' - + ' line4\n' - + '+line4\n'; - - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 2, - lines: [ - ' line2', - '-line3', - '-line4', - '+line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should merge removals that start in the leading section', function() { - const mine = - '@@ -1,2 +0,0 @@\n' - + '-line2\n' - + '-line3\n'; - const theirs = - '@@ -2,2 +2 @@\n' - + '-line3\n' - + ' line4\n'; - const expected = { - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 1, - lines: [ - '-line2', - '-line3', - ' line4' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should conflict edits that start in the leading section', function() { - const mine = - '@@ -1,5 +1 @@\n' - + '-line2\n' - + '-line3\n' - + '-line3\n' - + '-line3\n' - + '-line3\n' - + '+line4\n'; - const theirs = - '@@ -2,5 +2,3 @@\n' - + ' line3\n' - + ' line3\n' - + '-line3\n' - + '-line3\n' - + ' line5\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 6, - newStart: 1, - lines: [ - '-line2', - { - conflict: true, - mine: [ - '-line3', - '-line3', - '-line3', - '-line3', - '+line4' - ], - theirs: [ - ' line3', - ' line3', - '-line3', - '-line3' - ] - }, - ' line5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should handle multiple conflicts in one hunk', function() { - const mine = - '@@ -1,7 +1,7 @@\n' - + ' line1\n' - + '-line2\n' - + '+line2-1\n' - + ' line3\n' - + ' line4\n' - + ' line5\n' - + '-line6\n' - + '+line6-1\n' - + ' line7\n'; - const theirs = - '@@ -1,7 +1,7 @@\n' - + ' line1\n' - + '-line2\n' - + '+line2-2\n' - + ' line3\n' - + ' line4\n' - + ' line5\n' - + '-line6\n' - + '+line6-2\n' - + ' line7\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 7, - newStart: 1, - newLines: 7, - lines: [ - ' line1', - { - conflict: true, - mine: [ - '-line2', - '+line2-1' - ], - theirs: [ - '-line2', - '+line2-2' - ] - }, - ' line3', - ' line4', - ' line5', - { - conflict: true, - mine: [ - '-line6', - '+line6-1' - ], - theirs: [ - '-line6', - '+line6-2' - ] - }, - ' line7' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should remove oldLines if base differs', function() { - const mine = - '@@ -1,8 +1,7 @@\n' - + ' line1\n' - + '-line2\n' - + '-line2-0\n' - + '+line2-1\n' - + ' line3\n' - + ' line4\n' - + ' line5\n' - + '-line6\n' - + '+line6-1\n' - + ' line7\n'; - const theirs = - '@@ -1,7 +1,8 @@\n' - + ' line1\n' - + '-line2\n' - + '+line2-2\n' - + '+line2-3\n' - + ' line3\n' - + ' line4\n' - + ' line5\n' - + '-line6\n' - + '+line6-2\n' - + ' line7\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - newStart: 1, - lines: [ - ' line1', - { - conflict: true, - mine: [ - '-line2', - '-line2-0', - '+line2-1' - ], - theirs: [ - '-line2', - '+line2-2', - '+line2-3' - ] - }, - ' line3', - ' line4', - ' line5', - { - conflict: true, - mine: [ - '-line6', - '+line6-1' - ], - theirs: [ - '-line6', - '+line6-2' - ] - }, - ' line7' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - - it('should handle multiple conflict sections', function() { - const mine = - '@@ -1,2 +1,2 @@\n' - + ' line2\n' - + ' line3\n'; - const theirs = - '@@ -1,2 +1,2 @@\n' - + ' line3\n' - + ' line4\n'; - const expected = { - hunks: [ - { - conflict: true, - oldStart: 1, - oldLines: 2, - newStart: 1, - newLines: 2, - lines: [ - { - conflict: true, - mine: [ - ' line2', - ' line3' - ], - theirs: [ - ' line3', - ' line4' - ] - } - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - - swapConflicts(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - }); - - it('should handle file name updates', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test2\theader2\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n'; - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test2', - newHeader: 'header2', - hunks: [] - }; - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - }); - it('should handle file name conflicts', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test-a\theader-a\n' - + '+++ test2\theader2\n'; - const theirs = - 'Index: test\n' - + '===================================================================\n' - + '--- test-b\theader-b\n' - + '+++ test3\theader3\n'; - const partialMatch = - 'Index: test\n' - + '===================================================================\n' - + '--- test-b\theader-a\n' - + '+++ test3\theader3\n'; - - expect(merge(mine, theirs)).to.eql({ - conflict: true, - index: 'test', - oldFileName: { - mine: 'test-a', - theirs: 'test-b' - }, - oldHeader: { - mine: 'header-a', - theirs: 'header-b' - }, - newFileName: { - mine: 'test2', - theirs: 'test3' - }, - newHeader: { - mine: 'header2', - theirs: 'header3' - }, - hunks: [] - }); - expect(merge(mine, partialMatch)).to.eql({ - conflict: true, - index: 'test', - oldFileName: { - mine: 'test-a', - theirs: 'test-b' - }, - oldHeader: 'header-a', - newFileName: { - mine: 'test2', - theirs: 'test3' - }, - newHeader: { - mine: 'header2', - theirs: 'header3' - }, - hunks: [] - }); - }); - it('should select available headers', function() { - const mine = - 'Index: test\n' - + '===================================================================\n' - + '--- test\theader1\n' - + '+++ test\theader2\n' - + '@@ -1,3 +1,4 @@\n' - + ' line2\n' - + ' line3\n' - + '+line4\n' - + ' line5\n'; - const theirs = - '@@ -25,3 +25,4 @@\n' - + ' foo2\n' - + ' foo3\n' - + '+foo4\n' - + ' foo5\n'; - - const expected = { - index: 'test', - oldFileName: 'test', - oldHeader: 'header1', - newFileName: 'test', - newHeader: 'header2', - hunks: [ - { - oldStart: 1, oldLines: 3, - newStart: 1, newLines: 4, - lines: [ - ' line2', - ' line3', - '+line4', - ' line5' - ] - }, - { - oldStart: 25, oldLines: 3, - newStart: 26, newLines: 4, - lines: [ - ' foo2', - ' foo3', - '+foo4', - ' foo5' - ] - } - ] - }; - - expect(merge(mine, theirs)).to.eql(expected); - expect(merge(theirs, mine)).to.eql(expected); - expect(merge(mine, parsePatch(theirs)[0])).to.eql(expected); - expect(merge(theirs, parsePatch(mine)[0])).to.eql(expected); - }); - - it('should diff from base', function() { - expect(merge('foo\nbar\nbaz\n', 'foo\nbaz\nbat\n', 'foo\nbaz\n')).to.eql({ - hunks: [ - { - oldStart: 1, oldLines: 2, - newStart: 1, newLines: 4, - lines: [ - ' foo', - '+bar', - ' baz', - '+bat' - ] - } - ] - }); - }); - it('should error if not passed base', function() { - expect(function() { - merge('foo', 'foo'); - }).to['throw']('Must provide a base reference or pass in a patch'); - }); - }); -}); - -function swapConflicts(expected) { - expected.hunks.forEach(function(hunk) { - hunk.lines.forEach(function(line) { - if (line.conflict) { - let tmp = line.mine; - line.mine = line.theirs; - line.theirs = tmp; - } - }); - }); -}