Skip to content

Commit

Permalink
Fixes issue #19: Add support for options so that the Patience optimiz…
Browse files Browse the repository at this point in the history
…ation can be disabled for merges
  • Loading branch information
lassevagsaether-karlsen committed Jul 27, 2017
1 parent c7b7dd6 commit f216281
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 30 deletions.
38 changes: 38 additions & 0 deletions DiffLib.Tests/MergeTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using JetBrains.Annotations;

using NUnit.Framework;

// ReSharper disable PossibleNullReferenceException
Expand Down Expand Up @@ -58,5 +62,39 @@ public void Perform_TestCases(string commonBase, string left, string right, stri
var output = new string(Merge.Perform(commonBase.ToCharArray(), left.ToCharArray(), right.ToCharArray(), new BasicReplaceInsertDeleteDiffElementAligner<char>(), new TakeLeftThenRightMergeConflictResolver<char>()).ToArray());
Assert.That(output, Is.EqualTo(expected));
}

[NotNull]
private List<string> StringToLines(string input)
{
var reader = new StringReader(input);
var result = new List<string>();

string line;
while ((line = reader.ReadLine()) != null)
result.Add(line);

return result;
}

private class AbortIfConflictResolver<T> : IMergeConflictResolver<T>
{
public IEnumerable<T> Resolve(IList<T> commonBase, IList<T> left, IList<T> right)
{
throw new NotSupportedException();
}
}

