diff --git a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs index 252f7a047..eebbca005 100644 --- a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs +++ b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs @@ -55,7 +55,7 @@ public void CanCompareACommitTreeAgainstItsParent() Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); - TreeEntryChanges treeEntryChanges = changes["1.txt"]; + TreeEntryChanges treeEntryChanges = changes["1.txt"].Single(); Assert.False(treeEntryChanges.IsBinaryComparison); Assert.Equal("1.txt", treeEntryChanges.Path); @@ -128,7 +128,7 @@ public void CanCompareACommitTreeAgainstATreeWithNoCommonAncestor() Assert.Equal(9, changes.LinesAdded); Assert.Equal(2, changes.LinesDeleted); - Assert.Equal(2, changes["readme.txt"].LinesDeleted); + Assert.Equal(2, changes["readme.txt"].Single().LinesDeleted); } } @@ -161,8 +161,8 @@ public void CanDetectTheRenamingOfAModifiedFile() TreeChanges changes = repo.Diff.Compare(rootCommitTree, commitTreeWithRenamedFile); Assert.Equal(1, changes.Count()); - Assert.Equal("super-file.txt", changes["super-file.txt"].Path); - Assert.Equal("my-name-does-not-feel-right.txt", changes["super-file.txt"].OldPath); + Assert.Equal("super-file.txt", changes["super-file.txt"].Single().Path); + Assert.Equal("my-name-does-not-feel-right.txt", changes["super-file.txt"].Single().OldPath); //Assert.Equal(1, changes.FilesRenamed.Count()); } } @@ -275,12 +275,12 @@ public void CanCompareTwoVersionsOfAFileWithADiffOfTwoHunks() Assert.Equal(1, changes.Deleted.Count()); Assert.Equal(1, changes.Added.Count()); - TreeEntryChanges treeEntryChanges = changes["numbers.txt"]; + TreeEntryChanges treeEntryChanges = changes["numbers.txt"].Single(); Assert.Equal(3, treeEntryChanges.LinesAdded); Assert.Equal(1, treeEntryChanges.LinesDeleted); - Assert.Equal(Mode.Nonexistent, changes["my-name-does-not-feel-right.txt"].Mode); + Assert.Equal(Mode.Nonexistent, changes["my-name-does-not-feel-right.txt"].Single().Mode); var expected = new StringBuilder() .Append("diff --git a/numbers.txt b/numbers.txt\n") @@ -354,5 +354,68 @@ public void CanCompareTwoVersionsOfAFileWithADiffOfTwoHunks() Assert.Equal(expected.ToString(), changes.Patch); } } + + [Fact] + public void CanHandleTwoTreeEntryChangesWithTheSamePath() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + + using (Repository repo = Repository.Init(scd.DirectoryPath)) + { + Blob mainContent = CreateBlob(repo, "awesome content\n"); + Blob linkContent = CreateBlob(repo, "../../objc/Nu.h"); + + const string path = "include/Nu/Nu.h"; + + var tdOld = new TreeDefinition() + .Add(path, linkContent, Mode.SymbolicLink) + .Add("objc/Nu.h", mainContent, Mode.NonExecutableFile); + + Tree treeOld = repo.ObjectDatabase.CreateTree(tdOld); + + var tdNew = new TreeDefinition() + .Add(path, mainContent, Mode.NonExecutableFile); + + Tree treeNew = repo.ObjectDatabase.CreateTree(tdNew); + + TreeChanges changes = repo.Diff.Compare(treeOld, treeNew); + + if (IsRunningOnLinux()) + { + TreeEntryChanges[] tecs = changes[path].ToArray(); + Assert.Equal(2, tecs.Length); + + TreeEntryChanges change1 = tecs[0]; + Assert.Equal(Mode.SymbolicLink, change1.OldMode); + Assert.Equal(Mode.Nonexistent, change1.Mode); + Assert.Equal(ChangeKind.Deleted, change1.Status); + Assert.Equal("include/Nu/Nu.h", change1.Path); + + TreeEntryChanges change2 = tecs[1]; + Assert.Equal(Mode.Nonexistent, change2.OldMode); + Assert.Equal(Mode.NonExecutableFile, change2.Mode); + Assert.Equal(ChangeKind.Added, change2.Status); + Assert.Equal("include/Nu/Nu.h", change1.Path); + + return; + } + + Assert.Equal(1, changes[path].Count()); + TreeEntryChanges change = changes[path].Single(); + + Assert.Equal(Mode.SymbolicLink, change.Mode); + Assert.Equal(change.OldMode, change.Mode); + Assert.Equal(ChangeKind.Modified, change.Status); + } + } + + private static Blob CreateBlob(Repository repo, string content) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + using (var binReader = new BinaryReader(stream)) + { + return repo.ObjectDatabase.CreateBlob(binReader); + } + } } } diff --git a/LibGit2Sharp.Tests/StatusFixture.cs b/LibGit2Sharp.Tests/StatusFixture.cs index fa3599294..8850f4a89 100644 --- a/LibGit2Sharp.Tests/StatusFixture.cs +++ b/LibGit2Sharp.Tests/StatusFixture.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Text; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -241,5 +242,59 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives() Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored); } } + + [Fact] + public void CanHandleTwoStatusEntryChangesWithTheSamePath() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + + using (Repository repo = Repository.Init(scd.DirectoryPath)) + { + Blob mainContent = CreateBlob(repo, "awesome content\n"); + Blob linkContent = CreateBlob(repo, "../../objc/Nu.h"); + + const string path = "include/Nu/Nu.h"; + + var tdOld = new TreeDefinition() + .Add(path, linkContent, Mode.SymbolicLink) + .Add("objc/Nu.h", mainContent, Mode.NonExecutableFile); + + Tree tree = repo.ObjectDatabase.CreateTree(tdOld); + + Commit commit = repo.ObjectDatabase.CreateCommit("A symlink", DummySignature, DummySignature, tree, Enumerable.Empty()); + repo.Refs.UpdateTarget("HEAD", commit.Id.Sha); + repo.Reset(ResetOptions.Mixed); + + string fullPath = Path.Combine(repo.Info.WorkingDirectory, "include/Nu"); + Directory.CreateDirectory(fullPath); + + File.WriteAllText(Path.Combine(fullPath, "Nu.h"), "awesome content\n"); + + RepositoryStatus status = repo.Index.RetrieveStatus(); + + if (IsRunningOnLinux()) + { + Assert.Equal(3, status.Count()); + Assert.Equal(new[] { Path.Combine(Path.Combine("include", "Nu"), "Nu.h"), Path.Combine("objc", "Nu.h") }, status.Missing.ToArray()); + Assert.Equal(0, status.Added.Count()); + Assert.Equal(0, status.Modified.Count()); + Assert.Equal(Path.Combine(Path.Combine("include", "Nu"), "Nu.h"), status.Untracked.Single()); + return; + } + + Assert.Equal(2, status.Count()); + Assert.Equal(Path.Combine(Path.Combine("include", "Nu"), "Nu.h"), status.Modified.Single()); + Assert.Equal(Path.Combine("objc", "Nu.h"), status.Missing.Single()); + } + } + + private static Blob CreateBlob(Repository repo, string content) + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + using (var binReader = new BinaryReader(stream)) + { + return repo.ObjectDatabase.CreateBlob(binReader); + } + } } } diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index dfbf16a8b..a7cd91903 100644 --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs +++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs @@ -103,5 +103,12 @@ protected static void AssertValueInConfigFile(string configFilePath, string rege var r = new Regex(regex, RegexOptions.Multiline).Match(text); Assert.True(r.Success, text); } + + protected static bool IsRunningOnLinux() + { + // see http://mono-project.com/FAQ%3a_Technical#Mono_Platforms + var p = (int)Environment.OSVersion.Platform; + return (p == 4) || (p == 6) || (p == 128); + } } } diff --git a/LibGit2Sharp/TreeChanges.cs b/LibGit2Sharp/TreeChanges.cs index 51875f7d1..8938d36da 100644 --- a/LibGit2Sharp/TreeChanges.cs +++ b/LibGit2Sharp/TreeChanges.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Text; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; @@ -13,7 +14,7 @@ namespace LibGit2Sharp /// public class TreeChanges : IEnumerable { - private readonly IDictionary changes = new Dictionary(); + private readonly IDictionary> changes = new Dictionary>(); private readonly List added = new List(); private readonly List deleted = new List(); private readonly List modified = new List(); @@ -79,7 +80,7 @@ private TreeEntryChanges AddFileChange(GitDiffDelta delta, GitDiffLineOrigin lin var newFilePath = FilePathMarshaler.FromNative(delta.NewFile.Path); if (lineorigin != GitDiffLineOrigin.GIT_DIFF_LINE_FILE_HDR) - return this[newFilePath]; + return this[newFilePath].Last(); var oldFilePath = FilePathMarshaler.FromNative(delta.OldFile.Path); var newMode = (Mode)delta.NewFile.Mode; @@ -90,7 +91,9 @@ private TreeEntryChanges AddFileChange(GitDiffDelta delta, GitDiffLineOrigin lin var diffFile = new TreeEntryChanges(newFilePath, newMode, newOid, delta.Status, oldFilePath, oldMode, oldOid, delta.IsBinary()); fileDispatcher[delta.Status](this, diffFile); - changes.Add(newFilePath, diffFile); + + var newFilePathChanges = this[newFilePath] ?? (changes[newFilePath] = new List()); + newFilePathChanges.Add(diffFile); return diffFile; } @@ -102,7 +105,7 @@ private TreeEntryChanges AddFileChange(GitDiffDelta delta, GitDiffLineOrigin lin /// An object that can be used to iterate through the collection. public virtual IEnumerator GetEnumerator() { - return changes.Values.GetEnumerator(); + return changes.Values.SelectMany(tec => tec.AsEnumerable()).GetEnumerator(); } /// @@ -119,16 +122,16 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Gets the corresponding to the specified . /// - public virtual TreeEntryChanges this[string path] + public virtual IEnumerable this[string path] { get { return this[(FilePath)path]; } } - private TreeEntryChanges this[FilePath path] + private List this[FilePath path] { get { - TreeEntryChanges treeEntryChanges; + List treeEntryChanges; if (changes.TryGetValue(path, out treeEntryChanges)) { return treeEntryChanges; diff --git a/LibGit2Sharp/TreeDefinition.cs b/LibGit2Sharp/TreeDefinition.cs index 8a725a5eb..7da0eea6b 100644 --- a/LibGit2Sharp/TreeDefinition.cs +++ b/LibGit2Sharp/TreeDefinition.cs @@ -136,7 +136,7 @@ public TreeDefinition Add(string targetTreeEntryPath, Blob blob, Mode mode) { Ensure.ArgumentNotNull(blob, "blob"); Ensure.ArgumentConformsTo(mode, - m => m.HasAny(new[] { Mode.ExecutableFile, Mode.NonExecutableFile, Mode.NonExecutableGroupWritableFile }), "mode"); + m => m.HasAny(new[] { Mode.ExecutableFile, Mode.NonExecutableFile, Mode.NonExecutableGroupWritableFile, Mode.SymbolicLink }), "mode"); TreeEntryDefinition ted = TreeEntryDefinition.From(blob, mode);