Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add unmatched pathspecs options #343

Closed
wants to merge 3 commits into from

3 participants

@yorah

This is related to libgit2/libgit2sharp#274

  • :zzz: Implementation in progress
  • :flashlight: Ready for review
  • :8ball: more work
  • :star: Really ready for review this time

Points of attention:

  • Remove() is not included in this PR.
  • This PR includes some bug fixing related to libgit2/libgit2sharp#330 (the bugs were there before, they were just put into light by the aforementionned PR)
@yorah

As a bonus, the build is now :green_heart:

For Stage(), I had to include Unmodified records, to differentiate between:

  • a non-existent file
  • a removed file
  • an unaltered file (which acts as an unmatched pathspec when we don't include Unmodified files)
  • a file that was already added to the index (same thing)

I have the feeling I'm missing some other edge cases though, but can't figure out which ones...

LibGit2Sharp/Core/Proxy.cs
@@ -253,7 +253,7 @@ public static Signature git_commit_committer(GitObjectSafeHandle obj)
{
using (ThreadAffinity())
using (var treePtr = new ObjectSafeWrapper(tree.Id, repo))
- using (var parentObjectPtrs = new DisposableEnumerable<ObjectSafeWrapper>(parentIds.Select(id => new ObjectSafeWrapper(id, repo))))
+ using (var parentObjectPtrs = new DisposableEnumerable<ObjectSafeWrapper>(parentIds.Select(id => new ObjectSafeWrapper(id, repo)).ToArray()))
@dahlbyk Collaborator
dahlbyk added a note

Using ToList() here would avoid an extra array allocation for the final result.

Based on usage of parentObjectPtrs.Count() below, should DisposableEnumerable be converted to DisposableCollection to avoid an extra enumeration?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@yorah

Latest changes:

  • implemented @dahlbyk proposal regarding DisposableEnumerable (actually, I just exposed a Count property to avoid the extra enumeration => I did not feel like implementing the whole ICollection<T> interface as we don't need it).
  • by default, pathspec validation is now strict. This means that unless you specify otherwise, an exception will be thrown if one of the passed path/pathspec doesn't match.
@dahlbyk
Collaborator

actually, I just exposed a Count property to avoid the extra enumeration => I did not feel like implementing the whole ICollection interface as we don't need it

:+1:

@nulltoken
Owner

@yorah I cherry picked the first five commits and pushed them into vNext. Could you please rebase this PR to drop them?

by default, pathspec validation is now strict. This means that unless you specify otherwise, an exception will be thrown if one of the passed path/pathspec doesn't match.

I'm in favor of this approach. And the code looks pretty clean.

However, as this is quite a breaking change, I'd like to wait for some days to see if the other contributors have anything to say about this.

@nulltoken nulltoken referenced this pull request
Closed

Ignore Case via Repository.PathComparer #344

2 of 3 tasks complete
@yorah

Rebased and running.

As a bonus, I will also add some test coverage related to accepting globs as pathspecs for staging/unstaging. In the next few days.

@yorah

Added some tests related to accepting globs as pathspecs. Passing "*" is currently broken, waiting for feedback on libgit2/libgit2#1367.

Does anyone have an opinion related to the change of behaviour (strict vs lax)? Please? :cat:

@yorah

Proposal as of now (current code)

Only throw/notify for explicit paths (renamed the PathspecsOptions class to ExplicitPathsOptions to match what it does).

Advantages I see:

  • simple and predictable behavior
  • no breaking changes with what was previously there (if you don't pass an ExplicitPathsOptions instance, no notify/throw is done, and you get standard pathspec matching)

@dahlbyk regarding the proposal you made in libgit2/libgit2#1367, I think checking if offending pathspecs match any of the already found files in libgit2sharp is not easily feasible. This would mean either exposing the _fnmatch functionality implemented in libgit2 (which doesn't feel right), or re-implementing our own fnmatch. We would also have to take the ignore_case flag into account.

RFC

Before going further, I would like to make a request for comments.

Basically, the initial question this PR tries to address boils down to:
Is it ok to stage/unstage (and remove/move) a file which doesn't exist?

I think there are two main cases:

  • the client passes a list of pathspecs
  • the client passes a list of explicit paths
  • the client passes a mix of both (unlikely corner case, excluded on purpose => this looks like a "slippery slope"™, as the result will depend on the way the list is being sorted)

So the question can also be reformulated as:
What should be the expected behavior when a path/pathspec is unmatched?
Silently ignore? Notify? Throw?

@Haacked @dahlbyk @jamill @ethomson

@dahlbyk
Collaborator

@nulltoken would it be possible to cherry-pick 144ac99 and 5675ea0 so it's easier to read the core PR?

What should be the expected behavior when a path/pathspec is unmatched?

I'll defer to people that are actually building clients that need to Stage/Unstage.

@nulltoken
Owner

@nulltoken would it be possible to cherry-pick 144ac99 and 5675ea0 so it's easier to read the core PR?

@dahlbyk Done!

@nulltoken
Owner

@yorah Could you please rebase this PR on top of vNext?

@yorah

Could you please rebase this PR on top of vNext?

Done!

@Haacked @jamill whenever you have time, I would really appreciate your feedback.

LibGit2Sharp.Tests/StageFixture.cs
((10 lines not shown))
+ * D deleted_staged_file.txt
+ * D deleted_unstaged_file.txt
+ * M modified_staged_file.txt
+ * M modified_unstaged_file.txt
+ * M new.txt
+ * A new_tracked_file.txt
+ * ?? new_untracked_file.txt
+ *
+ * By passing "*" to Stage, the following files will be added/removed/updated from the index:
+ * - deleted_unstaged_file.txt : removed
+ * - modified_unstaged_file.txt : updated
+ * - new_untracked_file.txt : added
+ */
+ [Theory]
+ [InlineData("*u*", 0)]
+ //[InlineData("*", 0)] TODO: waiting for https://github.com/libgit2/libgit2/pull/1367
@nulltoken Owner

What's the current status of this?

@yorah
yorah added a note

Working with my latest proposal, uncommenting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
LibGit2Sharp.Tests/StageFixture.cs
((29 lines not shown))
+ [InlineData("*modified_unstaged*", 0)]
+ [InlineData("new_*file.txt", 1)]
+ public void CanStageWithPathspec(string relativePath, int expectedIndexCountVariation)
+ {
+ TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath);
+ using (var repo = new Repository(path.RepositoryPath))
+ {
+ int count = repo.Index.Count;
+
+ repo.Index.Stage(relativePath);
+
+ Assert.Equal(count + expectedIndexCountVariation, repo.Index.Count);
+ }
+ }
+
+ [Fact(Skip = "libgit2 doesn't notify of all matching pathspecs yet")]
@nulltoken Owner

What's the current status of this?

@yorah
yorah added a note

Working with my latest proposal, uncommenting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
yorah added some commits
@yorah yorah Add ExplicitPathsOptions to Repository.Diff.Compare()
All the overloads can now report and/or fail upon unmatched explicit paths.
By default, the passed list of paths will be treated as a list of pathspecs.

When an ExplicitPathsOptions is passed to the overloads, this list of paths
will be treated as explicit paths. In that case, the default behavior is to
throw when one of this explicit path is unmatched.
2027f49
@yorah yorah Add ExplicitPathsOptions to Repository.Reset() 6419588
@yorah yorah Add ExplicitPathsOptions to Repository.Index.Unstage()/Stage() 9405ac1
@nulltoken
Owner

:+1: Merged

@nulltoken nulltoken closed this
@yorah

Cool!

@dahlbyk I think this impacts your submodule PR (for the Stage() method, where we now rely on diff). Unless you beat me to it, I will have a look tomorrow to see if there is an easy way to make it work nicely with your PR!

@yorah yorah deleted the yorah:topic/unmatched-pathspecs branch
@dahlbyk dahlbyk referenced this pull request
Merged

Submodules! #312

10 of 13 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 9, 2013
  1. @yorah

    Add ExplicitPathsOptions to Repository.Diff.Compare()

    yorah authored
    All the overloads can now report and/or fail upon unmatched explicit paths.
    By default, the passed list of paths will be treated as a list of pathspecs.
    
    When an ExplicitPathsOptions is passed to the overloads, this list of paths
    will be treated as explicit paths. In that case, the default behavior is to
    throw when one of this explicit path is unmatched.
  2. @yorah
  3. @yorah
This page is out of date. Refresh to see the latest.
View
38 LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs
@@ -283,7 +283,7 @@ public void CanCompareASubsetofTheTreeAgainstTheIndex()
Tree tree = repo.Head.Tip.Tree;
TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.Index,
- new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" });
+ new[] { "deleted_staged_file.txt", "1/branch_file.txt" });
Assert.NotNull(changes);
@@ -292,6 +292,42 @@ public void CanCompareASubsetofTheTreeAgainstTheIndex()
}
}
+ private static void AssertCanCompareASubsetOfTheTreeAgainstTheIndex(TreeChanges changes)
+ {
+ Assert.NotNull(changes);
+ Assert.Equal(1, changes.Count());
+ Assert.Equal("deleted_staged_file.txt", changes.Deleted.Single().Path);
+ }
+
+ [Fact]
+ public void CanCompareASubsetofTheTreeAgainstTheIndexWithLaxExplicitPathsValidationAndANonExistentPath()
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Tree tree = repo.Head.Tip.Tree;
+
+ TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.Index,
+ new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false });
+ AssertCanCompareASubsetOfTheTreeAgainstTheIndex(changes);
+
+ changes = repo.Diff.Compare(tree, DiffTargets.Index,
+ new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" });
+ AssertCanCompareASubsetOfTheTreeAgainstTheIndex(changes);
+ }
+ }
+
+ [Fact]
+ public void ComparingASubsetofTheTreeAgainstTheIndexWithStrictExplicitPathsValidationAndANonExistentPathThrows()
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Tree tree = repo.Head.Tip.Tree;
+
+ Assert.Throws<UnmatchedPathException>(() => repo.Diff.Compare(tree, DiffTargets.Index,
+ new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }, new ExplicitPathsOptions()));
+ }
+ }
+
[Fact]
/*
* $ git init .
View
34 LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs
@@ -87,7 +87,7 @@ public void CanCompareASubsetofTheTreeAgainstOneOfItsAncestor()
Tree tree = repo.Head.Tip.Tree;
Tree ancestor = repo.Lookup<Commit>("9fd738e").Tree;
- TreeChanges changes = repo.Diff.Compare(ancestor, tree, new[]{ "1", "2/" });
+ TreeChanges changes = repo.Diff.Compare(ancestor, tree, new[]{ "1" });
Assert.NotNull(changes);
Assert.Equal(1, changes.Count());
@@ -133,6 +133,38 @@ public void CanCompareACommitTreeAgainstATreeWithNoCommonAncestor()
}
}
+ [Fact]
+ public void CanCompareATreeAgainstAnotherTreeWithLaxExplicitPathsValidationAndNonExistentPath()
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Tree commitTree = repo.Head.Tip.Tree;
+ Tree commitTreeWithDifferentAncestor = repo.Branches["refs/remotes/origin/test"].Tip.Tree;
+
+ TreeChanges changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree,
+ new[] { "if-I-exist-this-test-is-really-unlucky.txt" }, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false });
+ Assert.Equal(0, changes.Count());
+
+ changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree,
+ new[] { "if-I-exist-this-test-is-really-unlucky.txt" });
+ Assert.Equal(0, changes.Count());
+ }
+ }
+
+ [Fact]
+ public void ComparingATreeAgainstAnotherTreeWithStrictExplicitPathsValidationThrows()
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Tree commitTree = repo.Head.Tip.Tree;
+ Tree commitTreeWithDifferentAncestor = repo.Branches["refs/remotes/origin/test"].Tip.Tree;
+
+ Assert.Throws<UnmatchedPathException>(() =>
+ repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree,
+ new[] { "if-I-exist-this-test-is-really-unlucky.txt" }, new ExplicitPathsOptions()));
+ }
+ }
+
/*
* $ git diff -M f8d44d7..4be51d6
* diff --git a/my-name-does-not-feel-right.txt b/super-file.txt
View
127 LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs
@@ -1,6 +1,10 @@
+using System;
+using System.IO;
using System.Linq;
+using System.Text;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
+using Xunit.Extensions;
namespace LibGit2Sharp.Tests
{
@@ -36,6 +40,129 @@ public void CanCompareTheWorkDirAgainstTheIndex()
}
}
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("really-i-cant-exist.txt", FileStatus.Nonexistent)]
+ public void CanCompareTheWorkDirAgainstTheIndexWithLaxUnmatchedExplicitPathsValidation(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ TreeChanges changes = repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false });
+ Assert.Equal(0, changes.Count());
+
+ changes = repo.Diff.Compare(new[] { relativePath });
+ Assert.Equal(0, changes.Count());
+ }
+ }
+
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("really-i-cant-exist.txt", FileStatus.Nonexistent)]
+ public void ComparingTheWorkDirAgainstTheIndexWithStrictUnmatchedExplicitPathsValidationAndANonExistentPathspecThrows(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.Throws<UnmatchedPathException>(() => repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions()));
+ }
+ }
+
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("where-am-I.txt", FileStatus.Nonexistent)]
+ public void CallbackForUnmatchedExplicitPathsIsCalledWhenSet(string relativePath, FileStatus currentStatus)
+ {
+ var callback = new AssertUnmatchedPathspecsCallbackIsCalled();
+
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false,
+ OnUnmatchedPath = callback.OnUnmatchedPath });
+
+ Assert.True(callback.WasCalled);
+ }
+ }
+
+ private class AssertUnmatchedPathspecsCallbackIsCalled
+ {
+ public bool WasCalled;
+
+ public void OnUnmatchedPath(string unmatchedpath)
+ {
+ WasCalled = true;
+ }
+ }
+
+ [Fact]
+ public void ComparingReliesOnProvidedConfigEntriesIfAny()
+ {
+ const string file = "1/branch_file.txt";
+
+ string path = CloneStandardTestRepo();
+ using (var repo = new Repository(path))
+ {
+ TreeEntry entry = repo.Head[file];
+ Assert.Equal(Mode.ExecutableFile, entry.Mode);
+
+ // Recreate the file in the workdir without the executable bit
+ string fullpath = Path.Combine(repo.Info.WorkingDirectory, file);
+ File.Delete(fullpath);
+ File.WriteAllBytes(fullpath, ((Blob)(entry.Target)).Content);
+
+ // Unset the local core.filemode, if any.
+ repo.Config.Unset("core.filemode", ConfigurationLevel.Local);
+ }
+
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+
+ var options = BuildFakeSystemConfigFilemodeOption(scd, true);
+
+ using (var repo = new Repository(path, options))
+ {
+ TreeChanges changes = repo.Diff.Compare(new[] { file });
+
+ Assert.Equal(1, changes.Count());
+
+ var change = changes.Modified.Single();
+ Assert.Equal(Mode.ExecutableFile, change.OldMode);
+ Assert.Equal(Mode.NonExecutableFile, change.Mode);
+ }
+
+ options = BuildFakeSystemConfigFilemodeOption(scd, false);
+
+ using (var repo = new Repository(path, options))
+ {
+ TreeChanges changes = repo.Diff.Compare(new[] { file });
+
+ Assert.Equal(0, changes.Count());
+ }
+ }
+
+ private RepositoryOptions BuildFakeSystemConfigFilemodeOption(
+ SelfCleaningDirectory scd,
+ bool value)
+ {
+ Directory.CreateDirectory(scd.DirectoryPath);
+
+ var options = new RepositoryOptions
+ {
+ SystemConfigurationLocation = Path.Combine(
+ scd.RootedDirectoryPath, "fake-system.config")
+ };
+
+ StringBuilder sb = new StringBuilder()
+ .AppendFormat("[core]{0}", Environment.NewLine)
+ .AppendFormat("filemode = {1}{0}", Environment.NewLine, value);
+ File.WriteAllText(options.SystemConfigurationLocation, sb.ToString());
+
+ return options;
+ }
+
[Fact]
public void CanCompareTheWorkDirAgainstTheIndexWithUntrackedFiles()
{
View
1  LibGit2Sharp.Tests/MetaFixture.cs
@@ -18,6 +18,7 @@ public class MetaFixture
typeof(Repository),
typeof(RepositoryOptions),
typeof(Signature),
+ typeof(ExplicitPathsOptions),
};
// Related to https://github.com/libgit2/libgit2sharp/pull/251
View
15 LibGit2Sharp.Tests/ResetIndexFixture.cs
@@ -124,16 +124,27 @@ public void CanResetTheIndexToASubsetOfTheContentOfACommitWithCommitishAsArgumen
}
[Fact]
- public void CanResetTheIndexToASubsetOfTheContentOfACommitWithCommitAsArgument()
+ public void CanResetTheIndexToASubsetOfTheContentOfACommitWithCommitAsArgumentAndLaxUnmatchedExplicitPathsValidation()
{
string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
- repo.Reset(repo.Lookup<Commit>("5b5b025"), new[] { "new.txt" });
+ repo.Reset(repo.Lookup<Commit>("5b5b025"), new[] { "new.txt", "non-existent-path-28.txt" },
+ new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false });
Assert.Equal("a8233120f6ad708f843d861ce2b7228ec4e3dec6", repo.Index["README"].Id.Sha);
Assert.Equal("fa49b077972391ad58037050f2a75f74e3671e92", repo.Index["new.txt"].Id.Sha);
}
}
+
+ [Fact]
+ public void ResettingTheIndexToASubsetOfTheContentOfACommitWithCommitAsArgumentAndStrictUnmatchedPathspecsValidationThrows()
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ Assert.Throws<UnmatchedPathException>(() =>
+ repo.Reset(repo.Lookup<Commit>("5b5b025"), new[] { "new.txt", "non-existent-path-28.txt" }, new ExplicitPathsOptions()));
+ }
+ }
}
}
View
89 LibGit2Sharp.Tests/StageFixture.cs
@@ -60,14 +60,46 @@ public void CanStageTheUpdationOfAStagedFile()
[Theory]
[InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)]
[InlineData("deleted_staged_file.txt", FileStatus.Removed)]
- public void StagingAnUnknownFileThrows(string relativePath, FileStatus status)
+ public void StagingAnUnknownFileThrowsIfExplicitPath(string relativePath, FileStatus status)
{
using (var repo = new Repository(StandardTestRepoPath))
{
Assert.Null(repo.Index[relativePath]);
Assert.Equal(status, repo.Index.RetrieveStatus(relativePath));
- Assert.Throws<LibGit2SharpException>(() => repo.Index.Stage(relativePath));
+ Assert.Throws<UnmatchedPathException>(() => repo.Index.Stage(relativePath, new ExplicitPathsOptions()));
+ }
+ }
+
+ [Theory]
+ [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)]
+ [InlineData("deleted_staged_file.txt", FileStatus.Removed)]
+ public void CanStageAnUnknownFileWithLaxUnmatchedExplicitPathsValidation(string relativePath, FileStatus status)
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Assert.Null(repo.Index[relativePath]);
+ Assert.Equal(status, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.DoesNotThrow(() => repo.Index.Stage(relativePath));
+ Assert.DoesNotThrow(() => repo.Index.Stage(relativePath, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }));
+
+ Assert.Equal(status, repo.Index.RetrieveStatus(relativePath));
+ }
+ }
+
+ [Theory]
+ [InlineData("1/I-do-not-exist.txt", FileStatus.Nonexistent)]
+ [InlineData("deleted_staged_file.txt", FileStatus.Removed)]
+ public void StagingAnUnknownFileWithLaxExplicitPathsValidationDoesntThrow(string relativePath, FileStatus status)
+ {
+ using (var repo = new Repository(StandardTestRepoPath))
+ {
+ Assert.Null(repo.Index[relativePath]);
+ Assert.Equal(status, repo.Index.RetrieveStatus(relativePath));
+
+ repo.Index.Stage(relativePath);
+ repo.Index.Stage(relativePath, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false });
}
}
@@ -199,7 +231,7 @@ public void StagingANewFileWithAFullPathWhichEscapesOutOfTheWorkingDirThrows()
}
[Fact]
- public void StageFileWithBadParamsThrows()
+ public void StagingFileWithBadParamsThrows()
{
using (var repo = new Repository(StandardTestRepoPath))
{
@@ -209,5 +241,56 @@ public void StageFileWithBadParamsThrows()
Assert.Throws<ArgumentException>(() => repo.Index.Stage(new string[] { null }));
}
}
+
+ /*
+ * $ git status -s
+ * M 1/branch_file.txt
+ * M README
+ * M branch_file.txt
+ * D deleted_staged_file.txt
+ * D deleted_unstaged_file.txt
+ * M modified_staged_file.txt
+ * M modified_unstaged_file.txt
+ * M new.txt
+ * A new_tracked_file.txt
+ * ?? new_untracked_file.txt
+ *
+ * By passing "*" to Stage, the following files will be added/removed/updated from the index:
+ * - deleted_unstaged_file.txt : removed
+ * - modified_unstaged_file.txt : updated
+ * - new_untracked_file.txt : added
+ */
+ [Theory]
+ [InlineData("*u*", 0)]
+ [InlineData("*", 0)]
+ [InlineData("1/*", 0)]
+ [InlineData("RE*", 0)]
+ [InlineData("d*", -1)]
+ [InlineData("*modified_unstaged*", 0)]
+ [InlineData("new_*file.txt", 1)]
+ public void CanStageWithPathspec(string relativePath, int expectedIndexCountVariation)
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ int count = repo.Index.Count;
+
+ repo.Index.Stage(relativePath);
+
+ Assert.Equal(count + expectedIndexCountVariation, repo.Index.Count);
+ }
+ }
+
+ [Fact]
+ public void CanStageWithMultiplePathspecs()
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ int count = repo.Index.Count;
+
+ repo.Index.Stage(new string[] { "*", "u*" });
+
+ Assert.Equal(count, repo.Index.Count); // 1 added file, 1 deleted file, so same count
+ }
+ }
}
}
View
70 LibGit2Sharp.Tests/UnstageFixture.cs
@@ -65,11 +65,12 @@ public void CanStageAndUnstageAnIgnoredFile()
[InlineData("1/branch_file.txt", FileStatus.Unaltered, true, FileStatus.Unaltered, true, 0)]
[InlineData("deleted_unstaged_file.txt", FileStatus.Missing, true, FileStatus.Missing, true, 0)]
[InlineData("modified_unstaged_file.txt", FileStatus.Modified, true, FileStatus.Modified, true, 0)]
- [InlineData("new_untracked_file.txt", FileStatus.Untracked, false, FileStatus.Untracked, false, 0)]
[InlineData("modified_staged_file.txt", FileStatus.Staged, true, FileStatus.Modified, true, 0)]
[InlineData("new_tracked_file.txt", FileStatus.Added, true, FileStatus.Untracked, false, -1)]
- [InlineData("where-am-I.txt", FileStatus.Nonexistent, false, FileStatus.Nonexistent, false, 0)]
- public void CanUnStage(string relativePath, FileStatus currentStatus, bool doesCurrentlyExistInTheIndex, FileStatus expectedStatusOnceStaged, bool doesExistInTheIndexOnceStaged, int expectedIndexCountVariation)
+ [InlineData("deleted_staged_file.txt", FileStatus.Removed, false, FileStatus.Missing, true, 1)]
+ public void CanUnstage(
+ string relativePath, FileStatus currentStatus, bool doesCurrentlyExistInTheIndex,
+ FileStatus expectedStatusOnceStaged, bool doesExistInTheIndexOnceStaged, int expectedIndexCountVariation)
{
string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
@@ -86,6 +87,33 @@ public void CanUnStage(string relativePath, FileStatus currentStatus, bool doesC
}
}
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("where-am-I.txt", FileStatus.Nonexistent)]
+ public void UnstagingUnknownPathsWithStrictUnmatchedExplicitPathsValidationThrows(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.Throws<UnmatchedPathException>(() => repo.Index.Unstage(relativePath, new ExplicitPathsOptions()));
+ }
+ }
+
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("where-am-I.txt", FileStatus.Nonexistent)]
+ public void CanUnstageUnknownPathsWithLaxUnmatchedExplicitPathsValidation(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.DoesNotThrow(() => repo.Index.Unstage(relativePath, new ExplicitPathsOptions() { ShouldFailOnUnmatchedPath = false }));
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+ }
+ }
+
[Fact]
public void CanUnstageTheRemovalOfAFile()
{
@@ -125,6 +153,42 @@ public void CanUnstageUntrackedFileAgainstAnOrphanedHead()
RepositoryStatus status = repo.Index.RetrieveStatus();
Assert.Equal(0, status.Staged.Count());
Assert.Equal(1, status.Untracked.Count());
+
+ Assert.Throws<UnmatchedPathException>(() => repo.Index.Unstage("i-dont-exist", new ExplicitPathsOptions()));
+ }
+ }
+
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("where-am-I.txt", FileStatus.Nonexistent)]
+ public void UnstagingUnknownPathsAgainstAnOrphanedHeadWithStrictUnmatchedExplicitPathsValidationThrows(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ repo.Refs.UpdateTarget("HEAD", "refs/heads/orphaned");
+ Assert.True(repo.Info.IsHeadOrphaned);
+
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.Throws<UnmatchedPathException>(() => repo.Index.Unstage(relativePath, new ExplicitPathsOptions()));
+ }
+ }
+
+ [Theory]
+ [InlineData("new_untracked_file.txt", FileStatus.Untracked)]
+ [InlineData("where-am-I.txt", FileStatus.Nonexistent)]
+ public void CanUnstageUnknownPathsAgainstAnOrphanedHeadWithLaxUnmatchedExplicitPathsValidation(string relativePath, FileStatus currentStatus)
+ {
+ using (var repo = new Repository(CloneStandardTestRepo()))
+ {
+ repo.Refs.UpdateTarget("HEAD", "refs/heads/orphaned");
+ Assert.True(repo.Info.IsHeadOrphaned);
+
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
+
+ Assert.DoesNotThrow(() => repo.Index.Unstage(relativePath));
+ Assert.DoesNotThrow(() => repo.Index.Unstage(relativePath, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }));
+ Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath));
}
}
View
8 LibGit2Sharp/Core/GitDiff.cs
@@ -159,6 +159,12 @@ public void Dispose()
}
}
+ internal delegate int diff_notify_cb(
+ IntPtr diff_so_far,
+ IntPtr delta_to_add,
+ IntPtr matched_pathspec,
+ IntPtr payload);
+
[StructLayout(LayoutKind.Sequential)]
internal class GitDiffOptions : IDisposable
{
@@ -174,7 +180,7 @@ internal class GitDiffOptions : IDisposable
public GitStrArrayIn PathSpec;
public Int64 MaxSize;
- public IntPtr NotifyCallback;
+ public diff_notify_cb NotifyCallback;
public IntPtr NotifyPayload;
public void Dispose()
View
176 LibGit2Sharp/Diff.cs
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Text;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Compat;
using LibGit2Sharp.Core.Handles;
+using Environment = System.Environment;
namespace LibGit2Sharp
{
@@ -16,7 +19,7 @@ public class Diff
{
private readonly Repository repo;
- private GitDiffOptions BuildOptions(DiffOptions diffOptions, IEnumerable<string> paths = null)
+ private static GitDiffOptions BuildOptions(DiffOptions diffOptions, FilePath[] filePaths = null, MatchedPathsAggregator matchedPathsAggregator = null)
{
var options = new GitDiffOptions();
@@ -30,17 +33,42 @@ private GitDiffOptions BuildOptions(DiffOptions diffOptions, IEnumerable<string>
GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT;
}
- if (paths == null)
+ if (diffOptions.HasFlag(DiffOptions.IncludeIgnored))
+ {
+ options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_IGNORED;
+ }
+
+ if (diffOptions.HasFlag(DiffOptions.IncludeUnmodified))
+ {
+ options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED;
+ }
+
+ if (diffOptions.HasFlag(DiffOptions.DisablePathspecMatch))
+ {
+ options.Flags |= GitDiffOptionFlags.GIT_DIFF_DISABLE_PATHSPEC_MATCH;
+ }
+
+ if (matchedPathsAggregator != null)
+ {
+ options.NotifyCallback = matchedPathsAggregator.OnGitDiffNotify;
+ }
+
+ if (filePaths == null)
{
return options;
}
- options.PathSpec = GitStrArrayIn.BuildFrom(ToFilePaths(repo, paths));
+ options.PathSpec = GitStrArrayIn.BuildFrom(filePaths);
return options;
}
private static FilePath[] ToFilePaths(Repository repo, IEnumerable<string> paths)
{
+ if (paths == null)
+ {
+ return null;
+ }
+
var filePaths = new List<FilePath>();
foreach (string path in paths)
@@ -78,22 +106,30 @@ internal Diff(Repository repo)
/// <param name = "oldTree">The <see cref = "Tree"/> you want to compare from.</param>
/// <param name = "newTree">The <see cref = "Tree"/> you want to compare to.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be compared.</param>
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
/// <returns>A <see cref = "TreeChanges"/> containing the changes between the <paramref name = "oldTree"/> and the <paramref name = "newTree"/>.</returns>
- public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable<string> paths = null)
+ public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null)
{
- using(GitDiffOptions options = BuildOptions(DiffOptions.None, paths))
- using (DiffListSafeHandle diff = BuildDiffListFromTrees(
- oldTree != null ? oldTree.Id : null,
- newTree != null ? newTree.Id : null,
- options))
+ var comparer = TreeToTree(repo);
+ ObjectId oldTreeId = oldTree != null ? oldTree.Id : null;
+ ObjectId newTreeId = newTree != null ? newTree.Id : null;
+ var diffOptions = DiffOptions.None;
+
+ if (explicitPathsOptions != null)
{
- return new TreeChanges(diff);
+ diffOptions |= DiffOptions.DisablePathspecMatch;
+
+ if (explicitPathsOptions.ShouldFailOnUnmatchedPath ||
+ explicitPathsOptions.OnUnmatchedPath != null)
+ {
+ diffOptions |= DiffOptions.IncludeUnmodified;
+ }
}
- }
- private DiffListSafeHandle BuildDiffListFromTrees(ObjectId oldTree, ObjectId newTree, GitDiffOptions options)
- {
- return Proxy.git_diff_tree_to_tree(repo.Handle, oldTree, newTree, options);
+ return BuildTreeChangesFromComparer(oldTreeId, newTreeId, comparer, diffOptions, paths, explicitPathsOptions);
}
/// <summary>
@@ -128,21 +164,31 @@ public virtual ContentChanges Compare(Blob oldBlob, Blob newBlob)
/// <param name = "oldTree">The <see cref = "Tree"/> to compare from.</param>
/// <param name = "diffTargets">The targets to compare to.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be compared.</param>
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
/// <returns>A <see cref = "TreeChanges"/> containing the changes between the <see cref="Tree"/> and the selected target.</returns>
- public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable<string> paths = null)
+ public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null)
{
var comparer = handleRetrieverDispatcher[diffTargets](repo);
+ ObjectId oldTreeId = oldTree != null ? oldTree.Id : null;
DiffOptions diffOptions = diffTargets.HasFlag(DiffTargets.WorkingDirectory) ?
DiffOptions.IncludeUntracked : DiffOptions.None;
- using (GitDiffOptions options = BuildOptions(diffOptions, paths))
- using (DiffListSafeHandle dl = BuildDiffListFromTreeAndComparer(
- oldTree != null ? oldTree.Id : null,
- comparer, options))
+ if (explicitPathsOptions != null)
{
- return new TreeChanges(dl);
+ diffOptions |= DiffOptions.DisablePathspecMatch;
+
+ if (explicitPathsOptions.ShouldFailOnUnmatchedPath ||
+ explicitPathsOptions.OnUnmatchedPath != null)
+ {
+ diffOptions |= DiffOptions.IncludeUnmodified;
+ }
}
+
+ return BuildTreeChangesFromComparer(oldTreeId, null, comparer, diffOptions, paths, explicitPathsOptions);
}
/// <summary>
@@ -150,39 +196,61 @@ public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnume
/// </summary>
/// <param name = "paths">The list of paths (either files or directories) that should be compared.</param>
/// <param name = "includeUntracked">If true, include untracked files from the working dir as additions. Otherwise ignore them.</param>
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
/// <returns>A <see cref = "TreeChanges"/> containing the changes between the working directory and the index.</returns>
- public virtual TreeChanges Compare(IEnumerable<string> paths = null, bool includeUntracked = false)
+ public virtual TreeChanges Compare(IEnumerable<string> paths = null, bool includeUntracked = false, ExplicitPathsOptions explicitPathsOptions = null)
+ {
+ return Compare(includeUntracked ? DiffOptions.IncludeUntracked : DiffOptions.None, paths, explicitPathsOptions);
+ }
+
+ internal virtual TreeChanges Compare(DiffOptions diffOptions, IEnumerable<string> paths = null,
+ ExplicitPathsOptions explicitPathsOptions = null)
{
var comparer = WorkdirToIndex(repo);
- using (GitDiffOptions options = BuildOptions(includeUntracked ? DiffOptions.IncludeUntracked : DiffOptions.None, paths))
- using (DiffListSafeHandle dl = BuildDiffListFromComparer(null, comparer, options))
+ if (explicitPathsOptions != null)
{
- return new TreeChanges(dl);
+ diffOptions |= DiffOptions.DisablePathspecMatch;
+
+ if (explicitPathsOptions.ShouldFailOnUnmatchedPath ||
+ explicitPathsOptions.OnUnmatchedPath != null)
+ {
+ diffOptions |= DiffOptions.IncludeUnmodified;
+ }
}
+
+ return BuildTreeChangesFromComparer(null, null, comparer, diffOptions, paths, explicitPathsOptions);
}
- private delegate DiffListSafeHandle TreeComparisonHandleRetriever(ObjectId id, GitDiffOptions options);
+ private delegate DiffListSafeHandle TreeComparisonHandleRetriever(ObjectId oldTreeId, ObjectId newTreeId, GitDiffOptions options);
+
+ private static TreeComparisonHandleRetriever TreeToTree(Repository repo)
+ {
+ return (oh, nh, o) => Proxy.git_diff_tree_to_tree(repo.Handle, oh, nh, o);
+ }
private static TreeComparisonHandleRetriever WorkdirToIndex(Repository repo)
{
- return (h, o) => Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o);
+ return (oh, nh, o) => Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o);
}
private static TreeComparisonHandleRetriever WorkdirToTree(Repository repo)
{
- return (h, o) => Proxy.git_diff_tree_to_workdir(repo.Handle, h, o);
+ return (oh, nh, o) => Proxy.git_diff_tree_to_workdir(repo.Handle, oh, o);
}
private static TreeComparisonHandleRetriever WorkdirAndIndexToTree(Repository repo)
{
- TreeComparisonHandleRetriever comparisonHandleRetriever = (h, o) =>
+ TreeComparisonHandleRetriever comparisonHandleRetriever = (oh, nh, o) =>
{
DiffListSafeHandle diff = null, diff2 = null;
try
{
- diff = Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, h, o);
+ diff = Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o);
diff2 = Proxy.git_diff_index_to_workdir(repo.Handle, repo.Index.Handle, o);
Proxy.git_diff_merge(diff, diff2);
}
@@ -204,17 +272,57 @@ private static TreeComparisonHandleRetriever WorkdirAndIndexToTree(Repository re
private static TreeComparisonHandleRetriever IndexToTree(Repository repo)
{
- return (h, o) => Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, h, o);
+ return (oh, nh, o) => Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o);
+ }
+
+ private TreeChanges BuildTreeChangesFromComparer(
+ ObjectId oldTreeId, ObjectId newTreeId, TreeComparisonHandleRetriever comparisonHandleRetriever,
+ DiffOptions diffOptions, IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null)
+ {
+ var matchedPaths = new MatchedPathsAggregator();
+ var filePaths = ToFilePaths(repo, paths);
+
+ using (GitDiffOptions options = BuildOptions(diffOptions, filePaths, matchedPaths))
+ using (DiffListSafeHandle diffList = comparisonHandleRetriever(oldTreeId, newTreeId, options))
+ {
+ if (explicitPathsOptions != null)
+ {
+ DispatchUnmatchedPaths(explicitPathsOptions, filePaths, matchedPaths);
+ }
+
+ return new TreeChanges(diffList);
+ }
}
- private static DiffListSafeHandle BuildDiffListFromTreeAndComparer(ObjectId treeId, TreeComparisonHandleRetriever comparisonHandleRetriever, GitDiffOptions options)
+ private static void DispatchUnmatchedPaths(ExplicitPathsOptions explicitPathsOptions,
+ IEnumerable<FilePath> filePaths,
+ IEnumerable<FilePath> matchedPaths)
{
- return BuildDiffListFromComparer(treeId, comparisonHandleRetriever, options);
+ List<FilePath> unmatchedPaths = (filePaths != null ?
+ filePaths.Except(matchedPaths) : Enumerable.Empty<FilePath>()).ToList();
+
+ if (!unmatchedPaths.Any())
+ {
+ return;
+ }
+
+ if (explicitPathsOptions.OnUnmatchedPath != null)
+ {
+ unmatchedPaths.ForEach(filePath => explicitPathsOptions.OnUnmatchedPath(filePath.Native));
+ }
+
+ if (explicitPathsOptions.ShouldFailOnUnmatchedPath)
+ {
+ throw new UnmatchedPathException(BuildUnmatchedPathsMessage(unmatchedPaths));
+ }
}
- private static DiffListSafeHandle BuildDiffListFromComparer(ObjectId treeId, TreeComparisonHandleRetriever comparisonHandleRetriever, GitDiffOptions options)
+ private static string BuildUnmatchedPathsMessage(List<FilePath> unmatchedPaths)
{
- return comparisonHandleRetriever(treeId, options);
+ var message = new StringBuilder("There were some unmatched paths:" + Environment.NewLine);
+ unmatchedPaths.ForEach(filePath => message.AppendFormat("- {0}{1}", filePath.Native, Environment.NewLine));
+
+ return message.ToString();
}
}
}
View
21 LibGit2Sharp/DiffOptions.cs
@@ -12,12 +12,29 @@ internal enum DiffOptions
/// <summary>
/// No special behavior.
/// </summary>
- None,
+ None = 0,
/// <summary>
/// Include untracked files among the files to be processed, when
/// diffing against the working directory.
/// </summary>
- IncludeUntracked,
+ IncludeUntracked = (1 << 1),
+
+ /// <summary>
+ /// Include unmodified files among the files to be processed, when
+ /// diffing against the working directory.
+ /// </summary>
+ IncludeUnmodified = (1 << 2),
+
+ /// <summary>
+ /// Treats the passed pathspecs as explicit paths (no pathspec match).
+ /// </summary>
+ DisablePathspecMatch = (1 << 3),
+
+ /// <summary>
+ /// Include ignored files among the files to be processed, when
+ /// diffing against the working directory.
+ /// </summary>
+ IncludeIgnored = (1 << 4),
}
}
View
37 LibGit2Sharp/ExplicitPathsOptions.cs
@@ -0,0 +1,37 @@
+using LibGit2Sharp.Handlers;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// Allows callers to specify how unmatched paths should be handled
+ /// by operations such as Reset(), Compare(), Unstage(), ...
+ /// <para>
+ /// By passing these options, the passed paths will be treated as
+ /// explicit paths, and NOT pathspecs containing globs.
+ /// </para>
+ /// </summary>
+ public class ExplicitPathsOptions
+ {
+ /// <summary>
+ /// Associated paths will be treated as explicit paths.
+ /// </summary>
+ public ExplicitPathsOptions()
+ {
+ ShouldFailOnUnmatchedPath = true;
+ }
+
+ /// <summary>
+ /// When set to true, the called operation will throw a <see cref="UnmatchedPathException"/> when an unmatched
+ /// path is encountered.
+ /// <para>
+ /// Set to true by default.
+ /// </para>
+ /// </summary>
+ public bool ShouldFailOnUnmatchedPath { get; set; }
+
+ /// <summary>
+ /// Sets a callback that will be called once for each unmatched path.
+ /// </summary>
+ public UnmatchedPathHandler OnUnmatchedPath { get; set; }
+ }
+}
View
9 LibGit2Sharp/Handlers.cs
@@ -46,4 +46,13 @@
/// <param name="completedSteps">Number of completed steps.</param>
/// <param name="totalSteps">Total number of steps.</param>
public delegate void CheckoutProgressHandler(string path, int completedSteps, int totalSteps);
+
+ /// <summary>
+ /// Delegate definition for unmatched path callback.
+ /// <para>
+ /// This callback will be called to notify the caller of unmatched path.
+ /// </para>
+ /// </summary>
+ /// <param name="unmatchedPath">The unmatched path.</param>
+ public delegate void UnmatchedPathHandler(string unmatchedPath);
}
View
6 LibGit2Sharp/IRepository.cs
@@ -125,7 +125,11 @@ public interface IRepository : IDisposable
/// </summary>
/// <param name = "commit">The target commit object.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be considered.</param>
- void Reset(Commit commit, IEnumerable<string> paths = null);
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ void Reset(Commit commit, IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null);
/// <summary>
/// Clean the working tree by removing files that are not under version control.
View
77 LibGit2Sharp/Index.cs
@@ -128,51 +128,51 @@ IEnumerator IEnumerable.GetEnumerator()
/// Promotes to the staging area the latest modifications of a file in the working directory (addition, updation or removal).
/// </summary>
/// <param name = "path">The path of the file within the working directory.</param>
- public virtual void Stage(string path)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="path"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public virtual void Stage(string path, ExplicitPathsOptions explicitPathsOptions = null)
{
Ensure.ArgumentNotNull(path, "path");
- Stage(new[] { path });
+ Stage(new[] { path }, explicitPathsOptions);
}
/// <summary>
/// Promotes to the staging area the latest modifications of a collection of files in the working directory (addition, updation or removal).
/// </summary>
/// <param name = "paths">The collection of paths of the files within the working directory.</param>
- public virtual void Stage(IEnumerable<string> paths)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public virtual void Stage(IEnumerable<string> paths, ExplicitPathsOptions explicitPathsOptions = null)
{
- //TODO: Stage() should support following use cases:
- // - Recursively staging the content of a directory
+ Ensure.ArgumentNotNull(paths, "paths");
- IEnumerable<KeyValuePair<string, FileStatus>> batch = PrepareBatch(paths);
+ TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUntracked | DiffOptions.IncludeIgnored, paths, explicitPathsOptions);
- foreach (KeyValuePair<string, FileStatus> kvp in batch)
+ foreach (var treeEntryChanges in changes)
{
- if (Directory.Exists(kvp.Key))
- {
- throw new NotImplementedException();
- }
-
- if (!kvp.Value.HasFlag(FileStatus.Nonexistent))
+ switch (treeEntryChanges.Status)
{
- continue;
- }
+ case ChangeKind.Unmodified:
+ continue;
- throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Can not stage '{0}'. The file does not exist.", kvp.Key));
- }
+ case ChangeKind.Deleted:
+ RemoveFromIndex(treeEntryChanges.Path);
+ continue;
- foreach (KeyValuePair<string, FileStatus> kvp in batch)
- {
- string relativePath = kvp.Key;
- FileStatus fileStatus = kvp.Value;
+ case ChangeKind.Added:
+ /* Fall through */
+ case ChangeKind.Modified:
+ AddToIndex(treeEntryChanges.Path);
+ continue;
- if (fileStatus.HasFlag(FileStatus.Missing))
- {
- RemoveFromIndex(relativePath);
- }
- else
- {
- AddToIndex(relativePath);
+ default:
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "Entry '{0}' bears an unexpected ChangeKind '{1}'", treeEntryChanges.Path, treeEntryChanges.Status));
}
}
@@ -183,30 +183,38 @@ public virtual void Stage(IEnumerable<string> paths)
/// Removes from the staging area all the modifications of a file since the latest commit (addition, updation or removal).
/// </summary>
/// <param name = "path">The path of the file within the working directory.</param>
- public virtual void Unstage(string path)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="path"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public virtual void Unstage(string path, ExplicitPathsOptions explicitPathsOptions = null)
{
Ensure.ArgumentNotNull(path, "path");
- Unstage(new[] { path });
+ Unstage(new[] { path }, explicitPathsOptions);
}
/// <summary>
/// Removes from the staging area all the modifications of a collection of file since the latest commit (addition, updation or removal).
/// </summary>
/// <param name = "paths">The collection of paths of the files within the working directory.</param>
- public virtual void Unstage(IEnumerable<string> paths)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public virtual void Unstage(IEnumerable<string> paths, ExplicitPathsOptions explicitPathsOptions = null)
{
Ensure.ArgumentNotNull(paths, "paths");
if (repo.Info.IsHeadOrphaned)
{
- TreeChanges changes = repo.Diff.Compare(null, DiffTargets.Index, paths);
+ TreeChanges changes = repo.Diff.Compare(null, DiffTargets.Index, paths, explicitPathsOptions);
Reset(changes);
}
else
{
- repo.Reset("HEAD", paths);
+ repo.Reset("HEAD", paths, explicitPathsOptions);
}
}
@@ -449,6 +457,9 @@ internal void Reset(TreeChanges changes)
{
switch (treeEntryChanges.Status)
{
+ case ChangeKind.Unmodified:
+ continue;
+
case ChangeKind.Added:
RemoveFromIndex(treeEntryChanges.Path);
continue;
View
3  LibGit2Sharp/LibGit2Sharp.csproj
@@ -69,14 +69,17 @@
<Compile Include="CheckoutOptions.cs" />
<Compile Include="Conflict.cs" />
<Compile Include="ConflictCollection.cs" />
+ <Compile Include="UnmatchedPathException.cs" />
<Compile Include="Core\Handles\GitFetchSpecHandle.cs" />
<Compile Include="Core\PathCase.cs" />
+ <Compile Include="MatchedPathsAggregator.cs" />
<Compile Include="Network.cs" />
<Compile Include="Core\GitRemoteHead.cs" />
<Compile Include="Stash.cs" />
<Compile Include="StashOptions.cs" />
<Compile Include="OrphanedHeadException.cs" />
<Compile Include="StashCollection.cs" />
+ <Compile Include="ExplicitPathsOptions.cs" />
<Compile Include="UnmergedIndexEntriesException.cs" />
<Compile Include="Commit.cs" />
<Compile Include="CommitLog.cs" />
View
43 LibGit2Sharp/MatchedPathsAggregator.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using LibGit2Sharp.Core;
+
+namespace LibGit2Sharp
+{
+ internal class MatchedPathsAggregator : IEnumerable<FilePath>
+ {
+ private readonly List<FilePath> matchedPaths = new List<FilePath>();
+
+ /// <summary>
+ /// The delegate with a signature that matches the native diff git_diff_notify_cb function's signature.
+ /// </summary>
+ /// <param name="diffListSoFar">The diff list so far, before the delta is inserted.</param>
+ /// <param name="deltaToAdd">The delta that is being diffed</param>
+ /// <param name="matchedPathspec">The pathsec that matched the path of the diffed files.</param>
+ /// <param name="payload">Payload object.</param>
+ internal int OnGitDiffNotify(IntPtr diffListSoFar, IntPtr deltaToAdd, IntPtr matchedPathspec, IntPtr payload)
+ {
+ // Convert null strings into empty strings.
+ var path = (matchedPathspec != IntPtr.Zero) ? FilePathMarshaler.FromNative(matchedPathspec) : FilePath.Empty;
+
+ if (matchedPaths.Contains(path))
+ {
+ return 0;
+ }
+
+ matchedPaths.Add(path);
+ return 0;
+ }
+
+ public IEnumerator<FilePath> GetEnumerator()
+ {
+ return matchedPaths.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
View
8 LibGit2Sharp/Repository.cs
@@ -641,7 +641,11 @@ public void Reset(ResetOptions resetOptions, Commit commit)
/// </summary>
/// <param name = "commit">The target commit object.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be considered.</param>
- public void Reset(Commit commit, IEnumerable<string> paths = null)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public void Reset(Commit commit, IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null)
{
if (Info.IsBare)
{
@@ -650,7 +654,7 @@ public void Reset(Commit commit, IEnumerable<string> paths = null)
Ensure.ArgumentNotNull(commit, "commit");
- TreeChanges changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths);
+ TreeChanges changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths, explicitPathsOptions);
Index.Reset(changes);
}
View
8 LibGit2Sharp/RepositoryExtensions.cs
@@ -136,7 +136,11 @@ public static void Reset(this IRepository repository, ResetOptions resetOptions,
/// <param name = "repository">The <see cref = "Repository" /> being worked with.</param>
/// <param name = "committish">A revparse spec for the target commit object.</param>
/// <param name = "paths">The list of paths (either files or directories) that should be considered.</param>
- public static void Reset(this IRepository repository, string committish = "HEAD", IEnumerable<string> paths = null)
+ /// <param name = "explicitPathsOptions">
+ /// If set, the passed <paramref name="paths"/> will be treated as explicit paths.
+ /// Use these options to determine how unmatched explicit paths should be handled.
+ /// </param>
+ public static void Reset(this IRepository repository, string committish = "HEAD", IEnumerable<string> paths = null, ExplicitPathsOptions explicitPathsOptions = null)
{
if (repository.Info.IsBare)
{
@@ -147,7 +151,7 @@ public static void Reset(this IRepository repository, string committish = "HEAD"
Commit commit = LookUpCommit(repository, committish);
- repository.Reset(commit, paths);
+ repository.Reset(commit, paths, explicitPathsOptions);
}
private static Commit LookUpCommit(IRepository repository, string committish)
View
12 LibGit2Sharp/TreeChanges.cs
@@ -56,6 +56,11 @@ private int PrintCallBack(GitDiffDelta delta, GitDiffRange range, GitDiffLineOri
string formattedoutput = Utf8Marshaler.FromNative(content, (int)contentlen);
TreeEntryChanges currentChange = AddFileChange(delta, lineorigin);
+ if (currentChange == null)
+ {
+ return 0;
+ }
+
AddLineChange(currentChange, lineorigin);
currentChange.AppendToPatch(formattedoutput);
@@ -82,6 +87,11 @@ 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)
@@ -93,7 +103,7 @@ private TreeEntryChanges AddFileChange(GitDiffDelta delta, GitDiffLineOrigin lin
var newOid = delta.NewFile.Oid;
var oldOid = delta.OldFile.Oid;
- if (delta.Status == ChangeKind.Untracked)
+ if (delta.Status == ChangeKind.Untracked || delta.Status == ChangeKind.Ignored)
{
delta.Status = ChangeKind.Added;
}
View
54 LibGit2Sharp/UnmatchedPathException.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Runtime.Serialization;
+using LibGit2Sharp.Core;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// The exception that is thrown when an explicit path or a list of explicit paths could not be matched.
+ /// </summary>
+ [Serializable]
+ public class UnmatchedPathException : LibGit2SharpException
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref = "UnmatchedPathException" /> class.
+ /// </summary>
+ public UnmatchedPathException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref = "UnmatchedPathException" /> class with a specified error message.
+ /// </summary>
+ /// <param name = "message">A message that describes the error. </param>
+ public UnmatchedPathException(string message)
+ : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref = "UnmatchedPathException" /> class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ /// </summary>
+ /// <param name = "message">The error message that explains the reason for the exception. </param>
+ /// <param name = "innerException">The exception that is the cause of the current exception. If the <paramref name = "innerException" /> parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param>
+ public UnmatchedPathException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref = "UnmatchedPathException" /> class with a serialized data.
+ /// </summary>
+ /// <param name = "info">The <see cref="SerializationInfo "/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name = "context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
+ protected UnmatchedPathException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ internal UnmatchedPathException(string message, GitErrorCode code, GitErrorCategory category)
+ : base(message, code, category)
+ {
+ }
+ }
+}
Something went wrong with that request. Please try again.