Skip to content

Commit eb935c1

Browse files
roend83ben
authored andcommitted
Initial rename detection work
1 parent e3b6d5e commit eb935c1

File tree

6 files changed

+344
-5
lines changed

6 files changed

+344
-5
lines changed

LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs

Lines changed: 264 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,284 @@ public void ComparingATreeAgainstAnotherTreeWithStrictExplicitPathsValidationThr
188188
* $ git diff -M --shortstat f8d44d7..4be51d6
189189
* 1 file changed, 1 insertion(+)
190190
*/
191-
[Fact(Skip = "Not implemented in libgit2 yet.")]
191+
[Fact]
192192
public void CanDetectTheRenamingOfAModifiedFile()
193193
{
194194
using (var repo = new Repository(StandardTestRepoPath))
195195
{
196196
Tree rootCommitTree = repo.Lookup<Commit>("f8d44d7").Tree;
197197
Tree commitTreeWithRenamedFile = repo.Lookup<Commit>("4be51d6").Tree;
198198

199-
var changes = repo.Diff.Compare<TreeChanges>(rootCommitTree, commitTreeWithRenamedFile);
199+
var changes = repo.Diff.Compare<TreeChanges>(rootCommitTree, commitTreeWithRenamedFile,
200+
compareOptions: new CompareOptions
201+
{
202+
DetectRenames = true,
203+
});
200204

201205
Assert.Equal(1, changes.Count());
202206
Assert.Equal("super-file.txt", changes["super-file.txt"].Path);
203207
Assert.Equal("my-name-does-not-feel-right.txt", changes["super-file.txt"].OldPath);
204-
//Assert.Equal(1, changes.FilesRenamed.Count());
208+
Assert.Equal(1, changes.Renamed.Count());
209+
}
210+
}
211+
212+
[Fact]
213+
public void CanDetectTheExactRenamingOfFilesWhenEnabled()
214+
{
215+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
216+
var path = Repository.Init(scd.DirectoryPath);
217+
using (var repo = new Repository(path))
218+
{
219+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
220+
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");
221+
222+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
223+
224+
repo.Index.Stage(originalPath);
225+
226+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
227+
228+
repo.Index.Move(originalPath, renamedPath);
229+
230+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
231+
232+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
233+
compareOptions:
234+
new CompareOptions
235+
{
236+
DetectRenames = true,
237+
});
238+
239+
Assert.Equal(1, changes.Count());
240+
Assert.Equal(1, changes.Renamed.Count());
241+
Assert.Equal("original.txt", changes.Renamed.Single().OldPath);
242+
Assert.Equal("renamed.txt", changes.Renamed.Single().Path);
243+
}
244+
}
245+
246+
[Fact]
247+
public void CanNotDetectTheExactRenamingFilesWhenNotEnabled()
248+
{
249+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
250+
var path = Repository.Init(scd.DirectoryPath);
251+
using (var repo = new Repository(path))
252+
{
253+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
254+
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");
255+
256+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
257+
258+
repo.Index.Stage(originalPath);
259+
260+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
261+
262+
repo.Index.Move(originalPath, renamedPath);
263+
264+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
265+
266+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
267+
compareOptions:
268+
new CompareOptions
269+
{
270+
DetectRenames = false,
271+
});
272+
273+
Assert.Equal(2, changes.Count());
274+
Assert.Equal(0, changes.Renamed.Count());
275+
}
276+
}
277+
278+
[Fact]
279+
public void CanDetectTheExactCopyingOfNonModifiedFilesWhenEnabled()
280+
{
281+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
282+
var path = Repository.Init(scd.DirectoryPath);
283+
using (var repo = new Repository(path))
284+
{
285+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
286+
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
287+
288+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
289+
290+
repo.Index.Stage(originalPath);
291+
292+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
293+
294+
File.Copy(originalPath, copiedPath);
295+
repo.Index.Stage(copiedPath);
296+
297+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
298+
299+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
300+
compareOptions:
301+
new CompareOptions
302+
{
303+
DetectCopiesFromUnmodified = true,
304+
});
305+
306+
Assert.Equal(1, changes.Count());
307+
Assert.Equal(1, changes.Copied.Count());
308+
Assert.Equal("original.txt", changes.Copied.Single().OldPath);
309+
Assert.Equal("copied.txt", changes.Copied.Single().Path);
310+
}
311+
}
312+
313+
[Fact]
314+
public void CanNotDetectTheExactCopyingOfNonModifiedFilesWhenNotEnabled()
315+
{
316+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
317+
var path = Repository.Init(scd.DirectoryPath);
318+
using (var repo = new Repository(path))
319+
{
320+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
321+
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
322+
323+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
324+
325+
repo.Index.Stage(originalPath);
326+
327+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
328+
329+
File.Copy(originalPath, copiedPath);
330+
repo.Index.Stage(copiedPath);
331+
332+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
333+
334+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
335+
compareOptions:
336+
new CompareOptions
337+
{
338+
DetectCopies = false,
339+
});
340+
341+
Assert.Equal(1, changes.Count());
342+
Assert.Equal(0, changes.Copied.Count());
343+
}
344+
}
345+
346+
[Fact]
347+
public void CanDetectTheExactCopyingOfModifiedFilesWhenEnabled()
348+
{
349+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
350+
var path = Repository.Init(scd.DirectoryPath);
351+
using (var repo = new Repository(path))
352+
{
353+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
354+
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
355+
356+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
357+
358+
repo.Index.Stage(originalPath);
359+
360+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
361+
362+
File.Copy(originalPath, copiedPath);
363+
File.AppendAllText(originalPath, "e\n");
364+
365+
repo.Index.Stage(originalPath);
366+
repo.Index.Stage(copiedPath);
367+
368+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
369+
370+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
371+
compareOptions:
372+
new CompareOptions
373+
{
374+
DetectCopies = true,
375+
});
376+
377+
Assert.Equal(2, changes.Count());
378+
Assert.Equal(1, changes.Copied.Count());
379+
Assert.Equal("original.txt", changes.Copied.Single().OldPath);
380+
Assert.Equal("copied.txt", changes.Copied.Single().Path);
381+
}
382+
}
383+
384+
[Fact]
385+
public void CanNotDetectTheExactCopyingOfModifiedFilesWhenNotEnabled()
386+
{
387+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
388+
var path = Repository.Init(scd.DirectoryPath);
389+
using (var repo = new Repository(path))
390+
{
391+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
392+
string copiedPath = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
393+
394+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
395+
396+
repo.Index.Stage(originalPath);
397+
398+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
399+
400+
File.Copy(originalPath, copiedPath);
401+
File.AppendAllText(originalPath, "e\n");
402+
403+
repo.Index.Stage(originalPath);
404+
repo.Index.Stage(copiedPath);
405+
406+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
407+
408+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree);
409+
410+
Assert.Equal(2, changes.Count());
411+
Assert.Equal(0, changes.Copied.Count());
205412
}
206413
}
207414