[Test]
public void Perform_DistinctAdditions_ShouldNotProduceAConflict()
{
var common = "{}".ToCharArray();
var left = "{a}".ToCharArray();
var right = "{} {b}".ToCharArray();
var expected = "{a} {b}".ToCharArray();

var result = Merge.Perform(common, left, right, new DiffOptions { EnablePatienceOptimization = false }, new BasicReplaceInsertDeleteDiffElementAligner<char>(), new AbortIfConflictResolver<char>()).ToList();

CollectionAssert.AreEqual(expected, result);
}
}
}
45 changes: 42 additions & 3 deletions DiffLib/Diff.Static.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace DiffLib
/// <summary>
/// Static API class for DiffLib.
/// </summary>
[PublicAPI]
public static class Diff
{
/// <summary>
Expand Down Expand Up @@ -35,17 +36,55 @@ public static class Diff
/// </exception>
[NotNull]
public static IEnumerable<DiffSection> CalculateSections<T>([NotNull] IList<T> collection1, [NotNull] IList<T> collection2, [CanBeNull] IEqualityComparer<T> comparer = null)
{
return CalculateSections(collection1, collection2, new DiffOptions(), comparer);
}

/// <summary>
/// Calculate sections of differences from the two collections using the specified comparer.
/// </summary>
/// <typeparam name="T">
/// The type of elements in the two collections.
/// </typeparam>
/// <param name="collection1">
/// The first collection.
/// </param>
/// <param name="collection2">
/// The second collection.
/// </param>
/// <param name="options">
/// A <see cref="DiffOptions"/> object specifying options to the diff algorithm, or <c>null</c> if defaults should be used.
/// </param>
/// <param name="comparer">
/// The <see cref="IEqualityComparer{T}"/> to use when determining if there is a match between
/// <paramref name="collection1"/> and <paramref name="collection2"/>.
/// </param>
/// <returns>
/// A collection of <see cref="DiffSection"/> values, containing the sections found.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="collection1"/> is <c>null</c>.</para>
/// <para>- or -</para>
/// <para><paramref name="collection2"/> is <c>null</c>.</para>
/// </exception>
[NotNull]
public static IEnumerable<DiffSection> CalculateSections<T>([NotNull] IList<T> collection1, [NotNull] IList<T> collection2, [CanBeNull] DiffOptions options, [CanBeNull] IEqualityComparer<T> comparer = null)
{
if (collection1 == null)
throw new ArgumentNullException(nameof(collection1));
if (collection2 == null)
throw new ArgumentNullException(nameof(collection2));

return LongestCommonSubsectionDiff.Calculate(collection1, collection2, comparer ?? EqualityComparer<T>.Default);
comparer = comparer ?? EqualityComparer<T>.Default;
Assume.That(comparer != null);

options = options ?? new DiffOptions();

return LongestCommonSubsectionDiff.Calculate(collection1, collection2, options, comparer);
}

/// <summary>
/// Align the sections found by <see cref="CalculateSections{T}"/> by trying to find out, within each section, which elements from one collection line up the best with
/// Align the sections found by <see cref="CalculateSections{T}(IList{T},IList{T},DiffOptions,IEqualityComparer{T})"/> by trying to find out, within each section, which elements from one collection line up the best with
/// elements from the other collection.
/// </summary>
/// <typeparam name="T">
Expand All @@ -58,7 +97,7 @@ public static IEnumerable<DiffSection> CalculateSections<T>([NotNull] IList<T> c
/// The second collection.
/// </param>
/// <param name="diffSections">
/// The section values found by <see cref="CalculateSections{T}"/>.
/// The section values found by <see cref="CalculateSections{T}(IList{T},IList{T},DiffOptions,IEqualityComparer{T})"/>.
/// </param>
/// <param name="aligner">
/// An alignment strategy, provided through the <see cref="IDiffElementAligner{T}"/> interface.
Expand Down
22 changes: 22 additions & 0 deletions DiffLib/DiffOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;

using JetBrains.Annotations;

namespace DiffLib
{
/// <summary>
/// This class is used to specify options to the diff algorithm.
/// </summary>
[PublicAPI]
public class DiffOptions
{
/// <summary>
/// Gets or sets a value indicating whether the patience optimization is enabled. Default is <c>true</c>.
/// </summary>
/// <remarks>
/// For more information about the patience optimization, see this question on Stack Overflow:
/// https://stackoverflow.com/questions/4045017/what-is-git-diff-patience-for
/// </remarks>
public bool EnablePatienceOptimization { get; set; } = true;
}
}
42 changes: 41 additions & 1 deletion DiffLib/ListExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,56 @@ public static class ListExtensions
/// similar that react to changes to the list.
/// </remarks>
public static void MutateToBeLike<T>([NotNull] this IList<T> target, [NotNull] IList<T> source, [CanBeNull] IEqualityComparer<T> comparer = null, [CanBeNull] IDiffElementAligner<T> aligner = null)
{
MutateToBeLike(target, source, new DiffOptions(), comparer, aligner);
}

/// <summary>
/// Mutate the specified list to have the same elements as another list, by inserting or removing as needed. The end result is that
/// <paramref name="target"/> will have equivalent elements as <paramref name="source"/>, in the same order and positions.
/// </summary>
/// <typeparam name="T">
/// The type of elements in the lists.
/// </typeparam>
/// <param name="target">
/// The list to mutate. Elements will possibly be inserted into or deleted from this list.
/// </param>
/// <param name="source">
/// The list to use as the source of mutations for <paramref name="target"/>.
/// </param>
/// <param name="options">
/// A <see cref="DiffOptions"/> object specifying options to the diff algorithm, or <c>null</c> if defaults should be used.
/// </param>
/// <param name="comparer">
/// The optional <see cref="IEqualityComparer{T}"/> to use when comparing elements.
/// If not specified/<c>null</c>, <see cref="EqualityComparer{T}.Default"/> will be used.
/// </param>
/// <param name="aligner">
/// The <see cref="IDiffElementAligner{T}"/> to use when aligning elements.
/// If not specified/<c>null</c>, <see cref="BasicReplaceInsertDeleteDiffElementAligner{T}"/> will be used.
/// </param>
/// <exception cref="ArgumentNullException">
/// <para><paramref name="target"/> is <c>null</c>.</para>
/// <para>- or -</para>
/// <para><paramref name="source"/> is <c>null</c>.</para>
/// </exception>
/// <remarks>
/// The main purpose of this method is to avoid clearing and refilling the list from scratch and instead
/// make adjustments to it to have the right elements. Useful in conjunction with UI bindings and
/// similar that react to changes to the list.
/// </remarks>
public static void MutateToBeLike<T>([NotNull] this IList<T> target, [NotNull] IList<T> source, [CanBeNull] DiffOptions options, [CanBeNull] IEqualityComparer<T> comparer = null, [CanBeNull] IDiffElementAligner<T> aligner = null)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (source == null)
throw new ArgumentNullException(nameof(source));

options = options ?? new DiffOptions();
comparer = comparer ?? EqualityComparer<T>.Default;
aligner = aligner ?? new BasicReplaceInsertDeleteDiffElementAligner<T>();

var sections = Diff.CalculateSections(target, source, comparer);
var sections = Diff.CalculateSections(target, source, options, comparer);
var items = Diff.AlignElements(target, source, sections, aligner).ToList();

Assume.That(items != null);
Expand Down
42 changes: 22 additions & 20 deletions DiffLib/LongestCommonSubsectionDiff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,39 @@ namespace DiffLib
{
internal static class LongestCommonSubsectionDiff
{
[NotNull]
public static IEnumerable<DiffSection> Calculate<T>([NotNull] IList<T> collection1, [NotNull] IList<T> collection2, [CanBeNull] IEqualityComparer<T> comparer)
[NotNull, PublicAPI]
public static IEnumerable<DiffSection> Calculate<T>([NotNull] IList<T> collection1, [NotNull] IList<T> collection2, [NotNull] DiffOptions options, [NotNull] IEqualityComparer<T> comparer)
{
comparer = comparer ?? EqualityComparer<T>.Default;
Assume.That(comparer != null);

return Calculate(collection1, 0, collection1.Count, collection2, 0, collection2.Count, comparer, new LongestCommonSubsequence<T>(collection1, collection2, comparer));
return Calculate(collection1, 0, collection1.Count, collection2, 0, collection2.Count, comparer, new LongestCommonSubsequence<T>(collection1, collection2, comparer), options);
}

[NotNull]
private static IEnumerable<DiffSection> Calculate<T>([NotNull] IList<T> collection1, int lower1, int upper1, [NotNull] IList<T> collection2, int lower2, int upper2, [NotNull] IEqualityComparer<T> comparer, [NotNull] LongestCommonSubsequence<T> lcs)
private static IEnumerable<DiffSection> Calculate<T>([NotNull] IList<T> collection1, int lower1, int upper1, [NotNull] IList<T> collection2, int lower2, int upper2, [NotNull] IEqualityComparer<T> comparer, [NotNull] LongestCommonSubsequence<T> lcs, [NotNull] DiffOptions options)
{
// Short-circuit recursive call when nothing left (usually because match was found at the very start or end of a subsection
if (lower1 == upper1 && lower2 == upper2)
yield break;

// Patience modification, let's find matching elements at both ends and remove those from LCS consideration
int matchStart = MatchStart(collection1, lower1, upper1, collection2, lower2, upper2, comparer);
int matchEnd = 0;

if (matchStart > 0)
if (options.EnablePatienceOptimization)
{
yield return new DiffSection(isMatch: true, lengthInCollection1: matchStart, lengthInCollection2: matchStart);
lower1 += matchStart;
lower2 += matchStart;
}
int matchStart = MatchStart(collection1, lower1, upper1, collection2, lower2, upper2, comparer);
if (matchStart > 0)
{
yield return new DiffSection(isMatch: true, lengthInCollection1: matchStart, lengthInCollection2: matchStart);

int matchEnd = MatchEnd(collection1, lower1, upper1, collection2, lower2, upper2, comparer);
if (matchEnd > 0)
{
upper1 -= matchEnd;
upper2 -= matchEnd;
lower1 += matchStart;
lower2 += matchStart;
}

matchEnd = MatchEnd(collection1, lower1, upper1, collection2, lower2, upper2, comparer);
if (matchEnd > 0)
{
upper1 -= matchEnd;
upper2 -= matchEnd;
}
}

if (lower1 < upper1 || lower2 < upper2)
Expand All @@ -54,14 +56,14 @@ private static IEnumerable<DiffSection> Calculate<T>([NotNull] IList<T> collecti
if (lcs.Find(lower1, upper1, lower2, upper2, out position1, out position2, out length))
{
// Recursively apply calculation to portion before common subsequence
foreach (var section in Calculate(collection1, lower1, position1, collection2, lower2, position2, comparer, lcs))
foreach (var section in Calculate(collection1, lower1, position1, collection2, lower2, position2, comparer, lcs, options))
yield return section;

// Output match
yield return new DiffSection(isMatch: true, lengthInCollection1: length, lengthInCollection2: length);

// Recursively apply calculation to portion after common subsequence
foreach (var section in Calculate(collection1, position1 + length, upper1, collection2, position2 + length, upper2, comparer, lcs))
foreach (var section in Calculate(collection1, position1 + length, upper1, collection2, position2 + length, upper2, comparer, lcs, options))
yield return section;
}
else
Expand Down
45 changes: 44 additions & 1 deletion DiffLib/Merge.Static.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace DiffLib
/// <summary>
/// Static API class for the merge portion of DiffLib.
/// </summary>
[PublicAPI]
public static class Merge
{
/// <summary>
Expand Down Expand Up @@ -44,6 +45,47 @@ public static class Merge
/// </exception>
[NotNull, ItemCanBeNull]
public static IEnumerable<T> Perform<T>([NotNull] IList<T> commonBase, [NotNull] IList<T> left, [NotNull] IList<T> right, [NotNull] IDiffElementAligner<T> aligner, [NotNull] IMergeConflictResolver<T> conflictResolver, [CanBeNull] IEqualityComparer<T> comparer = null)
{
return Perform(commonBase, left, right, new DiffOptions(), aligner, conflictResolver, comparer);
}

/// <summary>
/// Performs a merge using a 3-way merge, returning the final merged output.
/// </summary>
/// <typeparam name="T">
/// The type of elements in the collections being merged.
/// </typeparam>
/// <param name="commonBase">
/// The common base/ancestor of both <paramref name="left"/> and <paramref name="right"/>.
/// </param>
/// <param name="left">
/// The left side being merged with the <paramref name="right"/>.
/// </param>
/// <param name="right">
/// The right side being merged with the <paramref name="left"/>.
/// </param>
/// <param name="diffOptions">
/// A <see cref="DiffOptions"/> object specifying options to the diff algorithm, or <c>null</c> if defaults should be used.
/// </param>
/// <param name="aligner">
/// A <see cref="IDiffElementAligner{T}"/> implementation that will be responsible for lining up common vs. left and common vs. right as well as left vs. right
/// during the merge.
/// </param>
/// <param name="conflictResolver">
/// A <see cref="IMergeConflictResolver{T}"/> implementation that will be used to resolve conflicting modifications between left and right.
/// </param>
/// <param name="comparer">
/// A <see cref="IEqualityComparer{T}"/> implementation that will be used to compare elements of all the collections. If <c>null</c> is specified then
/// <see cref="EqualityComparer{T}.Default"/> will be used.
/// </param>
/// <returns>
/// The final merged collection of elements from <paramref name="left"/> and <paramref name="right"/>.
/// </returns>
/// <exception cref="MergeConflictException">
/// The <paramref name="conflictResolver"/> threw a <see cref="MergeConflictException"/> to indicate a failure to resolve a conflict.
/// </exception>
[NotNull, ItemCanBeNull]
public static IEnumerable<T> Perform<T>([NotNull] IList<T> commonBase, [NotNull] IList<T> left, [NotNull] IList<T> right, [CanBeNull] DiffOptions diffOptions, [NotNull] IDiffElementAligner<T> aligner, [NotNull] IMergeConflictResolver<T> conflictResolver, [CanBeNull] IEqualityComparer<T> comparer = null)
{
if (commonBase == null)
throw new ArgumentNullException(nameof(commonBase));
Expand All @@ -56,10 +98,11 @@ public static IEnumerable<T> Perform<T>([NotNull] IList<T> commonBase, [NotNull]
if (conflictResolver == null)
throw new ArgumentNullException(nameof(conflictResolver));

diffOptions = diffOptions ?? new DiffOptions();
comparer = comparer ?? EqualityComparer<T>.Default;
Assume.That(comparer != null);

return new Merge<T>(commonBase, left, right, aligner, conflictResolver, comparer);
return new Merge<T>(commonBase, left, right, aligner, conflictResolver, comparer, diffOptions);

}
}
Expand Down
Loading

0 comments on commit f216281

Please sign in to comment.