Recursive AI

Three-way text merge in C#

OneDiff is a free, open-source diff library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

A three-way merge combines two edited versions using their shared original to identify independent changes.

Terminal
dotnet add package RecursiveAI.OneDiff

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using OneDiff;
 
var result = Diff.Merge(
    baseText: "red\nmiddle\nblue\n",
    ours: "RED\nmiddle\nblue\n",
    theirs: "red\nmiddle\nBLUE\n");
Console.WriteLine(
    $"Conflicts: {result.HasConflicts}");
Console.Write(result.Text);
Text
Conflicts: False
RED
middle
BLUE

One side changes the first line and the other changes the last. HasConflicts is false, so the merged text can be used without resolving competing edits. Conflicts are returned as marked text, not thrown as exceptions.

Unzip the example, then dotnet run.

Variations

Inspect a conflict.

C#
using OneDiff;
var result = Diff.Merge("30 days\n", "60 days\n", "90 days\n");
Console.WriteLine(
    $"Conflicts: {result.HasConflicts}");
Console.Write(result.Text);
Text
Conflicts: True
<<<<<<< ours
60 days
=======
90 days
>>>>>>> theirs

Resolve explicitly in favor of ours. This policy selects 60 days for the conflicting region.

C#
using OneDiff;
using OneDiff.Merge;
var result = Diff.Merge("30 days\n", "60 days\n", "90 days\n",
    new MergeSettings { Resolution = ConflictResolution.Ours });
Console.WriteLine(
    $"Conflicts: {result.HasConflicts}");
Console.Write(result.Text);
Text
Conflicts: False
60 days

The short answer

Use Diff.Merge(baseText, ours, theirs) from OneDiff. Pass the common ancestor first, not one of the edited copies. Read HasConflicts before saving Text as a finished document. With default settings, conflicting text contains markers and both alternatives.

The merge documentation describes a line-based operation: it combines independent line changes, keeps identical changes once, and marks competing changes. It does not parse C# syntax, infer author intent, or decide whether the result is a valid program.

How do you do a three-way text merge in C#?

Keep the original version when branching into two editing sessions. At reconciliation time, supply that exact original and the two descendants. Comparing just the descendants tells you where they differ but not which side changed a particular line. The base gives the merge that missing information.

In the example, the original has red, middle and blue on separate lines. Ours changes red to RED; theirs changes blue to BLUE. The unchanged middle line separates the edits, and the result includes both changes. All inputs are in memory; this operation does not read or modify a file.

For file-backed workflows, read all three versions with the same intended encoding. Only write a final output when the conflict policy permits it. Keep the original versions available until review is complete. A successful text merge says nothing about whether merged code compiles or whether combined settings still make sense.

How do you resolve three-way merge conflicts?

The first variation changes the same line to two different payment terms. With default settings, HasConflicts is true and Text contains <<<<<<< ours, ======= and >>>>>>> theirs. These are review markers, not part of a finished payment clause.

The second variation chooses ConflictResolution.Ours explicitly. This resolves conflicting regions in favor of ours; it is not a reason to silently prefer the current user in every application. The result then reports no unresolved conflicts. The region information still records where the versions disagreed, as described in the public API.

Theirs chooses the other side; Union writes ours followed by theirs in conflicting regions. Union is appropriate only when retaining both alternatives is meaningful. Two competing values for one setting generally need a decision, not concatenation. Neither an automatic policy nor a marker-free string is a semantic correctness check.

What is a three-way merge algorithm?

OneDiff aligns each descendant with the base, then walks the changed ranges. A change made on only one side is taken from that side. Matching changes on both sides are taken once. Incompatible changed ranges become conflicts. Touching ranges can conflict too, so changes do not have to replace the exact same character to need review.

The text form works on lines, including line endings: a and a\n differ, as do LF and CRLF endings. A lone carriage return is not a line break. Normalize endings before the merge only if that normalization is part of your application’s policy.

This differs from applying a unified patch, which applies one change description to a target. It also differs from combining structured reviewer changes: a text merge produces one merged string, whereas a reviewer combination preserves attributed changes and conflicts for review.

All onediff recipes