-
Notifications
You must be signed in to change notification settings - Fork 501
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #54
- Loading branch information
Showing
3 changed files
with
1,582 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,349 @@ | ||
import {structuredPatch} from './create'; | ||
import {parsePatch} from './parse'; | ||
|
||
import {arrayEqual, arrayStartsWith} from '../util/array'; | ||
|
||
export function calcLineCount(hunk) { | ||
let conflicted = false; | ||
|
||
hunk.oldLines = 0; | ||
hunk.newLines = 0; | ||
|
||
hunk.lines.forEach(function(line) { | ||
if (typeof line !== 'string') { | ||
conflicted = true; | ||
return; | ||
} | ||
|
||
if (line[0] === '+' || line[0] === ' ') { | ||
hunk.newLines++; | ||
} | ||
if (line[0] === '-' || line[0] === ' ') { | ||
hunk.oldLines++; | ||
} | ||
}); | ||
|
||
if (conflicted) { | ||
delete hunk.oldLines; | ||
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 | ||
}; | ||
} | ||
|
||
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
export function arrayEqual(a, b) { | ||
if (a.length !== b.length) { | ||
return false; | ||
} | ||
|
||
return arrayStartsWith(a, b); | ||
} | ||
|
||
export function arrayStartsWith(array, start) { | ||
if (start.length > array.length) { | ||
return false; | ||
} | ||
|
||
for (let i = 0; i < start.length; i++) { | ||
if (start[i] !== array[i]) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
Oops, something went wrong.