Skip to content

Add a bunch of stuffs to Index.Remove() #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
63 changes: 54 additions & 9 deletions LibGit2Sharp.Tests/ConflictFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
Expand All @@ -14,18 +15,62 @@ public static IEnumerable<object[]> 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)
{
Expand Down Expand Up @@ -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);
}
}
Expand Down
47 changes: 0 additions & 47 deletions LibGit2Sharp.Tests/IndexFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LibGit2SharpException>(() => repo.Index.Remove(filepath));
}
}

[Fact]
public void RemovingFileWithBadParamsThrows()
{
using (var repo = new Repository(StandardTestRepoPath))
{
Assert.Throws<ArgumentException>(() => repo.Index.Remove(string.Empty));
Assert.Throws<ArgumentNullException>(() => repo.Index.Remove((string)null));
Assert.Throws<ArgumentException>(() => repo.Index.Remove(new string[] { }));
Assert.Throws<ArgumentException>(() => repo.Index.Remove(new string[] { null }));
}
}

[Fact]
public void PathsOfIndexEntriesAreExpressedInNativeFormat()
{
Expand Down
1 change: 1 addition & 0 deletions LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CheckoutFixture.cs" />
<Compile Include="RemoveFixture.cs" />
<Compile Include="RemoteFixture.cs" />
<Compile Include="PushFixture.cs" />
<Compile Include="ReflogFixture.cs" />
Expand Down
187 changes: 187 additions & 0 deletions LibGit2Sharp.Tests/RemoveFixture.cs
Original file line number Diff line number Diff line change
@@ -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 <file>' works (file removed only from index).
* 'git rm <file>' 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 <file> and 'git rm --cached <file>' 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 <file>' works (removes the file from the index)
* 'git rm <file>' fails ("error: '<file>' 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 <file>' works (removes the file from the index)
* 'git rm <file>' fails ("error: '<file>' 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 <file>' works (removes the file from the index)
* 'git rm <file>' throws ("error: '<file>' 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<RemoveFromIndexException>(() => 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 <file>' and 'git rm --cached <file>' both fail ("error: '<file>' 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<RemoveFromIndexException>(() => repo.Index.Remove(filename));
Assert.Throws<RemoveFromIndexException>(() => 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<UnmatchedPathException>(
() => repo.Index.Remove(relativePath, i%2 == 0, new ExplicitPathsOptions()));
}
}
}

[Fact]
public void RemovingFileWithBadParamsThrows()
{
using (var repo = new Repository(StandardTestRepoPath))
{
Assert.Throws<ArgumentException>(() => repo.Index.Remove(string.Empty));
Assert.Throws<ArgumentNullException>(() => repo.Index.Remove((string)null));
Assert.Throws<ArgumentException>(() => repo.Index.Remove(new string[] { }));
Assert.Throws<ArgumentException>(() => repo.Index.Remove(new string[] { null }));
}
}
}
}
9 changes: 8 additions & 1 deletion LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
}
}
5 changes: 2 additions & 3 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Loading