Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Lib/NativeBinaries/amd64/git2-96fb6a6.dll
Binary file not shown.
Binary file not shown.
Binary file removed Lib/NativeBinaries/amd64/git2-98eaf39.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
488 changes: 482 additions & 6 deletions LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions LibGit2Sharp/CompareOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace LibGit2Sharp
{
/// <summary>
Expand Down Expand Up @@ -25,5 +27,15 @@ public CompareOptions()
/// (Default = 0)
/// </summary>
public int InterhunkLines { get; set; }

/// <summary>
/// Options for rename detection. If null, the `diff.renames` configuration setting is used.
/// </summary>
public SimilarityOptions Similarity { get; set; }

/// <summary>
/// Include "unmodified" entries in the results.
/// </summary>
public bool IncludeUnmodified { get; set; }
}
}
58 changes: 58 additions & 0 deletions LibGit2Sharp/Core/GitDiff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ internal class GitDiffLine
public int NewLineNo;
public int NumLines;
public UIntPtr contentLen;
public Int64 contentOffset;
public IntPtr content;
}

Expand All @@ -274,4 +275,61 @@ enum GitDiffFormat
GIT_DIFF_FORMAT_NAME_ONLY = 4, // < like git diff --name-only
GIT_DIFF_FORMAT_NAME_STATUS = 5, // < like git diff --name-status
}

[Flags]
enum GitDiffFindFlags
{
GIT_DIFF_FIND_RENAMES = (1 << 0),
// consider old side of modified for renames? (`--break-rewrites=N`)
GIT_DIFF_FIND_RENAMES_FROM_REWRITES = (1 << 1),

// look for copies? (a la `--find-copies`)
GIT_DIFF_FIND_COPIES = (1 << 2),
// consider unmodified as copy sources? (`--find-copies-harder`)
GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED = (1 << 3),

// mark large rewrites for split (`--break-rewrites=/M`)
GIT_DIFF_FIND_REWRITES = (1 << 4),
// actually split large rewrites into delete/add pairs
GIT_DIFF_BREAK_REWRITES = (1 << 5),
// mark rewrites for split and break into delete/add pairs
GIT_DIFF_FIND_AND_BREAK_REWRITES =
(GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES),

// find renames/copies for untracked items in working directory
GIT_DIFF_FIND_FOR_UNTRACKED = (1 << 6),

// turn on all finding features
GIT_DIFF_FIND_ALL = (0x0ff),

// measure similarity ignoring leading whitespace (default)
GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE = 0,
// measure similarity ignoring all whitespace
GIT_DIFF_FIND_IGNORE_WHITESPACE = (1 << 12),
// measure similarity including all data
GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE = (1 << 13),
// measure similarity only by comparing SHAs (fast and cheap)
GIT_DIFF_FIND_EXACT_MATCH_ONLY = (1 << 14),

// do not break rewrites unless they contribute to a rename
GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY = (1 << 15),

// Remove any UNMODIFIED deltas after find_similar is done.
GIT_DIFF_FIND_REMOVE_UNMODIFIED = (1 << 16),
}

[StructLayout(LayoutKind.Sequential)]
internal class GitDiffFindOptions
{
public uint Version = 1;
public GitDiffFindFlags Flags;
public UInt16 RenameThreshold;
public UInt16 RenameFromRewriteThreshold;
public UInt16 CopyThreshold;
public UInt16 BreakRewriteThreshold;
public UIntPtr RenameLimit;

// TODO
public IntPtr SimilarityMetric;
}
}
2 changes: 1 addition & 1 deletion LibGit2Sharp/Core/NativeDllName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ namespace LibGit2Sharp.Core
{
internal static class NativeDllName
{
public const string Name = "git2-98eaf39";
public const string Name = "git2-96fb6a6";
}
}
5 changes: 5 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,11 @@ internal static extern int git_diff_foreach(
git_diff_line_cb lineCallback,
IntPtr payload);

