diff --git a/LibGit2Sharp.Tests/StashFixture.cs b/LibGit2Sharp.Tests/StashFixture.cs index c630d5746..ee413c921 100644 --- a/LibGit2Sharp.Tests/StashFixture.cs +++ b/LibGit2Sharp.Tests/StashFixture.cs @@ -204,6 +204,156 @@ public void CanStashIgnoredFiles() } } + [Fact] + public void CanStashAndApplyWithOptions() + { + string path = SandboxStandardTestRepo(); + using (var repo = new Repository(path)) + { + var stasher = Constants.Signature; + + const string filename = "staged_file_path.txt"; + Touch(repo.Info.WorkingDirectory, filename, "I'm staged\n"); + repo.Stage(filename); + + repo.Stashes.Add(stasher, "This stash with default options"); + Assert.Equal(StashApplyStatus.Applied, repo.Stashes.Apply(0)); + + Assert.Equal(FileStatus.NewInWorkdir, repo.RetrieveStatus(filename)); + Assert.Equal(1, repo.Stashes.Count()); + + repo.Stage(filename); + + repo.Stashes.Add(stasher, "This stash with default options"); + Assert.Equal(StashApplyStatus.Applied, repo.Stashes.Apply( + 0, + new StashApplyOptions + { + ApplyModifiers = StashApplyModifiers.ReinstateIndex, + })); + + Assert.Equal(FileStatus.NewInIndex, repo.RetrieveStatus(filename)); + Assert.Equal(2, repo.Stashes.Count()); + } + } + + [Fact] + public void CanStashAndPop() + { + string path = SandboxStandardTestRepo(); + using (var repo = new Repository(path)) + { + var stasher = Constants.Signature; + + Assert.Equal(0, repo.Stashes.Count()); + + const string filename = "staged_file_path.txt"; + const string contents = "I'm staged"; + Touch(repo.Info.WorkingDirectory, filename, contents); + repo.Stage(filename); + + repo.Stashes.Add(stasher, "This stash with default options"); + Assert.Equal(1, repo.Stashes.Count()); + + Assert.Equal(StashApplyStatus.Applied, repo.Stashes.Pop(0)); + Assert.Equal(0, repo.Stashes.Count()); + + Assert.Equal(FileStatus.NewInWorkdir, repo.RetrieveStatus(filename)); + Assert.Equal(contents, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, filename))); + } + } + + [Fact] + public void StashReportsConflictsWhenReinstated() + { + string path = SandboxStandardTestRepo(); + using (var repo = new Repository(path)) + { + var stasher = Constants.Signature; + + const string filename = "staged_file_path.txt"; + const string originalContents = "I'm pre-stash."; + const string filename2 = "unstaged_file_path.txt"; + const string newContents = "I'm post-stash."; + + Touch(repo.Info.WorkingDirectory, filename, originalContents); + repo.Stage(filename); + Touch(repo.Info.WorkingDirectory, filename2, originalContents); + + repo.Stashes.Add(stasher, "This stash with default options"); + + Touch(repo.Info.WorkingDirectory, filename, newContents); + repo.Stage(filename); + Touch(repo.Info.WorkingDirectory, filename2, newContents); + + Assert.Equal(StashApplyStatus.Conflicts, repo.Stashes.Pop(0, new StashApplyOptions + { + ApplyModifiers = StashApplyModifiers.ReinstateIndex, + })); + Assert.Equal(1, repo.Stashes.Count()); + Assert.Equal(newContents, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, filename))); + Assert.Equal(newContents, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, filename2))); + } + } + + [Fact] + public void StashCallsTheCallback() + { + string path = SandboxStandardTestRepo(); + using (var repo = new Repository(path)) + { + var stasher = Constants.Signature; + bool called; + + const string filename = "staged_file_path.txt"; + const string filename2 = "unstaged_file_path.txt"; + const string originalContents = "I'm pre-stash."; + + Touch(repo.Info.WorkingDirectory, filename, originalContents); + repo.Stage(filename); + Touch(repo.Info.WorkingDirectory, filename2, originalContents); + + repo.Stashes.Add(stasher, "This stash with default options"); + + called = false; + repo.Stashes.Apply(0, new StashApplyOptions + { + ProgressHandler = (progress) => { called = true; return true; } + }); + + Assert.Equal(true, called); + + repo.Reset(ResetMode.Hard); + + called = false; + repo.Stashes.Pop(0, new StashApplyOptions + { + ProgressHandler = (progress) => { called = true; return true; } + }); + + Assert.Equal(true, called); + } + } + + [Fact] + public void StashApplyReportsNotFound() + { + string path = SandboxStandardTestRepo(); + using (var repo = new Repository(path)) + { + var stasher = Constants.Signature; + + const string filename = "unstaged_file_path.txt"; + Touch(repo.Info.WorkingDirectory, filename, "I'm unstaged\n"); + + repo.Stashes.Add(stasher, "This stash with default options", StashModifiers.IncludeUntracked); + Touch(repo.Info.WorkingDirectory, filename, "I'm another unstaged\n"); + + Assert.Equal(StashApplyStatus.NotFound, repo.Stashes.Pop(1)); + Assert.Throws(() => repo.Stashes.Pop(-1)); + } + } + [Theory] [InlineData(-1)] [InlineData(-42)] diff --git a/LibGit2Sharp/Core/GitStashApplyOpts.cs b/LibGit2Sharp/Core/GitStashApplyOpts.cs new file mode 100644 index 000000000..e7f2be19c --- /dev/null +++ b/LibGit2Sharp/Core/GitStashApplyOpts.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + internal delegate int stash_apply_progress_cb(StashApplyProgress progress, IntPtr payload); + + [StructLayout(LayoutKind.Sequential)] + internal class GitStashApplyOpts + { + public uint Version = 1; + + public StashApplyModifiers Flags; + public GitCheckoutOpts CheckoutOptions; + + public stash_apply_progress_cb ApplyProgressCallback; + public IntPtr ProgressPayload; + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 9b4e818f3..819d0769e 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1366,6 +1366,18 @@ internal static extern int git_stash_foreach( [DllImport(libgit2)] internal static extern int git_stash_drop(RepositorySafeHandle repo, UIntPtr index); + [DllImport(libgit2)] + internal static extern int git_stash_apply( + RepositorySafeHandle repo, + UIntPtr index, + GitStashApplyOpts opts); + + [DllImport(libgit2)] + internal static extern int git_stash_pop( + RepositorySafeHandle repo, + UIntPtr index, + GitStashApplyOpts opts); + [DllImport(libgit2)] internal static extern int git_status_file( out FileStatus statusflags, diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 65a389a08..0e437caba 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -2452,6 +2452,38 @@ public static void git_stash_drop(RepositorySafeHandle repo, int index) Ensure.BooleanResult(res); } + private static StashApplyStatus get_stash_status(int res) + { + if (res == (int)GitErrorCode.Conflict) + { + return StashApplyStatus.Conflicts; + } + + if (res == (int)GitErrorCode.NotFound) + { + return StashApplyStatus.NotFound; + } + + Ensure.ZeroResult(res); + return StashApplyStatus.Applied; + } + + public static StashApplyStatus git_stash_apply( + RepositorySafeHandle repo, + int index, + GitStashApplyOpts opts) + { + return get_stash_status(NativeMethods.git_stash_apply(repo, (UIntPtr)index, opts)); + } + + public static StashApplyStatus git_stash_pop( + RepositorySafeHandle repo, + int index, + GitStashApplyOpts opts) + { + return get_stash_status(NativeMethods.git_stash_pop(repo, (UIntPtr)index, opts)); + } + #endregion #region git_status_ diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs index 196b438fd..6d01c512e 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -112,6 +112,13 @@ namespace LibGit2Sharp.Handlers /// The refspec which didn't match the default. public delegate void RemoteRenameFailureHandler(string problematicRefspec); + /// + /// Delegate definition for stash application callback. + /// + /// The current step of the stash application. + /// True to continue checkout operation; false to cancel checkout operation. + public delegate bool StashApplyProgressHandler(StashApplyProgress progress); + /// /// The stages of pack building. /// diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index c20b0933b..545f74030 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -357,6 +357,10 @@ + + + + diff --git a/LibGit2Sharp/StashApplyOptions.cs b/LibGit2Sharp/StashApplyOptions.cs new file mode 100644 index 000000000..0af54829d --- /dev/null +++ b/LibGit2Sharp/StashApplyOptions.cs @@ -0,0 +1,48 @@ +using System; +using LibGit2Sharp.Core; +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp +{ + /// + /// The options to be used for stash application. + /// + public sealed class StashApplyOptions + { + /// + /// for controlling checkout index reinstating./> + /// + /// The flags. + public StashApplyModifiers ApplyModifiers { get; set; } + + /// + /// controlling checkout behavior. + /// + /// The checkout options. + public CheckoutOptions CheckoutOptions { get; set; } + + /// + /// for controlling stash application progress./> + /// + /// The progress handler. + public StashApplyProgressHandler ProgressHandler { get; set; } + } + + /// + /// The flags which control whether the index should be reinstated. + /// + [Flags] + public enum StashApplyModifiers + { + /// + /// Default. Will apply the stash and result in an index with conflicts + /// if any arise. + /// + Default = 0, + + /// + /// In case any conflicts arise, this will not apply the stash. + /// + ReinstateIndex = (1 << 0), + } +} diff --git a/LibGit2Sharp/StashApplyProgress.cs b/LibGit2Sharp/StashApplyProgress.cs new file mode 100644 index 000000000..c96839769 --- /dev/null +++ b/LibGit2Sharp/StashApplyProgress.cs @@ -0,0 +1,50 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// The current progress of the stash application. + /// + public enum StashApplyProgress + { + /// + /// Not passed by the callback. Used as dummy value. + /// + None = 0, + + /// + /// Loading the stashed data from the object database. + /// + LoadingStash, + + /// + /// The stored index is being analyzed. + /// + AnalyzeIndex, + + /// + /// The modified files are being analyzed. + /// + AnalyzeModified, + + /// + /// The untracked and ignored files are being analyzed. + /// + AnalyzeUntracked, + + /// + /// The untracked files are being written to disk. + /// + CheckoutUntracked, + + /// + /// The modified files are being written to disk. + /// + CheckoutModified, + + /// + /// The stash was applied successfully. + /// + Done, + } +} diff --git a/LibGit2Sharp/StashApplyStatus.cs b/LibGit2Sharp/StashApplyStatus.cs new file mode 100644 index 000000000..25ab991a4 --- /dev/null +++ b/LibGit2Sharp/StashApplyStatus.cs @@ -0,0 +1,25 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// The result of a stash application operation. + /// + public enum StashApplyStatus + { + /// + /// The stash application was successful. + /// + Applied, + + /// + /// The stash application ended up with conflicts. + /// + Conflicts, + + /// + /// The stash index given was not found. + /// + NotFound, + } +} diff --git a/LibGit2Sharp/StashCollection.cs b/LibGit2Sharp/StashCollection.cs index 1bc509a27..4cae63703 100644 --- a/LibGit2Sharp/StashCollection.cs +++ b/LibGit2Sharp/StashCollection.cs @@ -131,6 +131,92 @@ public virtual Stash Add(Signature stasher, string message, StashModifiers optio return new Stash(repo, oid, 0); } + /// + /// Applies a single stashed state from the stash list + /// + /// the index of the stash to remove (0 being the most recent one). + /// the options to use for checking out the stash. + public virtual StashApplyStatus Apply(int index, StashApplyOptions options) + { + if (index < 0) + { + throw new ArgumentException("The passed index must be a positive integer.", "index"); + } + + if (options == null) + { + options = new StashApplyOptions(); + } + + using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options.CheckoutOptions ?? new CheckoutOptions())) + { + var opts = new GitStashApplyOpts + { + CheckoutOptions = checkoutOptionsWrapper.Options, + Flags = options.ApplyModifiers, + }; + + if (options.ProgressHandler != null) + { + opts.ApplyProgressCallback = (progress, payload) => options.ProgressHandler(progress) ? 0 : -1; + } + + return Proxy.git_stash_apply(repo.Handle, index, opts); + } + } + + /// + /// Applies a single stashed state from the stash list using the default options. + /// + /// the index of the stash to remove (0 being the most recent one). + public virtual StashApplyStatus Apply(int index) + { + return Apply(index, null); + } + + /// + /// Pops a single stashed state from the stash list + /// + /// the index of the stash to remove (0 being the most recent one). + /// the options to use for checking out the stash. + public virtual StashApplyStatus Pop(int index, StashApplyOptions options) + { + if (index < 0) + { + throw new ArgumentException("The passed index must be a positive integer.", "index"); + } + + if (options == null) + { + options = new StashApplyOptions(); + } + + using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options.CheckoutOptions ?? new CheckoutOptions())) + { + var opts = new GitStashApplyOpts + { + CheckoutOptions = checkoutOptionsWrapper.Options, + Flags = options.ApplyModifiers, + }; + + if (options.ProgressHandler != null) + { + opts.ApplyProgressCallback = (progress, payload) => options.ProgressHandler(progress) ? 0 : -1; + } + + return Proxy.git_stash_pop(repo.Handle, index, opts); + } + } + + /// + /// Pops a single stashed state from the stash list using the default options. + /// + /// the index of the stash to remove (0 being the most recent one). + public virtual StashApplyStatus Pop(int index) + { + return Pop(index, null); + } + /// /// Remove a single stashed state from the stash list. ///