Three-way merge
A three-way merge combines two versions of a document, and , that were edited independently from a common ancestor , the base. Each version is compared with the base to find out which regions it changed. A region that only one side changed is taken from that side; a region both sides changed in the same way is taken once; and a region both sides changed differently is a conflict, which the merge reports for a person to resolve[1][5]. The base is what makes the merge possible. Given only and , a line present in one and absent from the other could have been added by one side or deleted by the other, and no rule can tell which. The line-based algorithm diff3 has been part of Unix since 1979[7], and version control systems from RCS to Git merge files with it[4][6].
The merge rule
Suppose the three versions have been aligned into regions, so that , and are corresponding regions of the base and the two sides. The merged region is
The rule has the properties one expects: merging a side with an unchanged copy of the base returns the side, ; merging a version with itself returns it, ; and the result does not depend on which side is called , apart from the order in which a conflict lists the two sides. All the difficulty lies in the alignment.
The diff3 algorithm
diff3 aligns the three versions using two two-way diffs. It computes a longest common subsequence of and , and one of and , each a monotone matching of lines[2][3]. A line of the base that is matched in both diffs is a synchronization point: all three versions agree on it, and it is copied to the output. Between two consecutive synchronization points lies an unstable region, which may be empty in some versions, and the merge rule is applied to it[1].
diff3 on arrays of lines, with a quadratic longest-common-subsequence diff and output in Git's diff3 conflict style.
(* Longest common subsequence of two line arrays, as the list of matchedindex pairs in increasing order. *)let lcs (x : string array) (y : string array) =let n = Array.length x and m = Array.length y inlet t = Array.make_matrix (n + 1) (m + 1) 0 infor i = n - 1 downto 0 dofor j = m - 1 downto 0 dot.(i).(j) <- if x.(i) = y.(j) then t.(i+1).(j+1) + 1else max t.(i+1).(j) t.(i).(j+1)donedone;let rec walk i j =if i = n || j = m then []else if x.(i) = y.(j) then (i, j) :: walk (i + 1) (j + 1)else if t.(i+1).(j) >= t.(i).(j+1) then walk (i + 1) jelse walk i (j + 1)inwalk 0 0type chunk =| Stable of string list| Changed of string list (* merged cleanly *)| Conflict of string list * string list * string list (* a, base, b *)let slice a i j = Array.to_list (Array.sub a i (j - i))(* diff3: a base line matched in both a and b is a sync point. Between twoconsecutive sync points lies an unstable region, which is taken fromwhichever side changed it, or is a conflict if both did, differently. *)let merge (o : string array) (a : string array) (b : string array) =let ma = Hashtbl.of_seq (List.to_seq (lcs o a))and mb = Hashtbl.of_seq (List.to_seq (lcs o b)) inlet syncs =List.filter_map(fun i -> match Hashtbl.find_opt ma i, Hashtbl.find_opt mb i with| Some j, Some k -> Some (i, j, k) | _ -> None)(List.init (Array.length o) Fun.id)@ [ (Array.length o, Array.length a, Array.length b) ] (* sentinel *)inlet rec go (i0, j0, k0) = function| [] -> []| (i, j, k) :: rest ->let so = slice o i0 i and sa = slice a j0 j and sb = slice b k0 k inlet region =if so = sa && so = sb then []else if sa = so then [ Changed sb ]else if sb = so || sa = sb then [ Changed sa ]else [ Conflict (sa, so, sb) ]inlet line = if i < Array.length o then [ Stable [ o.(i) ] ] else [] inregion @ line @ go (i + 1, j + 1, k + 1) restingo (0, 0, 0) syncs(* Print the result the way Git does with merge.conflictStyle = diff3. *)let print chunks =List.iter(function| Stable l | Changed l -> List.iter print_endline l| Conflict (a, o, b) ->print_endline "<<<<<<< ours"; List.iter print_endline a;print_endline "||||||| base"; List.iter print_endline o;print_endline "======="; List.iter print_endline b;print_endline ">>>>>>> theirs")chunks
Two independent edits to different parts of a file.
open Diff3let lines s = Array.of_list (String.split_on_char '\n' s)let base = lines "let area r =\n let pi = 3.14 in\n pi *. r *. r\n\nlet perimeter r =\n 2. *. 3.14 *. r"let ours = lines "let area r =\n let pi = Float.pi in\n pi *. r *. r\n\nlet perimeter r =\n 2. *. 3.14 *. r"let theirs = lines "let area r =\n let pi = 3.14 in\n pi *. r *. r\n\nlet perimeter r =\n 2. *. 3.14 *. r\n\nlet diameter r = 2. *. r"
The merge takes both.
let area r =let pi = Float.pi inpi *. r *. rlet perimeter r =2. *. 3.14 *. rlet diameter r = 2. *. r
Conflicting, identical and adjacent edits.
open Diff3let lines s = Array.of_list (String.split_on_char '\n' s)let show title o a b = print_endline ("-- " ^ title); print (merge (lines o) (lines a) (lines b))
Running it.
-- both sides change the same linex = 1<<<<<<< oursy = 20||||||| basey = 2=======y = 200>>>>>>> theirsz = 3-- both make the same changex = 1y = 20z = 3-- changes to adjacent linesw = 0<<<<<<< oursx = 10y = 2||||||| basex = 1y = 2=======x = 1y = 20>>>>>>> theirsz = 3-- the same changes, one line apartw = 0x = 10k = 5y = 20z = 3
On these inputs the output is the same as GNU diff3 -m and git merge-file --diff3. The third case shows a property that surprises people: changes to adjacent lines conflict even though they do not overlap, because no base line lies between them to synchronize on, so the two changes fall into one unstable region. With one unchanged line between them, the same two edits merge cleanly. The section between the ||||||| and ======= markers shows the base version of the conflicting region, which Git prints when merge.conflictStyle is set to diff3 or zdiff3[12].
Properties
Khanna, Kunal and Pierce gave the first formal account of diff3 in 2007[1]. They showed that it is idempotent and that it is stable when the two sides edit regions of the base separated by enough unchanged lines, so that the merged result then contains both sides' changes. They also showed that in general it has fewer guarantees than one might hope. The result depends on which longest common subsequences the two-way diffs choose, and with repeated lines, such as closing braces or blank lines, a different but equally long choice can move a change into a different place or turn a clean merge into a conflict. diff3 is not a function of the edits the two sides made but of the three texts, and different edit histories that produce the same texts give the same merge.
Clean is not correct
A line-based merge knows nothing about the language of the file. Two changes can touch different lines and merge without conflict while being incompatible, as when one side renames a function and the other adds a new call to it under its old name.
One side renames area, the other adds a use of it.
open Diff3let lines s = Array.of_list (String.split_on_char '\n' s)(* Ours renames area to disc_area; theirs adds a new use of area. *)let base = lines "let area r = Float.pi *. r *. r\nlet total = area 1. +. area 2.\n\nlet () = print_float total"let ours = lines "let disc_area r = Float.pi *. r *. r\nlet total = disc_area 1. +. disc_area 2.\n\nlet () = print_float total"let theirs = lines "let area r = Float.pi *. r *. r\nlet total = area 1. +. area 2.\n\nlet () = print_float total\nlet ring = area 3. -. area 2."
The merge is clean.
let disc_area r = Float.pi *. r *. rlet total = disc_area 1. +. disc_area 2.let () = print_float totallet ring = area 3. -. area 2.
And the result does not compile.
File "merged.ml", line 5, characters 11-15:5 | let ring = area 3. -. area 2.^^^^Error: Unbound value area
Such semantic conflicts are found only by building and testing the merged result. Merge tools that parse the files first, called structured or semistructured merge, can resolve some textual conflicts, such as two independent additions to a list of declarations, and report others more precisely[5][11].
Choosing the base
In a version control system, the base is a common ancestor of the two commits being merged. Git uses a best common ancestor, one that is not an ancestor of any other common ancestor[6]. In a linear branch-and-merge history there is exactly one. In a criss-cross history, where each of two branches has merged the other, there can be several, and picking one arbitrarily can reintroduce conflicts that were already resolved or silently drop changes. Git's recursive strategy, and its successor ort, which became Git's default in 2021, first merge the common ancestors into a virtual base and then use it for the final three-way merge; according to Git's documentation, tests on the Linux kernel's history found that this gives fewer conflicts without causing mismerges[6].
Merges are also applied to trees of files, not only to lines. A file added on one side, or deleted on one side and unchanged on the other, follows the same rule as a line, and renames are detected by comparing file contents on each side with the base.
Alternatives
Operation-based systems record the edits themselves rather than comparing states. The patch theory of Darcs and Pijul treats a merge as combining two patches with a common origin, which Mimram and Di Giusto described as a pushout in a category of files and patches[9]. Conflict-free replicated data types design the data type so that concurrent operations always commute, so that every merge succeeds by construction[10]. Both give a merge with stronger guarantees than diff3 at the cost of a richer representation than plain text.
History
Hunt and McIlroy's diff appeared in 1976[2], and diff3, which compares three files and can produce a merged script, was part of Version 7 Unix in 1979[7]. Tichy's RCS provided a merge command built on diff3 in 1985[4], and CVS, begun by Dick Grune in 1986 and rewritten by Berliner, made concurrent editing followed by a three-way merge the normal way to work on shared code[8]. Myers's algorithm made the underlying diffs fast[3]. Mens surveyed textual, syntactic and semantic merging in 2002[5], and Khanna, Kunal and Pierce gave diff3 its first formal specification in 2007[1]. Git, released in 2005, uses the same line-level algorithm inside merge strategies that choose the base and handle whole trees[6].
see also
- Merkle treeA Merkle tree, or hash tree, is a binary tree in which every leaf holds the cryptographic hash of a data block and every interior node holds the hash of its two children, so that the root hash commits to the whole sequence. Any block can be shown to belong to the sequence with the logarithmically many sibling hashes on its path to the root, and two copies can be compared by descending only into subtrees whose hashes differ. Merkle trees underlie Git, Bitcoin, Certificate Transparency, replicated databases and hash-based signatures.
- RopeA rope is a binary tree whose leaves are strings and whose internal nodes represent the concatenation of their children, with each node caching the length of its left subtree. Concatenation creates one node instead of copying, and indexing, splitting, insertion and deletion in the middle take time logarithmic in the length for a balanced rope. Ropes are immutable and share structure between versions, which makes them suited to text editors with undo and to programs that build long strings piece by piece.
- ZipperA zipper represents a data structure together with a focus, a position inside it: the substructure at the focus, and the path from the focus back to the root together with everything to either side of that path. Moving the focus one step and editing at the focus take constant time, and the structure is persistent, so edits share everything off the path. The type of paths is the derivative of the structure's type, in the sense of calculus.
referenced by
further reading
- [1]S. Khanna, K. Kunal, B. C. Pierce, “A formal investigation of diff3”, FSTTCS, Lecture Notes in Computer Science 4855 (2007).
- [2]J. W. Hunt, M. D. McIlroy, “An algorithm for differential file comparison”, Bell Laboratories Computing Science Technical Report 41 (1976).
- [3]E. W. Myers, “An O(ND) difference algorithm and its variations”, Algorithmica 1 (1986).
- [4]W. F. Tichy, “RCS — a system for version control”, Software: Practice and Experience 15 (1985).
- [5]T. Mens, “A state-of-the-art survey on software merging”, IEEE Transactions on Software Engineering 28 (2002).
- [6]Git documentation, “git-merge(1)”, section “Merge strategies”, and “git-merge-base(1)”.
- [7]Unix Programmer’s Manual, Seventh Edition, “diff3(1)”, Bell Laboratories (1979).
- [8]B. Berliner, “CVS II: parallelizing software development”, USENIX Winter Conference (1990).
- [9]S. Mimram, C. Di Giusto, “A categorical theory of patches”, Electronic Notes in Theoretical Computer Science 298 (2013).
- [10]M. Shapiro, N. Preguiça, C. Baquero, M. Zawirski, “Conflict-free replicated data types”, SSS, Lecture Notes in Computer Science 6976 (2011).
- [11]S. Apel, J. Liebig, B. Brandl, C. Lengauer, C. Kästner, “Semistructured merge: rethinking merge in revision control systems”, ESEC/FSE (2011).
- [12]Git documentation, “git-config(1)”, entry “merge.conflictStyle”.
last updated