/// <summary> /// Perform a three-way merge of documents. /// </summary> /// <param name="original">Original document.</param> /// <param name="left">Left hand modification of document.</param> /// <param name="right">Right hand modification of document.</param> /// <param name="maxsize">Maximum size of the difference response.</param> /// <param name="priority">Merge priority.</param> /// <param name="conflict">Output of conflict flag.</param> /// <returns>Merged document.</returns> public static XDoc Merge(XDoc original, XDoc left, XDoc right, int maxsize, ArrayMergeDiffPriority priority, out bool conflict) { if (original == null) { throw new ArgumentNullException("original"); } if (left == null) { throw new ArgumentNullException("left"); } if (right == null) { throw new ArgumentNullException("right"); } return(Merge(Tokenize(original), Tokenize(left), Tokenize(right), maxsize, priority, out conflict)); }
/// <summary> /// Merge two diffs. /// </summary> /// <typeparam name="T">Type of value items in the provided diffs.</typeparam> /// <param name="left">Left hand diff.</param> /// <param name="right">Right hand diff.</param> /// <param name="priority">Priority of resolving ambigious <see cref="ArrayDiffKind"/> values.</param> /// <param name="equal">Equality delegate for value comparison.</param> /// <param name="track">Tracking function for correlating related diff items, in case on of the related items is ambigious.</param> /// <param name="hasConflicts">Indicator whether any conflicts were found.</param> /// <returns>Diff result of merge.</returns> public static Tuple <ArrayDiffKind, T>[] MergeDiff <T>(Tuple <ArrayDiffKind, T>[] left, Tuple <ArrayDiffKind, T>[] right, ArrayMergeDiffPriority priority, Equality <T> equal, Func <T, object> track, out bool hasConflicts) where T : class { if (left == null) { throw new ArgumentNullException("left"); } if (right == null) { throw new ArgumentNullException("right"); } // set a default equality test if none is provided if (equal == null) { equal = delegate(T lhs, T rhs) { return(((lhs == null) && (rhs == null)) || (((lhs != null) && (rhs != null)) && (lhs == rhs))); }; } // loop over all entries and mark them as conflicted when appropriate var result = new List <Tuple <ArrayDiffKind, T> >(left.Length + right.Length); int leftIndex = 0; int rightIndex = 0; bool ambiguous = false; hasConflicts = false; while (true) { if ((leftIndex < left.Length) && (rightIndex < right.Length)) { if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: both sides agree to keep the current element result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Same, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; ambiguous = false; } else if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: right-side removed element; this could be conflict result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.RemovedRight, right[rightIndex].Item2)); ++leftIndex; ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } else { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, right[rightIndex].Item2)); } ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: left-side removed element; this could be conflict result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.RemovedLeft, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: both sides removed the element; subsequent operations are ambiguous result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Removed, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side is attempting to add an item after an item that is deleted on the left-side result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); ++rightIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: left-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); } else { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } ++leftIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: left-side is attempting to add an item after an item that is deleted on the right-side result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); ++leftIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: both sides are adding element; order is ambgiguous, unless it's the same item if (equal(left[leftIndex].Item2, right[rightIndex].Item2)) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } else { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } ++leftIndex; ++rightIndex; ambiguous = true; } else { throw new InvalidOperationException(); } } else if (leftIndex < left.Length) { if (left[leftIndex].Item1 == ArrayDiffKind.Same) { throw new InvalidOperationException(); } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed) { throw new InvalidOperationException(); } else if (left[leftIndex].Item1 == ArrayDiffKind.Added) { // NOTE: left-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); } else { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } } ++leftIndex; } else if (rightIndex < right.Length) { if (right[rightIndex].Item1 == ArrayDiffKind.Same) { throw new InvalidOperationException(); } else if (right[rightIndex].Item1 == ArrayDiffKind.Removed) { throw new InvalidOperationException(); } else if (right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } else { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, right[rightIndex].Item2)); } } ++rightIndex; } else { break; } } // convert 'RemovedLeft' and 'RemovedRight' if they aren't followed by 'AddedLeft' or 'AddedRight' ambiguous = false; result.Reverse(); var modifiedResult = new List <Tuple <ArrayDiffKind, T> >(result.Count); foreach (var item in result) { switch (item.Item1) { case ArrayDiffKind.Same: case ArrayDiffKind.Removed: case ArrayDiffKind.Added: ambiguous = false; modifiedResult.Add(item); break; case ArrayDiffKind.AddedLeft: case ArrayDiffKind.AddedRight: ambiguous = true; hasConflicts = true; modifiedResult.Add(item); break; case ArrayDiffKind.RemovedLeft: case ArrayDiffKind.RemovedRight: if (!ambiguous) { modifiedResult.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Removed, item.Item2)); } else { hasConflicts = true; modifiedResult.Add(item); } break; } } modifiedResult.Reverse(); result = modifiedResult; // check if there is a merge priority to remove ambiguous changes if (priority != ArrayMergeDiffPriority.None) { // copy only accepted changes List <Tuple <ArrayDiffKind, T> > combined = result; result = new List <Tuple <ArrayDiffKind, T> >(combined.Count); for (int i = 0; i < combined.Count; ++i) { switch (combined[i].Item1) { case ArrayDiffKind.Same: case ArrayDiffKind.Removed: case ArrayDiffKind.Added: result.Add(combined[i]); break; case ArrayDiffKind.AddedLeft: if (priority == ArrayMergeDiffPriority.Left) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, combined[i].Item2)); } break; case ArrayDiffKind.RemovedLeft: result.Add(new Tuple <ArrayDiffKind, T>(priority == ArrayMergeDiffPriority.Left ? ArrayDiffKind.Removed : ArrayDiffKind.Same, combined[i].Item2)); break; case ArrayDiffKind.AddedRight: if (priority == ArrayMergeDiffPriority.Right) { result.Add(new Tuple <ArrayDiffKind, T>(ArrayDiffKind.Added, combined[i].Item2)); } break; case ArrayDiffKind.RemovedRight: result.Add(new Tuple <ArrayDiffKind, T>(priority == ArrayMergeDiffPriority.Right ? ArrayDiffKind.Removed : ArrayDiffKind.Same, combined[i].Item2)); break; } } } return(result.ToArray()); }
/// <summary> /// Perform a three-way merge between token sets resulting in a document. /// </summary> /// <param name="original">Original Token set.</param> /// <param name="left">Left hand token set.</param> /// <param name="right">Right hand token set.</param> /// <param name="maxsize">Maximum size of the difference response.</param> /// <param name="priority">Merge priority.</param> /// <param name="conflict">Output of conflict flag.</param> /// <returns>Merged document.</returns> public static XDoc Merge(Token[] original, Token[] left, Token[] right, int maxsize, ArrayMergeDiffPriority priority, out bool conflict) { if(original == null) { throw new ArgumentNullException("original"); } if(left == null) { throw new ArgumentNullException("left"); } if(right == null) { throw new ArgumentNullException("right"); } // create left diff Tuplet<ArrayDiffKind, Token>[] leftDiff = Diff(original, left, maxsize); if(leftDiff == null) { conflict = false; return null; } // create right diff Tuplet<ArrayDiffKind, Token>[] rightDiff = Diff(original, right, maxsize); if(rightDiff == null) { conflict = false; return null; } // merge changes Tuplet<ArrayDiffKind, Token>[] mergeDiff = ArrayUtil.MergeDiff(leftDiff, rightDiff, priority, Token.Equal, x => x.Key, out conflict); return Detokenize(mergeDiff); }
/// <summary> /// Perform a three-way merge of documents. /// </summary> /// <param name="original">Original document.</param> /// <param name="left">Left hand modification of document.</param> /// <param name="right">Right hand modification of document.</param> /// <param name="maxsize">Maximum size of the difference response.</param> /// <param name="priority">Merge priority.</param> /// <param name="conflict">Output of conflict flag.</param> /// <returns>Merged document.</returns> public static XDoc Merge(XDoc original, XDoc left, XDoc right, int maxsize, ArrayMergeDiffPriority priority, out bool conflict) { if(original == null) { throw new ArgumentNullException("original"); } if(left == null) { throw new ArgumentNullException("left"); } if(right == null) { throw new ArgumentNullException("right"); } return Merge(Tokenize(original), Tokenize(left), Tokenize(right), maxsize, priority, out conflict); }
/// <summary> /// Merge two diffs. /// </summary> /// <typeparam name="T">Type of value items in the provided diffs.</typeparam> /// <param name="left">Left hand diff.</param> /// <param name="right">Right hand diff.</param> /// <param name="priority">Priority of resolving ambigious <see cref="ArrayDiffKind"/> values.</param> /// <param name="equal">Equality delegate for value comparison.</param> /// <param name="track">Tracking function for correlating related diff items, in case on of the related items is ambigious.</param> /// <param name="hasConflicts">Indicator whether any conflicts were found.</param> /// <returns>Diff result of merge.</returns> public static Tuplet <ArrayDiffKind, T>[] MergeDiff <T>(Tuplet <ArrayDiffKind, T>[] left, Tuplet <ArrayDiffKind, T>[] right, ArrayMergeDiffPriority priority, Equality <T> equal, Func <T, object> track, out bool hasConflicts) where T : class { if (left == null) { throw new ArgumentNullException("left"); } if (right == null) { throw new ArgumentNullException("right"); } // set a default equality test if none is provided if (equal == null) { equal = delegate(T lhs, T rhs) { return(((lhs == null) && (rhs == null)) || (((lhs != null) && (rhs != null)) && (lhs == rhs))); }; } // loop over all entries and mark them as conflicted when appropriate List <Tuplet <ArrayDiffKind, T> > result = new List <Tuplet <ArrayDiffKind, T> >(left.Length + right.Length); int leftIndex = 0; int rightIndex = 0; bool ambiguous = false; hasConflicts = false; while (true) { if ((leftIndex < left.Length) && (rightIndex < right.Length)) { if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: both sides agree to keep the current element result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Same, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; ambiguous = false; } else if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: right-side removed element; this could be conflict result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.RemovedRight, right[rightIndex].Item2)); ++leftIndex; ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Same && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } else { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Added, right[rightIndex].Item2)); } ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: left-side removed element; this could be conflict result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.RemovedLeft, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: both sides removed the element; subsequent operations are ambiguous result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Removed, left[leftIndex].Item2)); ++leftIndex; ++rightIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side is attempting to add an item after an item that is deleted on the left-side result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); ++rightIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Same) { // NOTE: left-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); } else { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } ++leftIndex; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Removed) { // NOTE: left-side is attempting to add an item after an item that is deleted on the right-side result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); ++leftIndex; ambiguous = true; } else if (left[leftIndex].Item1 == ArrayDiffKind.Added && right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: both sides are adding element; order is ambgiguous, unless it's the same item if (equal(left[leftIndex].Item2, right[rightIndex].Item2)) { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } else { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } ++leftIndex; ++rightIndex; ambiguous = true; } else { throw new InvalidOperationException(); } } else if (leftIndex < left.Length) { if (left[leftIndex].Item1 == ArrayDiffKind.Same) { throw new InvalidOperationException(); } else if (left[leftIndex].Item1 == ArrayDiffKind.Removed) { throw new InvalidOperationException(); } else if (left[leftIndex].Item1 == ArrayDiffKind.Added) { // NOTE: left-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedLeft, left[leftIndex].Item2)); } else { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Added, left[leftIndex].Item2)); } } ++leftIndex; } else if (rightIndex < right.Length) { if (right[rightIndex].Item1 == ArrayDiffKind.Same) { throw new InvalidOperationException(); } else if (right[rightIndex].Item1 == ArrayDiffKind.Removed) { throw new InvalidOperationException(); } else if (right[rightIndex].Item1 == ArrayDiffKind.Added) { // NOTE: right-side added an element; conflict depends on ambiguity of current position if (ambiguous) { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.AddedRight, right[rightIndex].Item2)); } else { result.Add(new Tuplet <ArrayDiffKind, T>(ArrayDiffKind.Added, right[rightIndex].Item2)); } } ++rightIndex; } else { break; } } // convert 'RemovedLeft' and 'RemovedRight' if they aren't followed by 'AddedLeft' or 'AddedRight' ambiguous = false; for (int i = result.Count - 1; i >= 0; --i) { switch (result[i].Item1) { case ArrayDiffKind.Same: case ArrayDiffKind.Removed: case ArrayDiffKind.Added: ambiguous = false; break; case ArrayDiffKind.AddedLeft: case ArrayDiffKind.AddedRight: ambiguous = true; hasConflicts = true; break; case ArrayDiffKind.RemovedLeft: case ArrayDiffKind.RemovedRight: if (!ambiguous) { result[i].Item1 = ArrayDiffKind.Removed; } else { hasConflicts = true; } break; } } // check if we need to track dependencies between changes if (track != null) { Dictionary <object, List <Tuplet <ArrayDiffKind, T> > > dependencies = new Dictionary <object, List <Tuplet <ArrayDiffKind, T> > >(); Dictionary <object, List <Tuplet <ArrayDiffKind, T> > > conflictsLeft = new Dictionary <object, List <Tuplet <ArrayDiffKind, T> > >(); Dictionary <object, List <Tuplet <ArrayDiffKind, T> > > conflictsRight = new Dictionary <object, List <Tuplet <ArrayDiffKind, T> > >(); // detect changes that are conflicting for (int i = 0; i < result.Count; ++i) { object key = track(result[i].Item2); if (key != null) { List <Tuplet <ArrayDiffKind, T> > entry; // add change to list if (!dependencies.TryGetValue(key, out entry)) { entry = new List <Tuplet <ArrayDiffKind, T> >(); dependencies.Add(key, entry); } entry.Add(result[i]); // if change represents a conflict, add it to the conflict list switch (result[i].Item1) { case ArrayDiffKind.AddedLeft: case ArrayDiffKind.RemovedLeft: conflictsLeft[key] = entry; break; case ArrayDiffKind.AddedRight: case ArrayDiffKind.RemovedRight: conflictsRight[key] = entry; break; } } } // visit all left conflicts and elevate any 'Added' or 'Removed' items to 'AddedLeft' or 'RemovedLeft' foreach (List <Tuplet <ArrayDiffKind, T> > entry in conflictsLeft.Values) { foreach (Tuplet <ArrayDiffKind, T> item in entry) { if (item.Item1 == ArrayDiffKind.Added) { item.Item1 = ArrayDiffKind.AddedLeft; } else if (item.Item1 == ArrayDiffKind.Removed) { item.Item1 = ArrayDiffKind.RemovedLeft; } } } // visit all right conflicts and elevate any 'Added' or 'Removed' items to 'AddedRight' or 'RemovedRight' foreach (List <Tuplet <ArrayDiffKind, T> > entry in conflictsRight.Values) { foreach (Tuplet <ArrayDiffKind, T> item in entry) { if (item.Item1 == ArrayDiffKind.Added) { item.Item1 = ArrayDiffKind.AddedRight; } else if (item.Item1 == ArrayDiffKind.Removed) { item.Item1 = ArrayDiffKind.RemovedRight; } } } } // check if there is a merge priority to remove ambiguous changes if (priority != ArrayMergeDiffPriority.None) { // copy only accepted changes List <Tuplet <ArrayDiffKind, T> > combined = result; result = new List <Tuplet <ArrayDiffKind, T> >(combined.Count); for (int i = 0; i < combined.Count; ++i) { switch (combined[i].Item1) { case ArrayDiffKind.Same: case ArrayDiffKind.Removed: case ArrayDiffKind.Added: result.Add(combined[i]); break; case ArrayDiffKind.AddedLeft: if (priority == ArrayMergeDiffPriority.Left) { combined[i].Item1 = ArrayDiffKind.Added; result.Add(combined[i]); } break; case ArrayDiffKind.RemovedLeft: if (priority == ArrayMergeDiffPriority.Left) { combined[i].Item1 = ArrayDiffKind.Removed; } else { combined[i].Item1 = ArrayDiffKind.Same; } result.Add(combined[i]); break; case ArrayDiffKind.AddedRight: if (priority == ArrayMergeDiffPriority.Right) { combined[i].Item1 = ArrayDiffKind.Added; result.Add(combined[i]); } break; case ArrayDiffKind.RemovedRight: if (priority == ArrayMergeDiffPriority.Right) { combined[i].Item1 = ArrayDiffKind.Removed; } else { combined[i].Item1 = ArrayDiffKind.Same; } result.Add(combined[i]); break; } } } return(result.ToArray()); }
/// <summary> /// Perform a three-way merge between token sets resulting in a document. /// </summary> /// <param name="original">Original Token set.</param> /// <param name="left">Left hand token set.</param> /// <param name="right">Right hand token set.</param> /// <param name="maxsize">Maximum size of the difference response.</param> /// <param name="priority">Merge priority.</param> /// <param name="conflict">Output of conflict flag.</param> /// <returns>Merged document.</returns> public static XDoc Merge(Token[] original, Token[] left, Token[] right, int maxsize, ArrayMergeDiffPriority priority, out bool conflict) { if (original == null) { throw new ArgumentNullException("original"); } if (left == null) { throw new ArgumentNullException("left"); } if (right == null) { throw new ArgumentNullException("right"); } // create left diff var leftDiff = Diff(original, left, maxsize); if (leftDiff == null) { conflict = false; return(null); } // create right diff var rightDiff = Diff(original, right, maxsize); if (rightDiff == null) { conflict = false; return(null); } // merge changes var mergeDiff = ArrayUtil.MergeDiff(leftDiff, rightDiff, priority, Token.Equal, x => x.Key, out conflict); return(Detokenize(mergeDiff)); }