From 2027f49b18fa98f3e977fb8a13bd57ec3a174028 Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 11 Mar 2013 15:34:53 +0100 Subject: [PATCH 1/3] 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. --- LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs | 38 +++- LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs | 34 +++- .../DiffWorkdirToIndexFixture.cs | 127 +++++++++++++ LibGit2Sharp.Tests/MetaFixture.cs | 1 + LibGit2Sharp/Core/GitDiff.cs | 8 +- LibGit2Sharp/Diff.cs | 171 ++++++++++++++---- LibGit2Sharp/DiffOptions.cs | 15 +- LibGit2Sharp/ExplicitPathsOptions.cs | 37 ++++ LibGit2Sharp/Handlers.cs | 9 + LibGit2Sharp/LibGit2Sharp.csproj | 3 + LibGit2Sharp/MatchedPathsAggregator.cs | 43 +++++ LibGit2Sharp/TreeChanges.cs | 9 + LibGit2Sharp/UnmatchedPathException.cs | 54 ++++++ 13 files changed, 510 insertions(+), 39 deletions(-) create mode 100644 LibGit2Sharp/ExplicitPathsOptions.cs create mode 100644 LibGit2Sharp/MatchedPathsAggregator.cs create mode 100644 LibGit2Sharp/UnmatchedPathException.cs diff --git a/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs b/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs index 639d04e96..34e272bf1 100644 --- a/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs +++ b/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(() => repo.Diff.Compare(tree, DiffTargets.Index, + new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }, new ExplicitPathsOptions())); + } + } + [Fact] /* * $ git init . diff --git a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs index 493aeb4e4..f291cd6dd 100644 --- a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs +++ b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs @@ -87,7 +87,7 @@ public void CanCompareASubsetofTheTreeAgainstOneOfItsAncestor() Tree tree = repo.Head.Tip.Tree; Tree ancestor = repo.Lookup("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(() => + 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 diff --git a/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs b/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs index 4cc07306b..5232ef2a1 100644 --- a/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs +++ b/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(() => 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() { diff --git a/LibGit2Sharp.Tests/MetaFixture.cs b/LibGit2Sharp.Tests/MetaFixture.cs index 8cfb5014c..91ca9bbad 100644 --- a/LibGit2Sharp.Tests/MetaFixture.cs +++ b/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 diff --git a/LibGit2Sharp/Core/GitDiff.cs b/LibGit2Sharp/Core/GitDiff.cs index b6b7ae8b2..3eae44c6e 100644 --- a/LibGit2Sharp/Core/GitDiff.cs +++ b/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() diff --git a/LibGit2Sharp/Diff.cs b/LibGit2Sharp/Diff.cs index 288cd1930..2c3d5e6b7 100644 --- a/LibGit2Sharp/Diff.cs +++ b/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 paths = null) + private static GitDiffOptions BuildOptions(DiffOptions diffOptions, FilePath[] filePaths = null, MatchedPathsAggregator matchedPathsAggregator = null) { var options = new GitDiffOptions(); @@ -30,17 +33,37 @@ private GitDiffOptions BuildOptions(DiffOptions diffOptions, IEnumerable GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT; } - if (paths == null) + 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 paths) { + if (paths == null) + { + return null; + } + var filePaths = new List(); foreach (string path in paths) @@ -78,22 +101,30 @@ internal Diff(Repository repo) /// The you want to compare from. /// The you want to compare to. /// The list of paths (either files or directories) that should be compared. + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// /// A containing the changes between the and the . - public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable paths = null) + public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable 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); } /// @@ -128,21 +159,31 @@ private static IDictionaryThe to compare from. /// The targets to compare to. /// The list of paths (either files or directories) that should be compared. + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// /// A containing the changes between the and the selected target. - public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths = null) + public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable 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); } /// @@ -150,39 +191,61 @@ public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnume /// /// The list of paths (either files or directories) that should be compared. /// If true, include untracked files from the working dir as additions. Otherwise ignore them. + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// /// A containing the changes between the working directory and the index. - public virtual TreeChanges Compare(IEnumerable paths = null, bool includeUntracked = false) + public virtual TreeChanges Compare(IEnumerable paths = null, bool includeUntracked = false, ExplicitPathsOptions explicitPathsOptions = null) + { + return Compare(includeUntracked ? DiffOptions.IncludeUntracked : DiffOptions.None, paths, explicitPathsOptions); + } + + internal virtual TreeChanges Compare(DiffOptions diffOptions, IEnumerable 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 +267,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 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 filePaths, + IEnumerable matchedPaths) { - return BuildDiffListFromComparer(treeId, comparisonHandleRetriever, options); + List unmatchedPaths = (filePaths != null ? + filePaths.Except(matchedPaths) : Enumerable.Empty()).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 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(); } } } diff --git a/LibGit2Sharp/DiffOptions.cs b/LibGit2Sharp/DiffOptions.cs index 133e5a40e..61203f0bd 100644 --- a/LibGit2Sharp/DiffOptions.cs +++ b/LibGit2Sharp/DiffOptions.cs @@ -12,12 +12,23 @@ internal enum DiffOptions /// /// No special behavior. /// - None, + None = 0, /// /// Include untracked files among the files to be processed, when /// diffing against the working directory. /// - IncludeUntracked, + IncludeUntracked = (1 << 1), + + /// + /// Include unmodified files among the files to be processed, when + /// diffing against the working directory. + /// + IncludeUnmodified = (1 << 2), + + /// + /// Treats the passed pathspecs as explicit paths (no pathspec match). + /// + DisablePathspecMatch = (1 << 3) } } diff --git a/LibGit2Sharp/ExplicitPathsOptions.cs b/LibGit2Sharp/ExplicitPathsOptions.cs new file mode 100644 index 000000000..02e81c72d --- /dev/null +++ b/LibGit2Sharp/ExplicitPathsOptions.cs @@ -0,0 +1,37 @@ +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp +{ + /// + /// Allows callers to specify how unmatched paths should be handled + /// by operations such as Reset(), Compare(), Unstage(), ... + /// + /// By passing these options, the passed paths will be treated as + /// explicit paths, and NOT pathspecs containing globs. + /// + /// + public class ExplicitPathsOptions + { + /// + /// Associated paths will be treated as explicit paths. + /// + public ExplicitPathsOptions() + { + ShouldFailOnUnmatchedPath = true; + } + + /// + /// When set to true, the called operation will throw a when an unmatched + /// path is encountered. + /// + /// Set to true by default. + /// + /// + public bool ShouldFailOnUnmatchedPath { get; set; } + + /// + /// Sets a callback that will be called once for each unmatched path. + /// + public UnmatchedPathHandler OnUnmatchedPath { get; set; } + } +} diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs index 96b64b9f2..eb2dc03f5 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -46,4 +46,13 @@ /// Number of completed steps. /// Total number of steps. public delegate void CheckoutProgressHandler(string path, int completedSteps, int totalSteps); + + /// + /// Delegate definition for unmatched path callback. + /// + /// This callback will be called to notify the caller of unmatched path. + /// + /// + /// The unmatched path. + public delegate void UnmatchedPathHandler(string unmatchedPath); } diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 2475cf699..1eba31c0a 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -69,14 +69,17 @@ + + + diff --git a/LibGit2Sharp/MatchedPathsAggregator.cs b/LibGit2Sharp/MatchedPathsAggregator.cs new file mode 100644 index 000000000..4ae2e359f --- /dev/null +++ b/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 + { + private readonly List matchedPaths = new List(); + + /// + /// The delegate with a signature that matches the native diff git_diff_notify_cb function's signature. + /// + /// The diff list so far, before the delta is inserted. + /// The delta that is being diffed + /// The pathsec that matched the path of the diffed files. + /// Payload object. + 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 GetEnumerator() + { + return matchedPaths.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/LibGit2Sharp/TreeChanges.cs b/LibGit2Sharp/TreeChanges.cs index b1e90daa7..f31092feb 100644 --- a/LibGit2Sharp/TreeChanges.cs +++ b/LibGit2Sharp/TreeChanges.cs @@ -56,6 +56,10 @@ 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 +86,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) diff --git a/LibGit2Sharp/UnmatchedPathException.cs b/LibGit2Sharp/UnmatchedPathException.cs new file mode 100644 index 000000000..38f33c036 --- /dev/null +++ b/LibGit2Sharp/UnmatchedPathException.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Serialization; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// The exception that is thrown when an explicit path or a list of explicit paths could not be matched. + /// + [Serializable] + public class UnmatchedPathException : LibGit2SharpException + { + /// + /// Initializes a new instance of the class. + /// + public UnmatchedPathException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// A message that describes the error. + public UnmatchedPathException(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 UnmatchedPathException(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 UnmatchedPathException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + internal UnmatchedPathException(string message, GitErrorCode code, GitErrorCategory category) + : base(message, code, category) + { + } + } +} From 6419588c7c415e6e02e27dd474763dc55a029f78 Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 11 Mar 2013 15:35:03 +0100 Subject: [PATCH 2/3] Add ExplicitPathsOptions to Repository.Reset() --- LibGit2Sharp.Tests/ResetIndexFixture.cs | 15 +++++++++++++-- LibGit2Sharp/IRepository.cs | 6 +++++- LibGit2Sharp/Repository.cs | 8 ++++++-- LibGit2Sharp/RepositoryExtensions.cs | 8 ++++++-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/LibGit2Sharp.Tests/ResetIndexFixture.cs b/LibGit2Sharp.Tests/ResetIndexFixture.cs index 39ac16486..8b3a1e2f2 100644 --- a/LibGit2Sharp.Tests/ResetIndexFixture.cs +++ b/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("5b5b025"), new[] { "new.txt" }); + repo.Reset(repo.Lookup("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(() => + repo.Reset(repo.Lookup("5b5b025"), new[] { "new.txt", "non-existent-path-28.txt" }, new ExplicitPathsOptions())); + } + } } } diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index 12f3f01aa..da0b00b80 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -125,7 +125,11 @@ public interface IRepository : IDisposable /// /// The target commit object. /// The list of paths (either files or directories) that should be considered. - void Reset(Commit commit, IEnumerable paths = null); + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// + void Reset(Commit commit, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null); /// /// Clean the working tree by removing files that are not under version control. diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 1babaeadf..32768a470 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -641,7 +641,11 @@ public void Reset(ResetOptions resetOptions, Commit commit) /// /// The target commit object. /// The list of paths (either files or directories) that should be considered. - public void Reset(Commit commit, IEnumerable paths = null) + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// + public void Reset(Commit commit, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null) { if (Info.IsBare) { @@ -650,7 +654,7 @@ public void Reset(Commit commit, IEnumerable 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); } diff --git a/LibGit2Sharp/RepositoryExtensions.cs b/LibGit2Sharp/RepositoryExtensions.cs index 25c5b703e..88c48087a 100644 --- a/LibGit2Sharp/RepositoryExtensions.cs +++ b/LibGit2Sharp/RepositoryExtensions.cs @@ -136,7 +136,11 @@ public static void Reset(this IRepository repository, ResetOptions resetOptions, /// The being worked with. /// A revparse spec for the target commit object. /// The list of paths (either files or directories) that should be considered. - public static void Reset(this IRepository repository, string committish = "HEAD", IEnumerable paths = null) + /// + /// If set, the passed will be treated as explicit paths. + /// Use these options to determine how unmatched explicit paths should be handled. + /// + public static void Reset(this IRepository repository, string committish = "HEAD", IEnumerable 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) From 9405ac1b2337e1be41d53e0cb839d8f830165191 Mon Sep 17 00:00:00 2001 From: yorah Date: Mon, 11 Mar 2013 15:44:04 +0100 Subject: [PATCH 3/3] Add ExplicitPathsOptions to Repository.Index.Unstage()/Stage() --- LibGit2Sharp.Tests/StageFixture.cs | 89 +++++++++++++++++++++++++++- LibGit2Sharp.Tests/UnstageFixture.cs | 70 +++++++++++++++++++++- LibGit2Sharp/Diff.cs | 5 ++ LibGit2Sharp/DiffOptions.cs | 8 ++- LibGit2Sharp/Index.cs | 77 +++++++++++++----------- LibGit2Sharp/TreeChanges.cs | 3 +- 6 files changed, 211 insertions(+), 41 deletions(-) diff --git a/LibGit2Sharp.Tests/StageFixture.cs b/LibGit2Sharp.Tests/StageFixture.cs index cef274687..e61d747a8 100644 --- a/LibGit2Sharp.Tests/StageFixture.cs +++ b/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(() => repo.Index.Stage(relativePath)); + Assert.Throws(() => 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(() => 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 + } + } } } diff --git a/LibGit2Sharp.Tests/UnstageFixture.cs b/LibGit2Sharp.Tests/UnstageFixture.cs index 6f0fc32f3..524d8657f 100644 --- a/LibGit2Sharp.Tests/UnstageFixture.cs +++ b/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(() => 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(() => 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(() => 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)); } } diff --git a/LibGit2Sharp/Diff.cs b/LibGit2Sharp/Diff.cs index 2c3d5e6b7..a14071935 100644 --- a/LibGit2Sharp/Diff.cs +++ b/LibGit2Sharp/Diff.cs @@ -33,6 +33,11 @@ private static GitDiffOptions BuildOptions(DiffOptions diffOptions, FilePath[] f GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT; } + if (diffOptions.HasFlag(DiffOptions.IncludeIgnored)) + { + options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_IGNORED; + } + if (diffOptions.HasFlag(DiffOptions.IncludeUnmodified)) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED; diff --git a/LibGit2Sharp/DiffOptions.cs b/LibGit2Sharp/DiffOptions.cs index 61203f0bd..761a52a82 100644 --- a/LibGit2Sharp/DiffOptions.cs +++ b/LibGit2Sharp/DiffOptions.cs @@ -29,6 +29,12 @@ internal enum DiffOptions /// /// Treats the passed pathspecs as explicit paths (no pathspec match). /// - DisablePathspecMatch = (1 << 3) + DisablePathspecMatch = (1 << 3), + + /// + /// Include ignored files among the files to be processed, when + /// diffing against the working directory. + /// + IncludeIgnored = (1 << 4), } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index bd1d194bc..1732d45bb 100644 --- a/LibGit2Sharp/Index.cs +++ b/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). /// /// The path of the file within the working directory. - public virtual void Stage(string path) + /// + /// 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 Stage(string path, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); - Stage(new[] { path }); + Stage(new[] { path }, explicitPathsOptions); } /// /// Promotes to the staging area the latest modifications of a collection of files in the working directory (addition, updation or removal). /// /// The collection of paths of the files within the working directory. - public virtual void Stage(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 Stage(IEnumerable paths, ExplicitPathsOptions explicitPathsOptions = null) { - //TODO: Stage() should support following use cases: - // - Recursively staging the content of a directory + Ensure.ArgumentNotNull(paths, "paths"); - IEnumerable> batch = PrepareBatch(paths); + TreeChanges changes = repo.Diff.Compare(DiffOptions.IncludeUntracked | DiffOptions.IncludeIgnored, paths, explicitPathsOptions); - foreach (KeyValuePair 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 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 paths) /// Removes from the staging area all the modifications of a file since the latest commit (addition, updation or removal). /// /// The path of the file within the working directory. - public virtual void Unstage(string path) + /// + /// 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 Unstage(string path, ExplicitPathsOptions explicitPathsOptions = null) { Ensure.ArgumentNotNull(path, "path"); - Unstage(new[] { path }); + Unstage(new[] { path }, explicitPathsOptions); } /// /// Removes from the staging area all the modifications of a collection of file since the latest commit (addition, updation or removal). /// /// The collection of paths of the files within the working directory. - public virtual void Unstage(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 Unstage(IEnumerable 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; diff --git a/LibGit2Sharp/TreeChanges.cs b/LibGit2Sharp/TreeChanges.cs index f31092feb..0571d20e0 100644 --- a/LibGit2Sharp/TreeChanges.cs +++ b/LibGit2Sharp/TreeChanges.cs @@ -60,6 +60,7 @@ private int PrintCallBack(GitDiffDelta delta, GitDiffRange range, GitDiffLineOri { return 0; } + AddLineChange(currentChange, lineorigin); currentChange.AppendToPatch(formattedoutput); @@ -102,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; }