diff --git a/LibGit2Sharp.Tests/ConflictFixture.cs b/LibGit2Sharp.Tests/ConflictFixture.cs index d4db890b4..16a93206f 100644 --- a/LibGit2Sharp.Tests/ConflictFixture.cs +++ b/LibGit2Sharp.Tests/ConflictFixture.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -14,18 +15,62 @@ public static IEnumerable ConflictData { return new[] { - new string[] { "ancestor-and-ours.txt", "5dee68477001f447f50fa7ee7e6a818370b5c2fb", "dad0664ae617d36e464ec08ed969ff496432b075", null }, - new string[] { "ancestor-and-theirs.txt", "3aafd4d0bac33cc3c78c4c070f3966fb6e6f641a", null, "7b26cd5ac0ee68483ae4d5e1e00b064547ea8c9b" }, - new string[] { "ancestor-only.txt", "9736f4cd77759672322f3222ed3ddead1412d969", null, null }, - new string[] { "conflicts-one.txt", "1f85ca51b8e0aac893a621b61a9c2661d6aa6d81", "b7a41c703dc1f33185c76944177f3844ede2ee46", "516bd85f78061e09ccc714561d7b504672cb52da" }, - new string[] { "conflicts-two.txt", "84af62840be1b1c47b778a8a249f3ff45155038c", "ef70c7154145b09c7d08806e55fd0bfb7172576d", "220bd62631c8cf7a83ef39c6b94595f00517211e" }, - new string[] { "ours-and-theirs.txt", null, "9aaa9ae562a5f7362425a3fedc4d33ff74fe39e6", "0ca3f55d4ac2fa4703c149123b0b31d733112f86" }, - new string[] { "ours-only.txt", null, "9736f4cd77759672322f3222ed3ddead1412d969", null }, - new string[] { "theirs-only.txt", null, null, "9736f4cd77759672322f3222ed3ddead1412d969" }, + new[] { "ancestor-and-ours.txt", "5dee68477001f447f50fa7ee7e6a818370b5c2fb", "dad0664ae617d36e464ec08ed969ff496432b075", null }, + new[] { "ancestor-and-theirs.txt", "3aafd4d0bac33cc3c78c4c070f3966fb6e6f641a", null, "7b26cd5ac0ee68483ae4d5e1e00b064547ea8c9b" }, + new[] { "ancestor-only.txt", "9736f4cd77759672322f3222ed3ddead1412d969", null, null }, + new[] { "conflicts-one.txt", "1f85ca51b8e0aac893a621b61a9c2661d6aa6d81", "b7a41c703dc1f33185c76944177f3844ede2ee46", "516bd85f78061e09ccc714561d7b504672cb52da" }, + new[] { "conflicts-two.txt", "84af62840be1b1c47b778a8a249f3ff45155038c", "ef70c7154145b09c7d08806e55fd0bfb7172576d", "220bd62631c8cf7a83ef39c6b94595f00517211e" }, + new[] { "ours-and-theirs.txt", null, "9aaa9ae562a5f7362425a3fedc4d33ff74fe39e6", "0ca3f55d4ac2fa4703c149123b0b31d733112f86" }, + new[] { "ours-only.txt", null, "9736f4cd77759672322f3222ed3ddead1412d969", null }, + new[] { "theirs-only.txt", null, null, "9736f4cd77759672322f3222ed3ddead1412d969" }, }; } } + [Theory] + [InlineData(true, "ancestor-and-ours.txt", true, false, FileStatus.Removed, 2)] + [InlineData(false, "ancestor-and-ours.txt", true, true, FileStatus.Removed |FileStatus.Untracked, 2)] + [InlineData(true, "ancestor-and-theirs.txt", true, false, FileStatus.Nonexistent, 2)] + [InlineData(false, "ancestor-and-theirs.txt", true, true, FileStatus.Untracked, 2)] + [InlineData(true, "conflicts-one.txt", true, false, FileStatus.Removed, 3)] + [InlineData(false, "conflicts-one.txt", true, true, FileStatus.Removed | FileStatus.Untracked, 3)] + [InlineData(true, "conflicts-two.txt", true, false, FileStatus.Removed, 3)] + [InlineData(false, "conflicts-two.txt", true, true, FileStatus.Removed | FileStatus.Untracked, 3)] + [InlineData(true, "ours-and-theirs.txt", true, false, FileStatus.Removed, 2)] + [InlineData(false, "ours-and-theirs.txt", true, true, FileStatus.Removed | FileStatus.Untracked, 2)] + [InlineData(true, "ours-only.txt", true, false, FileStatus.Removed, 1)] + [InlineData(false, "ours-only.txt", true, true, FileStatus.Removed | FileStatus.Untracked, 1)] + [InlineData(true, "theirs-only.txt", true, false, FileStatus.Nonexistent, 1)] + [InlineData(false, "theirs-only.txt", true, true, FileStatus.Untracked, 1)] + /* Conflicts clearing through Index.Remove() only works when a version of the entry exists in the workdir. + * This is because libgit2's git_iterator_for_index() seem to only care about stage level 0. + * Corrolary: other cases only work out of sheer luck (however, the behaviour is stable, so I guess we + * can rely on it for the moment. + * [InlineData(true, "ancestor-only.txt", false, false, FileStatus.Nonexistent, 0)] + * [InlineData(false, "ancestor-only.txt", false, false, FileStatus.Nonexistent, 0)] + */ + public void CanClearConflictsByRemovingFromTheIndex( + bool removeFromWorkdir, string filename, bool existsBeforeRemove, bool existsAfterRemove, FileStatus lastStatus, int removedIndexEntries) + { + var path = CloneMergedTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(existsBeforeRemove, File.Exists(fullpath)); + Assert.NotNull(repo.Conflicts[filename]); + + repo.Index.Remove(filename, removeFromWorkdir); + + Assert.Null(repo.Conflicts[filename]); + Assert.Equal(count - removedIndexEntries, repo.Index.Count); + Assert.Equal(existsAfterRemove, File.Exists(fullpath)); + Assert.Equal(lastStatus, repo.Index.RetrieveStatus(filename)); + } + } + [Theory, PropertyData("ConflictData")] public void CanRetrieveSingleConflictByPath(string filepath, string ancestorId, string ourId, string theirId) { @@ -78,7 +123,7 @@ public void CanRetrieveAllConflicts() { using (var repo = new Repository(MergedTestRepoWorkingDirPath)) { - var expected = repo.Conflicts.Select(c => new string[] { GetPath(c), GetId(c.Ancestor), GetId(c.Ours), GetId(c.Theirs) }).ToArray(); + var expected = repo.Conflicts.Select(c => new[] { GetPath(c), GetId(c.Ancestor), GetId(c.Ours), GetId(c.Theirs) }).ToArray(); Assert.Equal(expected, ConflictData); } } diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 3e004ff5b..a1667dc4c 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -194,53 +194,6 @@ private static void InvalidMoveUseCases(string sourcePath, FileStatus sourceStat } } - [Theory] - [InlineData("1/branch_file.txt", FileStatus.Unaltered, true, FileStatus.Removed)] - [InlineData("deleted_unstaged_file.txt", FileStatus.Missing, false, FileStatus.Removed)] - public void CanRemoveAFile(string filename, FileStatus initialStatus, bool shouldInitiallyExist, FileStatus finalStatus) - { - string path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); - - Assert.Equal(shouldInitiallyExist, File.Exists(fullpath)); - Assert.Equal(initialStatus, repo.Index.RetrieveStatus(filename)); - - repo.Index.Remove(filename); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.False(File.Exists(fullpath)); - Assert.Equal(finalStatus, repo.Index.RetrieveStatus(filename)); - } - } - - [Theory] - [InlineData("deleted_staged_file.txt")] - [InlineData("modified_unstaged_file.txt")] - [InlineData("shadowcopy_of_an_unseen_ghost.txt")] - public void RemovingAInvalidFileThrows(string filepath) - { - using (var repo = new Repository(StandardTestRepoPath)) - { - Assert.Throws(() => repo.Index.Remove(filepath)); - } - } - - [Fact] - public void RemovingFileWithBadParamsThrows() - { - using (var repo = new Repository(StandardTestRepoPath)) - { - Assert.Throws(() => repo.Index.Remove(string.Empty)); - Assert.Throws(() => repo.Index.Remove((string)null)); - Assert.Throws(() => repo.Index.Remove(new string[] { })); - Assert.Throws(() => repo.Index.Remove(new string[] { null })); - } - } - [Fact] public void PathsOfIndexEntriesAreExpressedInNativeFormat() { diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 165670709..6fe9acb94 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -59,6 +59,7 @@ + diff --git a/LibGit2Sharp.Tests/RemoveFixture.cs b/LibGit2Sharp.Tests/RemoveFixture.cs new file mode 100644 index 000000000..6dc6507cb --- /dev/null +++ b/LibGit2Sharp.Tests/RemoveFixture.cs @@ -0,0 +1,187 @@ +using System; +using System.IO; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using Xunit.Extensions; + +namespace LibGit2Sharp.Tests +{ + public class RemoveFixture : BaseFixture + { + [Theory] + /*** + * Test case: file exists in workdir and index, and has not been modified. + * 'git rm --cached ' works (file removed only from index). + * 'git rm ' works (file removed from both index and workdir). + */ + [InlineData(false, "1/branch_file.txt", false, FileStatus.Unaltered, true, true, FileStatus.Untracked | FileStatus.Removed)] + [InlineData(true, "1/branch_file.txt", false, FileStatus.Unaltered, true, false, FileStatus.Removed)] + /*** + * Test case: file exists in the index, and has been removed from the wd. + * 'git rm and 'git rm --cached ' both work (file removed from the index) + */ + [InlineData(true, "deleted_unstaged_file.txt", false, FileStatus.Missing, false, false, FileStatus.Removed)] + [InlineData(false, "deleted_unstaged_file.txt", false, FileStatus.Missing, false, false, FileStatus.Removed)] + /*** + * Test case: modified file in wd, the modifications have not been promoted to the index yet. + * 'git rm --cached ' works (removes the file from the index) + * 'git rm ' fails ("error: '' has local modifications"). + */ + [InlineData(false, "modified_unstaged_file.txt", false, FileStatus.Modified, true, true, FileStatus.Untracked | FileStatus.Removed)] + [InlineData(true, "modified_unstaged_file.txt", true, FileStatus.Modified, true, true, 0)] + /*** + * Test case: modified file in wd, the modifications have already been promoted to the index. + * 'git rm --cached ' works (removes the file from the index) + * 'git rm ' fails ("error: '' has changes staged in the index") + */ + [InlineData(false, "modified_staged_file.txt", false, FileStatus.Staged, true, true, FileStatus.Untracked | FileStatus.Removed)] + [InlineData(true, "modified_staged_file.txt", true, FileStatus.Staged, true, true, 0)] + /*** + * Test case: modified file in wd, the modifications have already been promoted to the index, and + * the file does not exist in the HEAD. + * 'git rm --cached ' works (removes the file from the index) + * 'git rm ' throws ("error: '' has changes staged in the index") + */ + [InlineData(false, "new_tracked_file.txt", false, FileStatus.Added, true, true, FileStatus.Untracked)] + [InlineData(true, "new_tracked_file.txt", true, FileStatus.Added, true, true, 0)] + public void CanRemoveAnUnalteredFileFromTheIndexWithoutRemovingItFromTheWorkingDirectory( + bool removeFromWorkdir, string filename, bool throws, FileStatus initialStatus, bool existsBeforeRemove, bool existsAfterRemove, FileStatus lastStatus) + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(initialStatus, repo.Index.RetrieveStatus(filename)); + Assert.Equal(existsBeforeRemove, File.Exists(fullpath)); + + if (throws) + { + Assert.Throws(() => repo.Index.Remove(filename, removeFromWorkdir)); + Assert.Equal(count, repo.Index.Count); + } + else + { + repo.Index.Remove(filename, removeFromWorkdir); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.Equal(existsAfterRemove, File.Exists(fullpath)); + Assert.Equal(lastStatus, repo.Index.RetrieveStatus(filename)); + } + } + } + + /*** + * Test case: modified file in wd, the modifications have already been promoted to the index, and + * new modifications have been made in the wd. + * 'git rm ' and 'git rm --cached ' both fail ("error: '' has staged content different from both the file and the HEAD") + */ + [Fact] + public void RemovingAModifiedFileWhoseChangesHaveBeenPromotedToTheIndexAndWithAdditionalModificationsMadeToItThrows() + { + const string filename = "modified_staged_file.txt"; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(true, File.Exists(fullpath)); + + File.AppendAllText(fullpath, "additional content"); + Assert.Equal(FileStatus.Staged | FileStatus.Modified, repo.Index.RetrieveStatus(filename)); + + Assert.Throws(() => repo.Index.Remove(filename)); + Assert.Throws(() => repo.Index.Remove(filename, false)); + } + } + + [Fact] + public void CanRemoveAFolderThroughUsageOfPathspecsForNewlyAddedFiles() + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + repo.Index.Stage(Touch(repo.Info.WorkingDirectory, "2/subdir1/2.txt", "whone")); + repo.Index.Stage(Touch(repo.Info.WorkingDirectory, "2/subdir1/3.txt", "too")); + repo.Index.Stage(Touch(repo.Info.WorkingDirectory, "2/subdir2/4.txt", "tree")); + repo.Index.Stage(Touch(repo.Info.WorkingDirectory, "2/5.txt", "for")); + repo.Index.Stage(Touch(repo.Info.WorkingDirectory, "2/6.txt", "fyve")); + + int count = repo.Index.Count; + + Assert.True(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "2"))); + repo.Index.Remove("2", false); + + Assert.Equal(count - 5, repo.Index.Count); + } + } + + [Fact] + public void CanRemoveAFolderThroughUsageOfPathspecsForFilesAlreadyInTheIndexAndInTheHEAD() + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + Assert.True(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "1"))); + repo.Index.Remove("1"); + + Assert.False(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "1"))); + Assert.Equal(count - 1, repo.Index.Count); + } + } + + [Theory] + [InlineData("deleted_staged_file.txt", FileStatus.Removed)] + [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)] + public void RemovingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(string relativePath, FileStatus status) + { + for (int i = 0; i < 2; i++) + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Null(repo.Index[relativePath]); + Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + + repo.Index.Remove(relativePath, i % 2 == 0); + repo.Index.Remove(relativePath, i % 2 == 0, + new ExplicitPathsOptions {ShouldFailOnUnmatchedPath = false}); + } + } + } + + [Theory] + [InlineData("deleted_staged_file.txt", FileStatus.Removed)] + [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)] + public void RemovingAnUnknownFileThrowsIfExplicitPath(string relativePath, FileStatus status) + { + for (int i = 0; i < 2; i++) + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Null(repo.Index[relativePath]); + Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + + Assert.Throws( + () => repo.Index.Remove(relativePath, i%2 == 0, new ExplicitPathsOptions())); + } + } + } + + [Fact] + public void RemovingFileWithBadParamsThrows() + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Throws(() => repo.Index.Remove(string.Empty)); + Assert.Throws(() => repo.Index.Remove((string)null)); + Assert.Throws(() => repo.Index.Remove(new string[] { })); + Assert.Throws(() => repo.Index.Remove(new string[] { null })); + } + } + } +} diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index 807783e77..1ced05e7d 100644 --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs +++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs @@ -79,6 +79,11 @@ protected string CloneStandardTestRepo() return Clone(StandardTestRepoWorkingDirPath); } + protected string CloneMergedTestRepo() + { + return Clone(MergedTestRepoWorkingDirPath); + } + public string CloneSubmoduleTestRepo() { var submodule = Path.Combine(ResourcesDirectory.FullName, "submodule_wd"); @@ -192,7 +197,7 @@ private static RepositoryOptions BuildFakeRepositoryOptions(SelfCleaningDirector }; } - protected void Touch(string parent, string file, string content = null) + protected string Touch(string parent, string file, string content = null) { var lastIndex = file.LastIndexOf('/'); if (lastIndex > 0) @@ -203,6 +208,8 @@ protected void Touch(string parent, string file, string content = null) var filePath = Path.Combine(parent, file); File.AppendAllText(filePath, content ?? string.Empty, Encoding.ASCII); + + return file; } } } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 022f82432..0ae1895a0 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -483,10 +483,9 @@ internal static extern int git_index_open( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath indexpath); [DllImport(libgit2)] - internal static extern int git_index_remove( + internal static extern int git_index_remove_bypath( IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path, - int stage); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_index_write(IndexSafeHandle index); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index e2d39883a..921702c03 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -758,11 +758,11 @@ public static IndexSafeHandle git_index_open(FilePath indexpath) } } - public static void git_index_remove(IndexSafeHandle index, FilePath path, int stage) + public static void git_index_remove_bypath(IndexSafeHandle index, FilePath path) { using (ThreadAffinity()) { - int res = NativeMethods.git_index_remove(index, path, stage); + int res = NativeMethods.git_index_remove_bypath(index, path); Ensure.ZeroResult(res); } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 9b8c3c465..c5a74e328 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; @@ -292,89 +293,133 @@ public virtual void Move(IEnumerable sourcePaths, IEnumerable de } /// - /// Removes a file from the working directory and promotes the removal to the staging area. + /// Removes a file from the staging area, and optionally removes it from the working directory as well. /// /// If the file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// + /// + /// The default behavior is to remove the file from the working directory as well. + /// + /// + /// When not passing a , the passed path will be treated as + /// a pathspec. You can for example use it to pass the relative path to a folder inside the working directory, + /// so that all files beneath this folders, and the folder itself, will be removed. + /// /// /// The path of the file within the working directory. - public virtual void Remove(string path) + /// True to remove the file from the working directory, False otherwise. + /// + /// If set, the passed will be treated as an explicit path. + /// Use these options to determine how unmatched explicit paths should be handled. + /// + public virtual void Remove(string path, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); - Remove(new[] { path }); + Remove(new[] { path }, removeFromWorkingDirectory, explicitPathsOptions); } /// - /// Removes a collection of files from the working directory and promotes the removal to the staging area. + /// Removes a collection of fileS from the staging, and optionally removes them from the working directory as well. /// /// If a file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// + /// + /// The default behavior is to remove the files from the working directory as well. + /// + /// + /// When not passing a , the passed paths will be treated as + /// a pathspec. You can for example use it to pass the relative paths to folders inside the working directory, + /// so that all files beneath these folders, and the folders themselves, will be removed. + /// /// /// The collection of paths of the files within the working directory. - public virtual void Remove(IEnumerable paths) + /// True to remove the files from the working directory, False otherwise. + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// + public virtual void Remove(IEnumerable paths, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { - //TODO: Remove() should support following use cases: - // - Removing a directory and its content + var pathsList = paths.ToList(); + TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUnmodified | DiffOptions.IncludeUntracked, pathsList, explicitPathsOptions); - IEnumerable> batch = PrepareBatch(paths); + var pathsTodelete = pathsList.Where(p => Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, p))).ToList(); - foreach (KeyValuePair keyValuePair in batch) + foreach (var treeEntryChanges in changes) { - if (Directory.Exists(keyValuePair.Key)) - { - throw new NotImplementedException(); - } + var status = repo.Index.RetrieveStatus(treeEntryChanges.Path); - if (!keyValuePair.Value.HasAny(new[] { FileStatus.Nonexistent, FileStatus.Removed, FileStatus.Modified, FileStatus.Untracked })) + switch (treeEntryChanges.Status) { - continue; - } + case ChangeKind.Added: + case ChangeKind.Deleted: + pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); + break; - throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", keyValuePair.Key, keyValuePair.Value)); - } + case ChangeKind.Unmodified: + if (removeFromWorkingDirectory && ( + status.HasFlag(FileStatus.Staged) || + status.HasFlag(FileStatus.Added))) + { + throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has changes staged in the index. You can call the Remove() method with removeFromWorkingDirectory=false if you want to remove it from the index only.", + treeEntryChanges.Path)); + } + pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); + continue; - string wd = repo.Info.WorkingDirectory; - foreach (KeyValuePair keyValuePair in batch) - { - RemoveFromIndex(keyValuePair.Key); + case ChangeKind.Modified: + if (status.HasFlag(FileStatus.Modified) && status.HasFlag(FileStatus.Staged)) + { + throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has staged content different from both the working directory and the HEAD.", + treeEntryChanges.Path)); + } + if (removeFromWorkingDirectory) + { + throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}', as it has local modifications. You can call the Remove() method with removeFromWorkingDirectory=false if you want to remove it from the index only.", + treeEntryChanges.Path)); + } + pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); + continue; - if (File.Exists(Path.Combine(wd, keyValuePair.Key))) - { - File.Delete(Path.Combine(wd, keyValuePair.Key)); + + default: + throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", + treeEntryChanges.Path, treeEntryChanges.Status)); } } + if (removeFromWorkingDirectory) + { + RemoveFilesAndFolders(pathsTodelete); + } + UpdatePhysicalIndex(); } - private IEnumerable> PrepareBatch(IEnumerable paths) + private void RemoveFilesAndFolders(IEnumerable pathsList) { - Ensure.ArgumentNotNull(paths, "paths"); - - IDictionary dic = new Dictionary(); + string wd = repo.Info.WorkingDirectory; - foreach (string path in paths) + foreach (string path in pathsList) { - if (string.IsNullOrEmpty(path)) + string fileName = Path.Combine(wd, path); + + if (Directory.Exists(fileName)) { - throw new ArgumentException("At least one provided path is either null or empty.", "paths"); + Directory.Delete(fileName, true); + continue; } - string relativePath = repo.BuildRelativePathFrom(path); - FileStatus fileStatus = RetrieveStatus(relativePath); - - dic.Add(relativePath, fileStatus); - } + if (!File.Exists(fileName)) + { + continue; + } - if (dic.Count == 0) - { - throw new ArgumentException("No path has been provided.", "paths"); + File.Delete(fileName); } - - return dic; } private IDictionary, Tuple> PrepareBatch(IEnumerable leftPaths, IEnumerable rightPaths) @@ -421,9 +466,11 @@ private void AddToIndex(string relativePath) } } - private void RemoveFromIndex(string relativePath) + private string RemoveFromIndex(string relativePath) { - Proxy.git_index_remove(handle, relativePath, 0); + Proxy.git_index_remove_bypath(handle, relativePath); + + return relativePath; } private void UpdatePhysicalIndex() diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index e377fd389..d9c843406 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -69,6 +69,7 @@ + diff --git a/LibGit2Sharp/RemoveFromIndexException.cs b/LibGit2Sharp/RemoveFromIndexException.cs new file mode 100644 index 000000000..deb044b70 --- /dev/null +++ b/LibGit2Sharp/RemoveFromIndexException.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Serialization; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// The exception that is thrown when a file cannot be removed from the index. + /// + [Serializable] + public class RemoveFromIndexException : LibGit2SharpException + { + /// + /// Initializes a new instance of the class. + /// + public RemoveFromIndexException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message that describes the error. + public RemoveFromIndexException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. If the parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception. + public RemoveFromIndexException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class with a serialized data. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected RemoveFromIndexException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + internal RemoveFromIndexException(string message, GitErrorCode code, GitErrorCategory category) + : base(message, code, category) + { + } + } +} diff --git a/LibGit2Sharp/TreeChanges.cs b/LibGit2Sharp/TreeChanges.cs index 0571d20e0..c10798036 100644 --- a/LibGit2Sharp/TreeChanges.cs +++ b/LibGit2Sharp/TreeChanges.cs @@ -22,6 +22,7 @@ public class TreeChanges : IEnumerable private readonly List deleted = new List(); private readonly List modified = new List(); private readonly List typeChanged = new List(); + private readonly List unmodified = new List(); private int linesAdded; private int linesDeleted; @@ -37,6 +38,7 @@ private static IDictionary> Bu { ChangeKind.Deleted, (de, d) => de.deleted.Add(d) }, { ChangeKind.Added, (de, d) => de.added.Add(d) }, { ChangeKind.TypeChanged, (de, d) => de.typeChanged.Add(d) }, + { ChangeKind.Unmodified, (de, d) => de.unmodified.Add(d) }, }; } @@ -56,7 +58,7 @@ private int PrintCallBack(GitDiffDelta delta, GitDiffRange range, GitDiffLineOri string formattedoutput = Utf8Marshaler.FromNative(content, (int)contentlen); TreeEntryChanges currentChange = AddFileChange(delta, lineorigin); - if (currentChange == null) + if (delta.Status == ChangeKind.Unmodified) { return 0; } @@ -87,11 +89,6 @@ private void AddLineChange(Changes currentChange, GitDiffLineOrigin lineOrigin) private TreeEntryChanges AddFileChange(GitDiffDelta delta, GitDiffLineOrigin lineorigin) { - if (delta.Status == ChangeKind.Unmodified) - { - return null; - } - var newFilePath = FilePathMarshaler.FromNative(delta.NewFile.Path); if (lineorigin != GitDiffLineOrigin.GIT_DIFF_LINE_FILE_HDR)