[DllImport(libgit2)]
internal static extern int git_diff_find_similar(
DiffSafeHandle diff,
GitDiffFindOptions options);

[DllImport(libgit2)]
internal static extern int git_graph_ahead_behind(out UIntPtr ahead, out UIntPtr behind, RepositorySafeHandle repo, ref GitOid one, ref GitOid two);

Expand Down
9 changes: 9 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,15 @@ public static DiffSafeHandle git_diff_tree_to_workdir(
}
}

public static void git_diff_find_similar(DiffSafeHandle diff, GitDiffFindOptions options)
{
using (ThreadAffinity())
{
int res = NativeMethods.git_diff_find_similar(diff, options);
Ensure.ZeroResult(res);
}
}

#endregion

#region git_graph_
Expand Down
74 changes: 73 additions & 1 deletion LibGit2Sharp/Diff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ private static GitDiffOptions BuildOptions(DiffModifiers diffOptions, FilePath[]
GitDiffOptionFlags.GIT_DIFF_RECURSE_IGNORED_DIRS;
}

if (diffOptions.HasFlag(DiffModifiers.IncludeUnmodified))
if (diffOptions.HasFlag(DiffModifiers.IncludeUnmodified) || compareOptions.IncludeUnmodified ||
(compareOptions.Similarity != null &&
(compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.CopiesHarder ||
compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.Exact)))
{
options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED;
}
Expand Down Expand Up @@ -342,10 +345,79 @@ private DiffSafeHandle BuildDiffList(ObjectId oldTreeId, ObjectId newTreeId, Tre
throw;
}

DetectRenames(diffList, compareOptions);

return diffList;
}
}

