Skip to content

Add rename detection to tree comparisons #278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
223 changes: 222 additions & 1 deletion LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public void CanCompareTwoVersionsOfAFileWithADiffOfTwoHunks()
Assert.Equal(expected.ToString(), changes.Patch);
}
}

[Fact]
public void CanCompareATreeAgainstANullTree()
{
Expand Down Expand Up @@ -436,6 +436,227 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny()
}
}

[Fact]
public void CanDetectTheExactRenamingOfFilesWhenEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

repo.Index.Move(originalPath, renamedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, find: Find.Renames());

Assert.Equal(1, changes.Count());
Assert.Equal(1, changes.Renamed.Count());
Assert.Equal("original.txt", changes.Renamed.Single().OldPath);
Assert.Equal("renamed.txt", changes.Renamed.Single().Path);
}
}

[Fact]
public void CanNotDetectTheExactRenamingFilesWhenNotEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

repo.Index.Move(originalPath, renamedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree);

Assert.Equal(2, changes.Count());
Assert.Equal(0, changes.Renamed.Count());
}
}

[Fact]
public void CanDetectTheExactCopyingOfNonModifiedFilesWhenEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

File.Copy(originalPath, copiedPath);
repo.Index.Stage(copiedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, find: Find.Copies(checkUnmodifiedFiles: true));

Assert.Equal(1, changes.Count());
Assert.Equal(1, changes.Copied.Count());
Assert.Equal("original.txt", changes.Copied.Single().OldPath);
Assert.Equal("copied.txt", changes.Copied.Single().Path);
}
}

[Fact]
public void CanNotDetectTheExactCopyingOfNonModifiedFilesWhenNotEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

File.Copy(originalPath, copiedPath);
repo.Index.Stage(copiedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree);

Assert.Equal(1, changes.Count());
Assert.Equal(0, changes.Copied.Count());
}
}

[Fact]
public void CanDetectTheExactCopyingOfModifiedFilesWhenEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

File.Copy(originalPath, copiedPath);
File.AppendAllText(originalPath, "e\n");

repo.Index.Stage(originalPath);
repo.Index.Stage(copiedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, find: Find.Copies());

Assert.Equal(2, changes.Count());
Assert.Equal(1, changes.Copied.Count());
Assert.Equal("original.txt", changes.Copied.Single().OldPath);
Assert.Equal("copied.txt", changes.Copied.Single().Path);
}
}

[Fact]
public void CanNotDetectTheExactCopyingOfModifiedFilesWhenNotEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");

repo.Index.Stage(originalPath);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

File.Copy(originalPath, copiedPath);
File.AppendAllText(originalPath, "e\n");

repo.Index.Stage(originalPath);
repo.Index.Stage(copiedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree);

Assert.Equal(2, changes.Count());
Assert.Equal(0, changes.Copied.Count());
}
}

[Fact]
public void CanDetectTheExactRenamingExactCopyingOfNonModifiedAndModifiedFilesWhenEnabled()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
using (var repo = Repository.Init(scd.DirectoryPath))
{
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");
string originalPath2 = Path.Combine(repo.Info.WorkingDirectory, "original2.txt");
string copiedPath1 = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
string originalPath3 = Path.Combine(repo.Info.WorkingDirectory, "original3.txt");
string copiedPath2 = Path.Combine(repo.Info.WorkingDirectory, "copied2.txt");

File.WriteAllText(originalPath, "a\nb\nc\nd\n");
File.WriteAllText(originalPath2, "1\n2\n3\n4\n");
File.WriteAllText(originalPath3, "5\n6\n7\n8\n");

repo.Index.Stage(originalPath);
repo.Index.Stage(originalPath2);
repo.Index.Stage(originalPath3);

Commit old = repo.Commit("Initial", DummySignature, DummySignature);

File.Copy(originalPath2, copiedPath1);
File.Copy(originalPath3, copiedPath2);
File.AppendAllText(originalPath3, "9\n");

repo.Index.Stage(originalPath3);
repo.Index.Stage(copiedPath1);
repo.Index.Stage(copiedPath2);
repo.Index.Move(originalPath, renamedPath);

Commit @new = repo.Commit("Updated", DummySignature, DummySignature);

TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree,
find: Find.Renames().AndCopies(checkUnmodifiedFiles: true));

Assert.Equal(4, changes.Count());
Assert.Equal(1, changes.Renamed.Count());
Assert.Equal("original.txt", changes.Renamed.Single().OldPath);
Assert.Equal("renamed.txt", changes.Renamed.Single().Path);
Assert.Equal(2, changes.Copied.Count());
Assert.Equal("original2.txt", changes.Copied.ElementAt(0).OldPath);
Assert.Equal("copied.txt", changes.Copied.ElementAt(0).Path);
Assert.Equal("original3.txt", changes.Copied.ElementAt(1).OldPath);
Assert.Equal("copied2.txt", changes.Copied.ElementAt(1).Path);
}
}

