From 2a2ad53a90a127e3c839849c967840f794ca000b Mon Sep 17 00:00:00 2001 From: yorah Date: Tue, 23 Apr 2013 10:55:07 +0200 Subject: [PATCH 01/11] Add RemoveFromIndexException --- LibGit2Sharp/LibGit2Sharp.csproj | 1 + LibGit2Sharp/RemoveFromIndexException.cs | 54 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 LibGit2Sharp/RemoveFromIndexException.cs 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) + { + } + } +} From b57055ec2bc20d08aebd444d9e3dac8e5a73a62d Mon Sep 17 00:00:00 2001 From: yorah Date: Thu, 18 Apr 2013 17:34:32 +0200 Subject: [PATCH 02/11] Make Index.Remove() diff-based Note: it means that the paths passed to Remove() are pathspecs, and thus behavior has been aligned with Stage()/Unstage() => calling Remove() by passing a pathspec which doesn't match any paths doesn't throw anymore (breaking change). --- LibGit2Sharp.Tests/IndexFixture.cs | 9 ++--- LibGit2Sharp/Index.cs | 59 +++++++++--------------------- LibGit2Sharp/TreeChanges.cs | 9 ++--- 3 files changed, 23 insertions(+), 54 deletions(-) diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 3e004ff5b..49c0f4513 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -217,15 +217,12 @@ public void CanRemoveAFile(string filename, FileStatus initialStatus, bool shoul } } - [Theory] - [InlineData("deleted_staged_file.txt")] - [InlineData("modified_unstaged_file.txt")] - [InlineData("shadowcopy_of_an_unseen_ghost.txt")] - public void RemovingAInvalidFileThrows(string filepath) + [Fact] + public void RemovingAModifiedFileThrows() { using (var repo = new Repository(StandardTestRepoPath)) { - Assert.Throws(() => repo.Index.Remove(filepath)); + Assert.Throws(() => repo.Index.Remove("modified_unstaged_file.txt")); } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 9b8c3c465..5eb14b618 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; @@ -319,64 +320,38 @@ public virtual void Remove(IEnumerable paths) //TODO: Remove() should support following use cases: // - Removing a directory and its content - IEnumerable> batch = PrepareBatch(paths); + var pathsList = paths.ToList(); + TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUnmodified | DiffOptions.IncludeUntracked, pathsList); - foreach (KeyValuePair keyValuePair in batch) + foreach (var treeEntryChanges in changes) { - if (Directory.Exists(keyValuePair.Key)) + switch (treeEntryChanges.Status) { - throw new NotImplementedException(); - } + case ChangeKind.Added: + case ChangeKind.Deleted: + case ChangeKind.Unmodified: + RemoveFromIndex(treeEntryChanges.Path); + continue; - if (!keyValuePair.Value.HasAny(new[] { FileStatus.Nonexistent, FileStatus.Removed, FileStatus.Modified, FileStatus.Untracked })) - { - continue; + default: + throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", + treeEntryChanges.Path, treeEntryChanges.Status)); } - - throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", keyValuePair.Key, keyValuePair.Value)); } string wd = repo.Info.WorkingDirectory; - foreach (KeyValuePair keyValuePair in batch) + foreach (string path in pathsList.Where(p => !Directory.Exists(p))) { - RemoveFromIndex(keyValuePair.Key); - - if (File.Exists(Path.Combine(wd, keyValuePair.Key))) + string fullPath = Path.Combine(wd, path); + if (File.Exists(fullPath)) { - File.Delete(Path.Combine(wd, keyValuePair.Key)); + File.Delete(fullPath); } } UpdatePhysicalIndex(); } - private IEnumerable> PrepareBatch(IEnumerable paths) - { - Ensure.ArgumentNotNull(paths, "paths"); - - IDictionary dic = new Dictionary(); - - foreach (string path in paths) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("At least one provided path is either null or empty.", "paths"); - } - - string relativePath = repo.BuildRelativePathFrom(path); - FileStatus fileStatus = RetrieveStatus(relativePath); - - dic.Add(relativePath, fileStatus); - } - - if (dic.Count == 0) - { - throw new ArgumentException("No path has been provided.", "paths"); - } - - return dic; - } - private IDictionary, Tuple> PrepareBatch(IEnumerable leftPaths, IEnumerable rightPaths) { IDictionary, Tuple> dic = new Dictionary, Tuple>(); 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) From 60e5f6688306389feaa303a6bdd68d5fef74c078 Mon Sep 17 00:00:00 2001 From: yorah Date: Thu, 18 Apr 2013 17:47:50 +0200 Subject: [PATCH 03/11] Allow passing ExplicitPathsOptions to Index.Remove() This makes the behavior fully consistent with Index.Stage()/Unstage(). --- LibGit2Sharp.Tests/IndexFixture.cs | 29 +++++++++++++++++++++++++++++ LibGit2Sharp/Index.cs | 16 ++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 49c0f4513..ed7cb196b 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -217,6 +217,35 @@ public void CanRemoveAFile(string filename, FileStatus initialStatus, bool shoul } } + [Theory] + [InlineData("deleted_staged_file.txt", FileStatus.Removed)] + [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)] + public void RemovingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(string relativePath, FileStatus status) + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Null(repo.Index[relativePath]); + Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + + repo.Index.Remove(relativePath); + repo.Index.Remove(relativePath, 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) + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Null(repo.Index[relativePath]); + Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + + Assert.Throws(() => repo.Index.Remove(relativePath, new ExplicitPathsOptions())); + } + } + [Fact] public void RemovingAModifiedFileThrows() { diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 5eb14b618..d4e093436 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -300,11 +300,15 @@ public virtual void Move(IEnumerable sourcePaths, IEnumerable de /// /// /// The path of the file within the working directory. - public virtual void Remove(string path) + /// + /// 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, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); - Remove(new[] { path }); + Remove(new[] { path }, explicitPathsOptions); } /// @@ -315,13 +319,17 @@ public virtual void Remove(string path) /// /// /// The collection of paths of the files within the working directory. - public virtual void Remove(IEnumerable paths) + /// + /// 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, 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); + TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUnmodified | DiffOptions.IncludeUntracked, pathsList, explicitPathsOptions); foreach (var treeEntryChanges in changes) { From 00e40d8ba3f9a1d2f347faa4f2da15f5d7e09281 Mon Sep 17 00:00:00 2001 From: yorah Date: Thu, 18 Apr 2013 18:07:51 +0200 Subject: [PATCH 04/11] Make Index.Remove() able to remove folders Fixes #327 --- LibGit2Sharp.Tests/IndexFixture.cs | 22 ++++++++ LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs | 4 +- LibGit2Sharp/Index.cs | 51 +++++++++++++++---- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index ed7cb196b..4fadda2c3 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -194,6 +194,28 @@ private static void InvalidMoveUseCases(string sourcePath, FileStatus sourceStat } } + [Fact] + public void CanRemoveAFolderThroughUsageOfPathspecs() + { + 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"); + + Assert.False(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "2"))); + Assert.Equal(count - 5, repo.Index.Count); + } + } + [Theory] [InlineData("1/branch_file.txt", FileStatus.Unaltered, true, FileStatus.Removed)] [InlineData("deleted_unstaged_file.txt", FileStatus.Missing, false, FileStatus.Removed)] diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index 807783e77..ddb1182e1 100644 --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs +++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs @@ -192,7 +192,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 +203,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/Index.cs b/LibGit2Sharp/Index.cs index d4e093436..7f225b54f 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -298,6 +298,11 @@ public virtual void Move(IEnumerable sourcePaths, IEnumerable de /// If the file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// + /// + /// 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. /// @@ -317,6 +322,11 @@ public virtual void Remove(string path, ExplicitPathsOptions explicitPathsOption /// If a file has already been deleted from the working directory, this method will only deal /// with promoting the removal to the staging area. /// + /// + /// 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. /// @@ -325,20 +335,22 @@ public virtual void Remove(string path, ExplicitPathsOptions explicitPathsOption /// public virtual void Remove(IEnumerable paths, 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); + var pathsTodelete = pathsList.Where(p => Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, p))).ToList(); + foreach (var treeEntryChanges in changes) { switch (treeEntryChanges.Status) { case ChangeKind.Added: case ChangeKind.Deleted: + pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); + break; + case ChangeKind.Unmodified: - RemoveFromIndex(treeEntryChanges.Path); + pathsTodelete.Add(RemoveFromIndex(treeEntryChanges.Path)); continue; default: @@ -347,17 +359,32 @@ public virtual void Remove(IEnumerable paths, ExplicitPathsOptions expli } } + RemoveFilesAndFolders(pathsTodelete); + + UpdatePhysicalIndex(); + } + + private void RemoveFilesAndFolders(IEnumerable pathsList) + { string wd = repo.Info.WorkingDirectory; - foreach (string path in pathsList.Where(p => !Directory.Exists(p))) + + foreach (string path in pathsList) { - string fullPath = Path.Combine(wd, path); - if (File.Exists(fullPath)) + string fileName = Path.Combine(wd, path); + + if (Directory.Exists(fileName)) { - File.Delete(fullPath); + Directory.Delete(fileName, true); + continue; } - } - UpdatePhysicalIndex(); + if (!File.Exists(fileName)) + { + continue; + } + + File.Delete(fileName); + } } private IDictionary, Tuple> PrepareBatch(IEnumerable leftPaths, IEnumerable rightPaths) @@ -404,9 +431,11 @@ private void AddToIndex(string relativePath) } } - private void RemoveFromIndex(string relativePath) + private string RemoveFromIndex(string relativePath) { Proxy.git_index_remove(handle, relativePath, 0); + + return relativePath; } private void UpdatePhysicalIndex() From 0d4bc09fb2b610378538f4b23e5cd4079a5cba4a Mon Sep 17 00:00:00 2001 From: yorah Date: Thu, 18 Apr 2013 18:16:28 +0200 Subject: [PATCH 05/11] Make Index.Remove() able to remove only from index Fixes #270 --- LibGit2Sharp.Tests/IndexFixture.cs | 25 ++++++++++++++++++++++++- LibGit2Sharp/Index.cs | 21 ++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 4fadda2c3..c3e2bd72d 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -216,6 +216,29 @@ public void CanRemoveAFolderThroughUsageOfPathspecs() } } + [Fact] + public void CanRemoveAFileWithoutRemovingItFromTheWorkingDirectory() + { + const string fileName = "1/branch_file.txt"; + + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, fileName); + + Assert.Equal(FileStatus.Unaltered, repo.Index.RetrieveStatus(fileName)); + Assert.True(File.Exists(fullpath)); + + repo.Index.Remove(fileName, false); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.True(File.Exists(fullpath)); + Assert.Equal(FileStatus.Untracked | FileStatus.Removed, repo.Index.RetrieveStatus(fileName)); + } + } + [Theory] [InlineData("1/branch_file.txt", FileStatus.Unaltered, true, FileStatus.Removed)] [InlineData("deleted_unstaged_file.txt", FileStatus.Missing, false, FileStatus.Removed)] @@ -250,7 +273,7 @@ public void RemovingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(strin Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); repo.Index.Remove(relativePath); - repo.Index.Remove(relativePath, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); + repo.Index.Remove(relativePath, explicitPathsOptions: new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 7f225b54f..485944da6 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -293,47 +293,55 @@ 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. + /// 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, ExplicitPathsOptions explicitPathsOptions = null) + public virtual void Remove(string path, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); - Remove(new[] { path }, explicitPathsOptions); + 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. + /// 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, ExplicitPathsOptions explicitPathsOptions = null) + public virtual void Remove(IEnumerable paths, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { var pathsList = paths.ToList(); TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUnmodified | DiffOptions.IncludeUntracked, pathsList, explicitPathsOptions); @@ -359,7 +367,10 @@ public virtual void Remove(IEnumerable paths, ExplicitPathsOptions expli } } + if (removeFromWorkingDirectory) + { RemoveFilesAndFolders(pathsTodelete); + } UpdatePhysicalIndex(); } From ec9b617a29b01d43bc80c63fa9eb48daaac74155 Mon Sep 17 00:00:00 2001 From: yorah Date: Fri, 19 Apr 2013 11:41:01 +0200 Subject: [PATCH 06/11] Improve support for git rm --cached semantics Lots of test cases added, to mimic git rm --cached behavior. --- LibGit2Sharp.Tests/IndexFixture.cs | 215 +++++++++++++++++++++++++++-- LibGit2Sharp/Index.cs | 26 +++- 2 files changed, 226 insertions(+), 15 deletions(-) diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index c3e2bd72d..d867fdb8d 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -195,7 +195,7 @@ private static void InvalidMoveUseCases(string sourcePath, FileStatus sourceStat } [Fact] - public void CanRemoveAFolderThroughUsageOfPathspecs() + public void CanRemoveAFolderThroughUsageOfPathspecsForNewlyAddedFiles() { string path = CloneStandardTestRepo(); using (var repo = new Repository(path)) @@ -209,15 +209,30 @@ public void CanRemoveAFolderThroughUsageOfPathspecs() int count = repo.Index.Count; Assert.True(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "2"))); - repo.Index.Remove("2"); + repo.Index.Remove("2", false); - Assert.False(Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, "2"))); Assert.Equal(count - 5, repo.Index.Count); } } [Fact] - public void CanRemoveAFileWithoutRemovingItFromTheWorkingDirectory() + 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); + } + } + + [Fact] + public void CanRemoveAnUnalteredFileFromTheIndexWithoutRemovingItFromTheWorkingDirectory() { const string fileName = "1/branch_file.txt"; @@ -267,13 +282,17 @@ public void CanRemoveAFile(string filename, FileStatus initialStatus, bool shoul [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)] public void RemovingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(string relativePath, FileStatus status) { - using (var repo = new Repository(StandardTestRepoPath)) + for (int i = 0; i < 2; i++) { - Assert.Null(repo.Index[relativePath]); - Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Null(repo.Index[relativePath]); + Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); - repo.Index.Remove(relativePath); - repo.Index.Remove(relativePath, explicitPathsOptions: new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); + repo.Index.Remove(relativePath, i % 2 == 0); + repo.Index.Remove(relativePath, i % 2 == 0, + new ExplicitPathsOptions {ShouldFailOnUnmatchedPath = false}); + } } } @@ -281,22 +300,190 @@ public void RemovingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(strin [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())); + } + } + } + + /* Test case: modified file in wd, the modifications have not been promoted to the index yet. + * 'git rm ' fails ("error: '' has local modifications"). + */ + [Fact] + public void RemovingAModifiedFileWhoseChangesHaveNotBeenPromotedToTheIndexThrows() { using (var repo = new Repository(StandardTestRepoPath)) { - Assert.Null(repo.Index[relativePath]); - Assert.Equal(status, repo.Index.RetrieveStatus(relativePath)); + Assert.Throws(() => repo.Index.Remove("modified_unstaged_file.txt")); + } + } - Assert.Throws(() => repo.Index.Remove(relativePath, new ExplicitPathsOptions())); + /* 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) + */ + [Fact] + public void CanRemoveAModifiedFileWhoseChangesHaveNotBeenPromotedToTheIndex() + { + const string filename = "modified_unstaged_file.txt"; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(true, File.Exists(fullpath)); + Assert.Equal(FileStatus.Modified, repo.Index.RetrieveStatus(filename)); + + repo.Index.Remove(filename, false); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.True(File.Exists(fullpath)); + Assert.Equal(FileStatus.Untracked | FileStatus.Removed, repo.Index.RetrieveStatus(filename)); } } + /* Test case: modified file in wd, the modifications have already been promoted to the index. + * 'git rm ' fails ("error: '' has changes staged in the index") + */ [Fact] - public void RemovingAModifiedFileThrows() + public void RemovingAModifiedFileWhoseChangesHaveBeenPromotedToTheIndexThrows() { using (var repo = new Repository(StandardTestRepoPath)) { - Assert.Throws(() => repo.Index.Remove("modified_unstaged_file.txt")); + Assert.Throws(() => repo.Index.Remove("modified_staged_file.txt")); + } + } + + /* 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) + */ + [Fact] + public void CanRemoveAModifiedFileWhoseChangesHaveBeenPromotedToTheIndex() + { + const string filename = "modified_staged_file.txt"; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(true, File.Exists(fullpath)); + Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus(filename)); + + repo.Index.Remove(filename, false); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.True(File.Exists(fullpath)); + Assert.Equal(FileStatus.Untracked | FileStatus.Removed, 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)); + } + } + + /* 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 ' throws ("error: '' has changes staged in the index") + */ + [Fact] + public void RemovingANewlyAddedFileThrows() + { + const string filename = "new_tracked_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)); + Assert.Equal(FileStatus.Added, repo.Index.RetrieveStatus(filename)); + + Assert.Throws(() => repo.Index.Remove(filename)); + } + } + + /* 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) + */ + [Fact] + public void CanRemoveANewlyAddedFile() + { + const string filename = "new_tracked_file.txt"; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); + + Assert.Equal(true, File.Exists(fullpath)); + Assert.Equal(FileStatus.Added, repo.Index.RetrieveStatus(filename)); + + repo.Index.Remove(filename, false); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.True(File.Exists(fullpath)); + Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(filename)); + } + } + + /* Test case: file exists in the index, and has been removed from the wd. + * 'git rm and 'git rm --cached ' both work (remove the file from the index) + */ + [Fact] + public void CanRemoveAFileAlreadyDeletedFromTheWorkdir() + { + const string filename = "deleted_unstaged_file.txt"; + + for (int i = 0; i < 2; i++) + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + int count = repo.Index.Count; + + Assert.Equal(FileStatus.Missing, repo.Index.RetrieveStatus(filename)); + + repo.Index.Remove(filename, i % 2 == 0); + + Assert.Equal(count - 1, repo.Index.Count); + Assert.Equal(FileStatus.Removed, repo.Index.RetrieveStatus(filename)); + } } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 485944da6..c881a62f0 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -350,6 +350,8 @@ public virtual void Remove(IEnumerable paths, bool removeFromWorkingDire foreach (var treeEntryChanges in changes) { + var status = repo.Index.RetrieveStatus(treeEntryChanges.Path); + switch (treeEntryChanges.Status) { case ChangeKind.Added: @@ -358,11 +360,33 @@ public virtual void Remove(IEnumerable paths, bool removeFromWorkingDire break; 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; + + 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; + default: - throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", + throw new RemoveFromIndexException(string.Format(CultureInfo.InvariantCulture, "Unable to remove file '{0}'. Its current status is '{1}'.", treeEntryChanges.Path, treeEntryChanges.Status)); } } From b7deceadc97f411accfef7f7ff302506e444fbfc Mon Sep 17 00:00:00 2001 From: yorah Date: Fri, 19 Apr 2013 17:05:35 +0200 Subject: [PATCH 07/11] Move Index.Remove() tests to a separate file --- LibGit2Sharp.Tests/IndexFixture.cs | 305 ------------------- LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj | 1 + LibGit2Sharp.Tests/RemoveFixture.cs | 187 ++++++++++++ 3 files changed, 188 insertions(+), 305 deletions(-) create mode 100644 LibGit2Sharp.Tests/RemoveFixture.cs diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index d867fdb8d..a1667dc4c 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -194,311 +194,6 @@ private static void InvalidMoveUseCases(string sourcePath, FileStatus sourceStat } } - [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); - } - } - - [Fact] - public void CanRemoveAnUnalteredFileFromTheIndexWithoutRemovingItFromTheWorkingDirectory() - { - const string fileName = "1/branch_file.txt"; - - string path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - string fullpath = Path.Combine(repo.Info.WorkingDirectory, fileName); - - Assert.Equal(FileStatus.Unaltered, repo.Index.RetrieveStatus(fileName)); - Assert.True(File.Exists(fullpath)); - - repo.Index.Remove(fileName, false); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.True(File.Exists(fullpath)); - Assert.Equal(FileStatus.Untracked | FileStatus.Removed, repo.Index.RetrieveStatus(fileName)); - } - } - - [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", 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())); - } - } - } - - /* Test case: modified file in wd, the modifications have not been promoted to the index yet. - * 'git rm ' fails ("error: '' has local modifications"). - */ - [Fact] - public void RemovingAModifiedFileWhoseChangesHaveNotBeenPromotedToTheIndexThrows() - { - using (var repo = new Repository(StandardTestRepoPath)) - { - Assert.Throws(() => repo.Index.Remove("modified_unstaged_file.txt")); - } - } - - /* 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) - */ - [Fact] - public void CanRemoveAModifiedFileWhoseChangesHaveNotBeenPromotedToTheIndex() - { - const string filename = "modified_unstaged_file.txt"; - - var path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); - - Assert.Equal(true, File.Exists(fullpath)); - Assert.Equal(FileStatus.Modified, repo.Index.RetrieveStatus(filename)); - - repo.Index.Remove(filename, false); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.True(File.Exists(fullpath)); - Assert.Equal(FileStatus.Untracked | FileStatus.Removed, repo.Index.RetrieveStatus(filename)); - } - } - - /* Test case: modified file in wd, the modifications have already been promoted to the index. - * 'git rm ' fails ("error: '' has changes staged in the index") - */ - [Fact] - public void RemovingAModifiedFileWhoseChangesHaveBeenPromotedToTheIndexThrows() - { - using (var repo = new Repository(StandardTestRepoPath)) - { - Assert.Throws(() => repo.Index.Remove("modified_staged_file.txt")); - } - } - - /* 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) - */ - [Fact] - public void CanRemoveAModifiedFileWhoseChangesHaveBeenPromotedToTheIndex() - { - const string filename = "modified_staged_file.txt"; - - var path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); - - Assert.Equal(true, File.Exists(fullpath)); - Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus(filename)); - - repo.Index.Remove(filename, false); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.True(File.Exists(fullpath)); - Assert.Equal(FileStatus.Untracked | FileStatus.Removed, 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)); - } - } - - /* 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 ' throws ("error: '' has changes staged in the index") - */ - [Fact] - public void RemovingANewlyAddedFileThrows() - { - const string filename = "new_tracked_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)); - Assert.Equal(FileStatus.Added, repo.Index.RetrieveStatus(filename)); - - Assert.Throws(() => repo.Index.Remove(filename)); - } - } - - /* 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) - */ - [Fact] - public void CanRemoveANewlyAddedFile() - { - const string filename = "new_tracked_file.txt"; - - var path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - string fullpath = Path.Combine(repo.Info.WorkingDirectory, filename); - - Assert.Equal(true, File.Exists(fullpath)); - Assert.Equal(FileStatus.Added, repo.Index.RetrieveStatus(filename)); - - repo.Index.Remove(filename, false); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.True(File.Exists(fullpath)); - Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(filename)); - } - } - - /* Test case: file exists in the index, and has been removed from the wd. - * 'git rm and 'git rm --cached ' both work (remove the file from the index) - */ - [Fact] - public void CanRemoveAFileAlreadyDeletedFromTheWorkdir() - { - const string filename = "deleted_unstaged_file.txt"; - - for (int i = 0; i < 2; i++) - { - var path = CloneStandardTestRepo(); - using (var repo = new Repository(path)) - { - int count = repo.Index.Count; - - Assert.Equal(FileStatus.Missing, repo.Index.RetrieveStatus(filename)); - - repo.Index.Remove(filename, i % 2 == 0); - - Assert.Equal(count - 1, repo.Index.Count); - Assert.Equal(FileStatus.Removed, repo.Index.RetrieveStatus(filename)); - } - } - } - - [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 })); + } + } + } +} From a95144cc8fdce419273eed59fa50364ac94396f7 Mon Sep 17 00:00:00 2001 From: yorah Date: Fri, 19 Apr 2013 17:11:44 +0200 Subject: [PATCH 08/11] Switch internal implementation of Index.Remove() to git_index_remove_bypath() --- LibGit2Sharp/Core/NativeMethods.cs | 5 ++--- LibGit2Sharp/Core/Proxy.cs | 4 ++-- LibGit2Sharp/Index.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) 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 c881a62f0..c5a74e328 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -468,7 +468,7 @@ private void AddToIndex(string relativePath) private string RemoveFromIndex(string relativePath) { - Proxy.git_index_remove(handle, relativePath, 0); + Proxy.git_index_remove_bypath(handle, relativePath); return relativePath; } From 0efb0fcfea779319a9709ed6b9396dac2142715f Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 22 Apr 2013 11:41:59 +0200 Subject: [PATCH 09/11] Cleanup ConflictFixture --- LibGit2Sharp.Tests/ConflictFixture.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/LibGit2Sharp.Tests/ConflictFixture.cs b/LibGit2Sharp.Tests/ConflictFixture.cs index d4db890b4..57485f7da 100644 --- a/LibGit2Sharp.Tests/ConflictFixture.cs +++ b/LibGit2Sharp.Tests/ConflictFixture.cs @@ -14,14 +14,14 @@ 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" }, }; } } @@ -78,7 +78,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); } } From f8d94ae9ad42a8715ad6f39e94b297913a4aa711 Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 22 Apr 2013 11:46:26 +0200 Subject: [PATCH 10/11] Add BaseFixture.CloneMergedTestRepo() --- LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index ddb1182e1..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"); From 39f894708a2bad6440f3a0b30f87bc5102f2f9bd Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 22 Apr 2013 15:32:53 +0200 Subject: [PATCH 11/11] Add test to ensure conflicts are cleared when calling Index.Remove() Beware, this is supported only for files which exist in the workdir (cf. comment in code). Partially fixes #325 --- LibGit2Sharp.Tests/ConflictFixture.cs | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/LibGit2Sharp.Tests/ConflictFixture.cs b/LibGit2Sharp.Tests/ConflictFixture.cs index 57485f7da..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; @@ -26,6 +27,50 @@ public static IEnumerable ConflictData } } + [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) {