Merging

edit

TextMerge.Merge merges two versions of a text, "ours" and "theirs", that were both changed from a common base, as diff3 and git merge do:

var baseText = "red\ngreen\nblue\n";
var oursText = "red\nGREEN\nblue\n";            // Changes a line
var theirsText = "red\ngreen\nblue\nyellow\n";   // Adds a line

var result = TextMerge.Merge(baseText, oursText, theirsText);

Assert.False(result.HasConflicts);
Assert.Equal("red\nGREEN\nblue\nyellow\n", result.Text);

Both versions are compared to the base. A region changed in one version only takes that change, and a region changed the same way in both takes it once. A region changed differently in both is a conflict.

Conflicts

TextMergeResult.Text writes each conflict between markers, as git does, and HasConflicts tells whether there is one. The Chunks give the text of each region in the three versions, to resolve the conflicts in code:

var baseText = "title: Draft\nbody\n";
var oursText = "title: Final\nbody\n";
var theirsText = "title: Review\nbody\n";

var result = TextMerge.Merge(baseText, oursText, theirsText, new TextMergeOptions
{
    ConflictStyle = TextMergeConflictStyle.Diff3,
    OursLabel = "main",
    TheirsLabel = "feature",
});

Assert.True(result.HasConflicts);
Assert.Equal(
    "<<<<<<< main\n" +
    "title: Final\n" +
    "||||||| base\n" +
    "title: Draft\n" +
    "=======\n" +
    "title: Review\n" +
    ">>>>>>> feature\n" +
    "body\n",
    result.Text);

// Each region gives the text of the three versions, to resolve the conflict in code
var conflict = result.Chunks.Single(chunk => chunk.Kind is TextMergeChunkKind.Conflict);
Assert.Equal("title: Draft\n", conflict.BaseText);
Assert.Equal("title: Final\n", conflict.OursText);
Assert.Equal("title: Review\n", conflict.TheirsText);
Assert.Null(conflict.Text);
  • ConflictStyle chooses the markers: Merge writes both versions, Diff3 also writes the base after a ||||||| marker, like merge.conflictStyle=diff3 in git.
  • OursLabel, BaseLabel and TheirsLabel are written after the markers. An empty label writes the marker alone.
  • A marker always starts a line: a line feed is added after a version whose last line has none.
  • Two insertions at the same place are a conflict, even when they do not overlap, since the merge cannot choose their order. So is a line deleted in one version and changed in the other.

The regions

Each TextMergeChunk has a Kind:

KindMerged Text
UnchangedThe text of ours
ChangedInOursThe text of ours
ChangedInTheirsThe text of theirs
ChangedInBothThe text of ours, the same change as theirs
Conflictnull

The BaseText, OursText and TheirsText of the regions concatenate back to the three texts.

Options

DiffOptions sets the options of the two diffs: the chunker, the algorithm, the comparison options and the work limits. Lines are the default; words merge two edits of the same line that touch different words.

With a comparison option such as IgnoreCase, a region whose differences are ignored is unchanged and takes the text of ours. With IgnoreEndOfLine, the three texts are normalized first, so the merged text has \n line terminators.