diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 947622363..0b926318d 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -373,5 +373,109 @@ public void CanClearTheIndex() Assert.Equal(FileStatus.Removed | FileStatus.Untracked, repo.RetrieveStatus(testFile)); } } + + [Theory] + [InlineData("new_tracked_file.txt", FileStatus.Added, FileStatus.Untracked)] + [InlineData("modified_staged_file.txt", FileStatus.Staged, FileStatus.Removed | FileStatus.Untracked)] + [InlineData("i_dont_exist.txt", FileStatus.Nonexistent, FileStatus.Nonexistent)] + public void CanRemoveAnEntryFromTheIndex(string pathInTheIndex, FileStatus expectedBeforeStatus, FileStatus expectedAfterStatus) + { + var path = SandboxStandardTestRepoGitDir(); + using (var repo = new Repository(path)) + { + var before = repo.RetrieveStatus(pathInTheIndex); + Assert.Equal(expectedBeforeStatus, before); + + repo.Index.Remove(pathInTheIndex); + + var after = repo.RetrieveStatus(pathInTheIndex); + Assert.Equal(expectedAfterStatus, after); + } + } + + [Theory] + [InlineData("new_untracked_file.txt", FileStatus.Untracked, FileStatus.Added)] + [InlineData("modified_unstaged_file.txt", FileStatus.Modified, FileStatus.Staged)] + public void CanAddAnEntryToTheIndexFromAFileInTheWorkdir(string pathInTheWorkdir, FileStatus expectedBeforeStatus, FileStatus expectedAfterStatus) + { + var path = SandboxStandardTestRepoGitDir(); + using (var repo = new Repository(path)) + { + var before = repo.RetrieveStatus(pathInTheWorkdir); + Assert.Equal(expectedBeforeStatus, before); + + repo.Index.Add(pathInTheWorkdir); + + var after = repo.RetrieveStatus(pathInTheWorkdir); + Assert.Equal(expectedAfterStatus, after); + } + } + + [Fact] + public void CanAddAnEntryToTheIndexFromABlob() + { + var path = SandboxStandardTestRepoGitDir(); + using (var repo = new Repository(path)) + { + const string targetIndexEntryPath = "1.txt"; + var before = repo.RetrieveStatus(targetIndexEntryPath); + Assert.Equal(FileStatus.Unaltered, before); + + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); + + repo.Index.Add(blob, targetIndexEntryPath, Mode.NonExecutableFile); + + var after = repo.RetrieveStatus(targetIndexEntryPath); + Assert.Equal(FileStatus.Staged | FileStatus.Modified, after); + } + } + + [Fact] + public void AddingAnEntryToTheIndexFromAUnknwonFileInTheWorkdirThrows() + { + var path = SandboxStandardTestRepoGitDir(); + using (var repo = new Repository(path)) + { + const string filePath = "i_dont_exist.txt"; + var before = repo.RetrieveStatus(filePath); + Assert.Equal(FileStatus.Nonexistent, before); + + Assert.Throws(() => repo.Index.Add(filePath)); + } + } + + [Fact] + public void CanMimicGitAddAll() + { + var path = SandboxStandardTestRepoGitDir(); + using (var repo = new Repository(path)) + { + var before = repo.RetrieveStatus(); + Assert.True(before.Any(se => se.State == FileStatus.Untracked)); + Assert.True(before.Any(se => se.State == FileStatus.Modified)); + Assert.True(before.Any(se => se.State == FileStatus.Missing)); + + AddSomeCornerCases(repo); + + repo.Stage("*"); + + var after = repo.RetrieveStatus(); + Assert.False(after.Any(se => se.State == FileStatus.Untracked)); + Assert.False(after.Any(se => se.State == FileStatus.Modified)); + Assert.False(after.Any(se => se.State == FileStatus.Missing)); + } + } + + private static void AddSomeCornerCases(Repository repo) + { + // Turn 1.txt into a directory in the Index + repo.Index.Remove("1.txt"); + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); + repo.Index.Add(blob, "1.txt/Sneaky", Mode.NonExecutableFile); + + // Turn README into a symlink + Blob linkContent = OdbHelper.CreateBlob(repo, "1.txt/sneaky"); + repo.Index.Add(linkContent, "README", Mode.SymbolicLink); + } } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index 714a49fb8..3c29db985 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -157,11 +157,75 @@ public virtual void Clear() UpdatePhysicalIndex(); } - private string RemoveFromIndex(string relativePath) + private void RemoveFromIndex(string relativePath) { Proxy.git_index_remove_bypath(handle, relativePath); + } + + /// + /// Removes a specified entry from the index. + /// + /// The path of the entry to be removed. + public virtual void Remove(string indexEntryPath) + { + if (indexEntryPath == null) + { + throw new ArgumentNullException("indexEntryPath"); + } + + RemoveFromIndex(indexEntryPath); + + UpdatePhysicalIndex(); + } + + /// + /// Adds a file from the workdir in the . + /// + /// If an entry with the same path already exists in the , + /// the newly added one will overwrite it. + /// + /// + /// The path, in the working directory, of the file to be added. + public virtual void Add(string pathInTheWorkdir) + { + if (pathInTheWorkdir == null) + { + throw new ArgumentNullException("pathInTheWorkdir"); + } - return relativePath; + Proxy.git_index_add_bypath(handle, pathInTheWorkdir); + + UpdatePhysicalIndex(); + } + + /// + /// Adds an entry in the from a . + /// + /// If an entry with the same path already exists in the , + /// the newly added one will overwrite it. + /// + /// + /// The which content should be added to the . + /// The path to be used in the . + /// Either , + /// or . + public virtual void Add(Blob blob, string indexEntryPath, Mode indexEntryMode) + { + Ensure.ArgumentConformsTo(indexEntryMode, m => m.HasAny(TreeEntryDefinition.BlobModes), "indexEntryMode"); + + if (blob == null) + { + throw new ArgumentNullException("blob"); + } + + if (indexEntryPath == null) + { + throw new ArgumentNullException("indexEntryPath"); + } + + AddEntryToTheIndex(indexEntryPath, blob.Id, indexEntryMode); + + UpdatePhysicalIndex(); } private void UpdatePhysicalIndex() @@ -185,7 +249,11 @@ internal void Replace(TreeChanges changes) case ChangeKind.Deleted: /* Fall through */ case ChangeKind.Modified: - ReplaceIndexEntryWith(treeEntryChanges); + AddEntryToTheIndex( + treeEntryChanges.OldPath, + treeEntryChanges.OldOid, + treeEntryChanges.OldMode); + continue; default: @@ -207,13 +275,13 @@ public virtual ConflictCollection Conflicts } } - private void ReplaceIndexEntryWith(TreeEntryChanges treeEntryChanges) + private void AddEntryToTheIndex(string path, ObjectId id, Mode mode) { var indexEntry = new GitIndexEntry { - Mode = (uint)treeEntryChanges.OldMode, - Id = treeEntryChanges.OldOid.Oid, - Path = StrictFilePathMarshaler.FromManaged(treeEntryChanges.OldPath), + Mode = (uint)mode, + Id = id.Oid, + Path = StrictFilePathMarshaler.FromManaged(path), }; Proxy.git_index_add(handle, indexEntry); diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 363d89fd4..a26827538 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -1493,28 +1493,41 @@ public void Stage(IEnumerable paths, StageOptions stageOptions) diffModifiers |= DiffModifiers.IncludeIgnored; } - var changes = Diff.Compare(diffModifiers, paths, explicitPathsOptions); + var changes = Diff.Compare(diffModifiers, paths, explicitPathsOptions, + new CompareOptions { Similarity = SimilarityOptions.None }); - foreach (var treeEntryChanges in changes) + var unexpectedTypesOfChanges = changes + .Where( + tec => tec.Status != ChangeKind.Added && + tec.Status != ChangeKind.Modified && + tec.Status != ChangeKind.Unmodified && + tec.Status != ChangeKind.Deleted).ToList(); + + if (unexpectedTypesOfChanges.Count > 0) { - switch (treeEntryChanges.Status) - { - case ChangeKind.Unmodified: - continue; + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, + "Entry '{0}' bears an unexpected ChangeKind '{1}'", + unexpectedTypesOfChanges[0].Path, unexpectedTypesOfChanges[0].Status)); + } - case ChangeKind.Deleted: - RemoveFromIndex(treeEntryChanges.Path); - continue; + foreach (TreeEntryChanges treeEntryChanges in changes + .Where(tec => tec.Status == ChangeKind.Deleted)) + { + RemoveFromIndex(treeEntryChanges.Path); + } + foreach (TreeEntryChanges treeEntryChanges in changes) + { + switch (treeEntryChanges.Status) + { case ChangeKind.Added: - /* Fall through */ case ChangeKind.Modified: AddToIndex(treeEntryChanges.Path); - continue; + break; default: - throw new InvalidOperationException( - string.Format(CultureInfo.InvariantCulture, "Entry '{0}' bears an unexpected ChangeKind '{1}'", treeEntryChanges.Path, treeEntryChanges.Status)); + continue; } }