private void DetectRenames(DiffSafeHandle diffList, CompareOptions compareOptions)
{
var similarityOptions = (compareOptions == null) ? null : compareOptions.Similarity;
if (similarityOptions == null ||
similarityOptions.RenameDetectionMode == RenameDetectionMode.Default)
{
Proxy.git_diff_find_similar(diffList, null);
return;
}

if (similarityOptions.RenameDetectionMode == RenameDetectionMode.None)
{
return;
}

var opts = new GitDiffFindOptions
{
RenameThreshold = (ushort)similarityOptions.RenameThreshold,
RenameFromRewriteThreshold = (ushort)similarityOptions.RenameFromRewriteThreshold,
CopyThreshold = (ushort)similarityOptions.CopyThreshold,
BreakRewriteThreshold = (ushort)similarityOptions.BreakRewriteThreshold,
RenameLimit = (UIntPtr)similarityOptions.RenameLimit,
};

switch (similarityOptions.RenameDetectionMode)
{
case RenameDetectionMode.Exact:
opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_EXACT_MATCH_ONLY |
GitDiffFindFlags.GIT_DIFF_FIND_RENAMES |
GitDiffFindFlags.GIT_DIFF_FIND_COPIES |
GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
break;
case RenameDetectionMode.Renames:
opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES;
break;
case RenameDetectionMode.Copies:
opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES |
GitDiffFindFlags.GIT_DIFF_FIND_COPIES;
break;
case RenameDetectionMode.CopiesHarder:
opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES |
GitDiffFindFlags.GIT_DIFF_FIND_COPIES |
GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
break;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dahlbyk: I think this logic is correct? (There's an early return above.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


if (!compareOptions.IncludeUnmodified)
{
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_REMOVE_UNMODIFIED;
}

switch (similarityOptions.WhitespaceMode)
{
case WhitespaceMode.DontIgnoreWhitespace:
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE;
break;
case WhitespaceMode.IgnoreLeadingWhitespace:
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE;
break;
case WhitespaceMode.IgnoreAllWhitespace:
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_WHITESPACE;
break;
}

Proxy.git_diff_find_similar(diffList, opts);
}

private static void DispatchUnmatchedPaths(ExplicitPathsOptions explicitPathsOptions,
IEnumerable<FilePath> filePaths,
IEnumerable<FilePath> matchedPaths)
Expand Down
1 change: 1 addition & 0 deletions LibGit2Sharp/LibGit2Sharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<Compile Include="Core\Handles\StatusListSafeHandle.cs" />
<Compile Include="RenameDetails.cs" />
<Compile Include="StatusOptions.cs" />
<Compile Include="SimilarityOptions.cs" />
<Compile Include="UnbornBranchException.cs" />
<Compile Include="LockedFileException.cs" />
<Compile Include="Core\GitRepositoryInitOptions.cs" />
Expand Down
164 changes: 164 additions & 0 deletions LibGit2Sharp/SimilarityOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
namespace LibGit2Sharp
{
/// <summary>
/// Represents a mode for handling whitespace while detecting renames and copies.
/// </summary>
public enum WhitespaceMode
{
/// <summary>
/// Don't consider leading whitespace when comparing files
/// </summary>
IgnoreLeadingWhitespace,

/// <summary>
/// Don't consider any whitespace when comparing files
/// </summary>
IgnoreAllWhitespace,

/// <summary>
/// Include all whitespace when comparing files
/// </summary>
DontIgnoreWhitespace,
}

/// <summary>
/// Represents a mode for detecting renames and copies.
/// </summary>
public enum RenameDetectionMode
{
/// <summary>
/// Obey the user's `diff.renames` configuration setting
/// </summary>
Default,

/// <summary>
/// Attempt no rename or copy detection
/// </summary>
None,

/// <summary>
/// Detect exact renames and copies (compare SHA hashes only)
/// </summary>
Exact,

/// <summary>
/// Detect fuzzy renames (use similarity metric)
/// </summary>
Renames,

/// <summary>
/// Detect renames and copies
/// </summary>
Copies,

/// <summary>
/// Detect renames, and include unmodified files when looking for copies
/// </summary>
CopiesHarder,
}

/// <summary>
/// Options for handling file similarity
/// </summary>
public sealed class SimilarityOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="SimilarityOptions"/> class.
/// </summary>
public SimilarityOptions()
{
RenameDetectionMode = RenameDetectionMode.Default;
WhitespaceMode = WhitespaceMode.IgnoreLeadingWhitespace;
RenameThreshold = 50;
RenameFromRewriteThreshold = 50;
CopyThreshold = 50;
BreakRewriteThreshold = 60;
RenameLimit = 200;
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that does no rename detection
/// </summary>
public static SimilarityOptions None
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.None}; }
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that detects renames
/// </summary>
public static SimilarityOptions Renames
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Renames}; }
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that detects exact renames only
/// </summary>
public static SimilarityOptions Exact
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Exact}; }
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that detects renames and copies
/// </summary>
public static SimilarityOptions Copies
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Copies}; }
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that detects renames, and includes unmodified files when detecting copies
/// </summary>
public static SimilarityOptions CopiesHarder
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.CopiesHarder}; }
}

/// <summary>
/// Get a <see cref="SimilarityOptions"/> instance that obeys the user's `diff.renames` setting
/// </summary>
public static SimilarityOptions Default
{
get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Default}; }
}

/// <summary>
/// The mode for detecting renames and copies
/// </summary>
public RenameDetectionMode RenameDetectionMode { get; set; }

/// <summary>
/// The mode for handling whitespace when comparing files
/// </summary>
public WhitespaceMode WhitespaceMode { get; set; }

/// <summary>
/// Similarity in order to consider a rename
/// </summary>
public int RenameThreshold { get; set; }

/// <summary>
/// Similarity of a modified file in order to be eligible as a rename source
/// </summary>
public int RenameFromRewriteThreshold { get; set; }

/// <summary>
/// Similarity to consider a file a copy
/// </summary>
public int CopyThreshold { get; set; }

/// <summary>
/// Similarity to split modify into an add/delete pair
/// </summary>
public int BreakRewriteThreshold { get; set; }

/// <summary>
/// Maximum similarity sources to examine for a file
/// </summary>
public int RenameLimit { get; set; }

// TODO: custom metric
}
}
Loading