415+
[Fact]
416+
public void CanDetectTheExactRenamingExactCopyingOfNonModifiedAndModifiedFilesWhenEnabled()
417+
{
418+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
419+
var path = Repository.Init(scd.DirectoryPath);
420+
using (var repo = new Repository(path))
421+
{
422+
string originalPath = Path.Combine(repo.Info.WorkingDirectory, "original.txt");
423+
string renamedPath = Path.Combine(repo.Info.WorkingDirectory, "renamed.txt");
424+
string originalPath2 = Path.Combine(repo.Info.WorkingDirectory, "original2.txt");
425+
string copiedPath1 = Path.Combine(repo.Info.WorkingDirectory, "copied.txt");
426+
string originalPath3 = Path.Combine(repo.Info.WorkingDirectory, "original3.txt");
427+
string copiedPath2 = Path.Combine(repo.Info.WorkingDirectory, "copied2.txt");
428+
429+
File.WriteAllText(originalPath, "a\nb\nc\nd\n");
430+
File.WriteAllText(originalPath2, "1\n2\n3\n4\n");
431+
File.WriteAllText(originalPath3, "5\n6\n7\n8\n");
432+
433+
repo.Index.Stage(originalPath);
434+
repo.Index.Stage(originalPath2);
435+
repo.Index.Stage(originalPath3);
436+
437+
Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature);
438+
439+
File.Copy(originalPath2, copiedPath1);
440+
File.Copy(originalPath3, copiedPath2);
441+
File.AppendAllText(originalPath3, "9\n");
442+
443+
repo.Index.Stage(originalPath3);
444+
repo.Index.Stage(copiedPath1);
445+
repo.Index.Stage(copiedPath2);
446+
repo.Index.Move(originalPath, renamedPath);
447+
448+
Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature);
449+
450+
TreeChanges changes = repo.Diff.Compare<TreeChanges>(old.Tree, @new.Tree,
451+
compareOptions:
452+
new CompareOptions
453+
{
454+
DetectCopiesFromUnmodified = true,
455+
});
456+
457+
Assert.Equal(4, changes.Count());
458+
Assert.Equal(1, changes.Modified.Count());
459+
Assert.Equal(1, changes.Renamed.Count());
460+
Assert.Equal("original.txt", changes.Renamed.Single().OldPath);
461+
Assert.Equal("renamed.txt", changes.Renamed.Single().Path);
462+
Assert.Equal(2, changes.Copied.Count());
463+
Assert.Equal("original2.txt", changes.Copied.ElementAt(0).OldPath);
464+
Assert.Equal("copied.txt", changes.Copied.ElementAt(0).Path);
465+
Assert.Equal("original3.txt", changes.Copied.ElementAt(1).OldPath);
466+
Assert.Equal("copied2.txt", changes.Copied.ElementAt(1).Path);
467+
}
468+
}
208469
/*
209470
* $ git diff f8d44d7..ec9e401
210471
* diff --git a/numbers.txt b/numbers.txt

LibGit2Sharp/CompareOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,20 @@ public CompareOptions()
2525
/// (Default = 0)
2626
/// </summary>
2727
public int InterhunkLines { get; set; }
28+
29+
/// <summary>
30+
/// Look for renames?
31+
/// </summary>
32+
public bool DetectRenames { get; set; }
33+
34+
/// <summary>
35+
/// Look for copies?
36+
/// </summary>
37+
public bool DetectCopies { get; set; }
38+
39+
/// <summary>
40+
/// Consider unmodified files as copy sources?
41+
/// </summary>
42+
public bool DetectCopiesFromUnmodified { get; set; }
2843
}
2944
}

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,11 @@ internal static extern int git_diff_foreach(
473473
git_diff_line_cb lineCallback,
474474
IntPtr payload);
475475

476+
[DllImport(libgit2)]
477+
internal static extern int git_diff_find_similar(
478+
DiffSafeHandle diff,
479+
GitDiffFindOptions options);
480+
476481
[DllImport(libgit2)]
477482
internal static extern int git_graph_ahead_behind(out UIntPtr ahead, out UIntPtr behind, RepositorySafeHandle repo, ref GitOid one, ref GitOid two);
478483

LibGit2Sharp/Core/Proxy.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,15 @@ public static DiffSafeHandle git_diff_tree_to_workdir(
713713
}
714714
}
715715

716+
public static void git_diff_find_similar(DiffSafeHandle diff, GitDiffFindOptions options)
717+
{
718+
using (ThreadAffinity())
719+
{
720+
int res = NativeMethods.git_diff_find_similar(diff, options);
721+
Ensure.ZeroResult(res);
722+
}
723+
}
724+
716725
#endregion
717726

718727
#region git_graph_

LibGit2Sharp/Diff.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,10 +342,39 @@ private DiffSafeHandle BuildDiffList(ObjectId oldTreeId, ObjectId newTreeId, Tre
342342
throw;
343343
}
344344

345+
DetectRenames(diffList, compareOptions);
346+
345347
return diffList;
346348
}
347349
}
348350

351+
private void DetectRenames(DiffSafeHandle diffList, CompareOptions compareOptions)
352+
{
353+
if (compareOptions == null) return;
354+
355+
var opts = new GitDiffFindOptions();
356+
if (compareOptions.DetectRenames)
357+
{
358+
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_RENAMES;
359+
}
360+
if (compareOptions.DetectCopies)
361+
{
362+
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_RENAMES |
363+
GitDiffFindFlags.GIT_DIFF_FIND_COPIES;
364+
}
365+
if (compareOptions.DetectCopiesFromUnmodified)
366+
{
367+
opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_RENAMES |
368+
GitDiffFindFlags.GIT_DIFF_FIND_COPIES |
369+
GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED;
370+
}
371+
372+
if (opts.Flags != 0)
373+
{
374+
Proxy.git_diff_find_similar(diffList, opts);
375+
}
376+
}
377+
349378
private static void DispatchUnmatchedPaths(ExplicitPathsOptions explicitPathsOptions,
350379
IEnumerable<FilePath> filePaths,
351380
IEnumerable<FilePath> matchedPaths)

0 commit comments

Comments
 (0)