private RepositoryOptions BuildFakeSystemConfigFilemodeOption(
SelfCleaningDirectory scd,
bool value)
Expand Down
33 changes: 33 additions & 0 deletions LibGit2Sharp/Core/GitDiff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,37 @@ enum GitDiffLineOrigin : byte
GIT_DIFF_LINE_HUNK_HDR = 0x48, //'H',
GIT_DIFF_LINE_BINARY = 0x42, //'B',
}

[Flags]
internal enum GitDiffFindOptionFlags
{
GIT_DIFF_FIND_DEFAULT = 0,

/** look for renames? (`--find-renames`) */
GIT_DIFF_FIND_RENAMES = (1 << 0),

/** consider old size 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),

/** split large rewrites into delete/add pairs (`--break-rewrites=/M`) */
GIT_DIFF_FIND_AND_BREAK_REWRITES = (1 << 4),
}

[StructLayout(LayoutKind.Sequential)]
internal class GitDiffFindOptions
{
public uint Version = 1;
public GitDiffFindOptionFlags Flags;
public uint RenameThreshold;
public uint RenameFromRewriteThreshold;
public uint CopyThreshold;
public uint BreakRewriteThreshold;
public uint TargetLimit;
}
}
5 changes: 5 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ internal static extern int git_diff_merge(
DiffListSafeHandle onto,
DiffListSafeHandle from);

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

[DllImport(libgit2)]
internal static extern int git_diff_index_to_workdir(
out DiffListSafeHandle diff,
Expand Down
11 changes: 11 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,17 @@ public static DiffListSafeHandle git_diff_tree_to_workdir(
}
}

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

#endregion

#region git_graph_
Expand Down
45 changes: 35 additions & 10 deletions LibGit2Sharp/Diff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ private GitDiffOptions BuildOptions(DiffOptions diffOptions, IEnumerable<string>
GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT;
}

if (diffOptions.HasFlag(DiffOptions.IncludeUnmodified))
{
options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED;
}

if (paths == null)
{
return options;
Expand Down Expand Up @@ -92,27 +97,47 @@ internal Diff(Repository repo)
}

/// <summary>
/// Show changes between two <see cref = "Tree"/>s.
/// Show changes between two <see cref="Tree" />s.
/// </summary>
/// <param name = "oldTree">The <see cref = "Tree"/> you want to compare from.</param>
/// <param name = "newTree">The <see cref = "Tree"/> you want to compare to.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be compared.</param>
/// <returns>A <see cref = "TreeChanges"/> containing the changes between the <paramref name = "oldTree"/> and the <paramref name = "newTree"/>.</returns>
public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable<string> paths = null)
/// <param name="oldTree">The <see cref="Tree" /> you want to compare from.</param>
/// <param name="newTree">The <see cref="Tree" /> you want to compare to.</param>
/// <param name="paths">The list of paths (either files or directories) that should be compared.</param>
/// <param name="find">The options for finding renames and copies.</param>
/// <returns>
/// A <see cref="TreeChanges" /> containing the changes between the <paramref name="oldTree" /> and the <paramref name="newTree" />.
/// </returns>
public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable<string> paths = null, Find find = null)
{
using(GitDiffOptions options = BuildOptions(DiffOptions.None, paths))
var diffOptions = DiffOptions.None;
if (find != null && find.RequiresUnmodifiedFiles)
{
diffOptions |= DiffOptions.IncludeUnmodified;
}

using(GitDiffOptions options = BuildOptions(diffOptions, paths))
using (DiffListSafeHandle diff = BuildDiffListFromTrees(
oldTree != null ? oldTree.Id : null,
newTree != null ? newTree.Id : null,
options))
options,
find))
{
return new TreeChanges(diff);
}
}

private DiffListSafeHandle BuildDiffListFromTrees(ObjectId oldTree, ObjectId newTree, GitDiffOptions options)
private DiffListSafeHandle BuildDiffListFromTrees(ObjectId oldTree, ObjectId newTree, GitDiffOptions options, Find find)
{
var diff = Proxy.git_diff_tree_to_tree(repo.Handle, oldTree, newTree, options);
HandleRenameAndCopyDetection(diff, find);
return diff;
}

private void HandleRenameAndCopyDetection(DiffListSafeHandle diff, Find find)
{
return Proxy.git_diff_tree_to_tree(repo.Handle, oldTree, newTree, options);
if (find != null)
{
Proxy.git_diff_find_similar(diff, find.Options);
}
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions LibGit2Sharp/DiffOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ internal enum DiffOptions
/// diffing against the working directory.
/// </summary>
IncludeUntracked,

/// <summary>
/// Include unmodified files among the files to be processed. This
/// is a requirement when emulating --find-copies-harder
/// </summary>
IncludeUnmodified,
}
}
Loading