diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 647303025..2d54be3da 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -58,6 +58,7 @@ + diff --git a/LibGit2Sharp.Tests/StashFixture.cs b/LibGit2Sharp.Tests/StashFixture.cs new file mode 100644 index 000000000..a18f634c5 --- /dev/null +++ b/LibGit2Sharp.Tests/StashFixture.cs @@ -0,0 +1,191 @@ +using System; +using System.IO; +using System.Linq; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class StashFixture : BaseFixture + { + [Fact] + public void CannotAddStashAgainstBareRepository() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + Assert.Throws(() => repo.Stashes.Add(stasher, "My very first stash")); + } + } + + [Fact] + public void CanAddStash() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + Assert.True(repo.Index.RetrieveStatus().IsDirty); + + Stash stash = repo.Stashes.Add(stasher, "My very first stash", StashOptions.IncludeUntracked); + + // Check that untracked files are deleted from working directory + Assert.False(File.Exists(Path.Combine(repo.Info.WorkingDirectory, "new_untracked_file.txt"))); + + Assert.NotNull(stash); + Assert.Equal("stash@{0}", stash.CanonicalName); + Assert.Contains("My very first stash", stash.Message); + + var stashRef = repo.Refs["refs/stash"]; + Assert.Equal(stash.Target.Sha, stashRef.TargetIdentifier); + + Assert.False(repo.Index.RetrieveStatus().IsDirty); + + // Create extra file + string newFileFullPath = Path.Combine(repo.Info.WorkingDirectory, "stash_candidate.txt"); + File.WriteAllText(newFileFullPath, "Oh, I'm going to be stashed!\n"); + + Stash secondStash = repo.Stashes.Add(stasher, "My second stash", StashOptions.IncludeUntracked); + + Assert.NotNull(stash); + Assert.Equal("stash@{0}", stash.CanonicalName); + Assert.Contains("My second stash", secondStash.Message); + + Assert.Equal(2, repo.Stashes.Count()); + Assert.Equal("stash@{0}", repo.Stashes.First().CanonicalName); + Assert.Equal("stash@{1}", repo.Stashes.Last().CanonicalName); + + Assert.Equal(2, repo.Stashes.Count()); + Assert.Equal("stash@{0}", repo.Stashes.First().CanonicalName); + Assert.Equal("stash@{1}", repo.Stashes.Last().CanonicalName); + + // Stash history has been shifted + Assert.Equal(repo.Lookup("stash@{0}").Sha, secondStash.Target.Sha); + Assert.Equal(repo.Lookup("stash@{1}").Sha, stash.Target.Sha); + } + } + + [Fact] + public void AddingAStashWithNoMessageGeneratesADefaultOne() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + Stash stash = repo.Stashes.Add(stasher); + + Assert.NotNull(stash); + Assert.Equal("stash@{0}", stash.CanonicalName); + Assert.NotEmpty(stash.Target.Message); + + var stashRef = repo.Refs["refs/stash"]; + Assert.Equal(stash.Target.Sha, stashRef.TargetIdentifier); + } + } + + [Fact] + public void AddStashWithBadParamsShouldThrows() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + Assert.Throws(() => repo.Stashes.Add(null)); + } + } + + [Fact] + public void StashingAgainstCleanWorkDirShouldReturnANullStash() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + Stash stash = repo.Stashes.Add(stasher, "My very first stash", StashOptions.IncludeUntracked); + + Assert.NotNull(stash); + + //Stash against clean working directory + Assert.Null(repo.Stashes.Add(stasher)); + } + } + + [Fact] + public void CanStashWithoutOptions() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + var untrackedFilePath = Path.Combine(repo.Info.WorkingDirectory, "new_untracked_file.txt"); + File.WriteAllText(untrackedFilePath, "I'm untracked\n"); + + string stagedfilePath = Path.Combine(repo.Info.WorkingDirectory, "staged_file_path.txt"); + File.WriteAllText(stagedfilePath, "I'm staged\n"); + repo.Index.Stage(stagedfilePath); + + Stash stash = repo.Stashes.Add(stasher, "Stash with default options"); + + Assert.NotNull(stash); + + //It should not keep staged files + Assert.Equal(FileStatus.Nonexistent, repo.Index.RetrieveStatus("staged_file_path.txt")); + + //It should leave untracked files untracked + Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus("new_untracked_file.txt")); + } + } + + [Fact] + public void CanStashAndKeepIndex() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + using (var repo = new Repository(path.RepositoryPath)) + { + var stasher = DummySignature; + + string stagedfilePath = Path.Combine(repo.Info.WorkingDirectory, "staged_file_path.txt"); + File.WriteAllText(stagedfilePath, "I'm staged\n"); + repo.Index.Stage(stagedfilePath); + + Stash stash = repo.Stashes.Add(stasher, "This stash wil keep index", StashOptions.KeepIndex); + + Assert.NotNull(stash); + Assert.Equal(FileStatus.Added, repo.Index.RetrieveStatus("staged_file_path.txt")); + } + } + + [Fact] + public void CanStashIgnoredFiles() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath); + + using (var repo = new Repository(path.RepositoryPath)) + { + string gitIgnoreFilePath = Path.Combine(repo.Info.WorkingDirectory, ".gitignore"); + File.WriteAllText(gitIgnoreFilePath, "ignored_file.txt"); + repo.Index.Stage(gitIgnoreFilePath); + repo.Commit("Modify gitignore", Constants.Signature, Constants.Signature); + + string ignoredFilePath = Path.Combine(repo.Info.WorkingDirectory, "ignored_file.txt"); + File.WriteAllText(ignoredFilePath, "I'm ignored\n"); + + Assert.True(repo.Ignore.IsPathIgnored("ignored_file.txt")); + + var stasher = DummySignature; + repo.Stashes.Add(stasher, "This stash includes ignore files", StashOptions.IncludeIgnored); + + //TODO : below assertion doesn't pass. Bug? + //Assert.False(File.Exists(ignoredFilePath)); + + var blob = repo.Lookup("stash^3:ignored_file.txt"); + Assert.NotNull(blob); + } + } + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index a3f1d9f00..18d30e772 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -904,6 +904,26 @@ internal static extern int git_signature_new( long time, int offset); + [DllImport(libgit2)] + internal static extern int git_stash_save( + out GitOid id, + RepositorySafeHandle repo, + SignatureSafeHandle stasher, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8Marshaler))] string message, + StashOptions flags); + + internal delegate int git_stash_cb( + UIntPtr index, + IntPtr message, + ref GitOid stash_id, + IntPtr payload); + + [DllImport(libgit2)] + internal static extern int git_stash_foreach( + RepositorySafeHandle repo, + git_stash_cb callback, + IntPtr payload); + [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 a162f9d39..627aabbeb 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -1747,6 +1747,45 @@ public static SignatureSafeHandle git_signature_new(string name, string email, D #endregion + #region git_stash_ + + public static ObjectId git_stash_save( + RepositorySafeHandle repo, + Signature stasher, + string prettifiedMessage, + StashOptions options) + { + using (ThreadAffinity()) + using (SignatureSafeHandle stasherHandle = stasher.BuildHandle()) + { + GitOid stashOid; + + int res = NativeMethods.git_stash_save(out stashOid, repo, stasherHandle, prettifiedMessage, options); + + if (res == (int)GitErrorCode.NotFound) + { + return null; + } + + Ensure.Int32Result(res); + + return new ObjectId(stashOid); + } + } + + public static ICollection git_stash_foreach( + RepositorySafeHandle repo, + Func resultSelector) + { + return git_foreach( + resultSelector, + c => NativeMethods.git_stash_foreach( + repo, (UIntPtr i, IntPtr m, ref GitOid x, IntPtr p) => c((int)i, m, x, p), IntPtr.Zero), + GitErrorCode.NotFound); + } + + #endregion + #region git_status_ public static FileStatus git_status_file(RepositorySafeHandle repo, FilePath path) @@ -2012,6 +2051,30 @@ private static ICollection git_foreach( } } + private static ICollection git_foreach( + Func resultSelector, + Func, int> iterator, + params GitErrorCode[] ignoredErrorCodes) + { + using (ThreadAffinity()) + { + var result = new List(); + var res = iterator((w, x, y, payload) => + { + result.Add(resultSelector(w, x, y)); + return 0; + }); + + if (ignoredErrorCodes != null && ignoredErrorCodes.Contains((GitErrorCode)res)) + { + return new TResult[0]; + } + + Ensure.ZeroResult(res); + return result; + } + } + public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5); private static ICollection git_foreach( diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 42cf99e08..5f65b6127 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -71,7 +71,10 @@ + + + diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index f17276a6a..75df86aa0 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -28,6 +28,7 @@ public class Repository : IRepository private readonly ConflictCollection conflicts; private readonly ReferenceCollection refs; private readonly TagCollection tags; + private readonly StashCollection stashes; private readonly Lazy info; private readonly Diff diff; private readonly NoteCollection notes; @@ -103,6 +104,7 @@ public Repository(string path, RepositoryOptions options = null) refs = new ReferenceCollection(this); branches = new BranchCollection(this); tags = new TagCollection(this); + stashes = new StashCollection(this); info = new Lazy(() => new RepositoryInformation(this, isBare)); config = new Lazy( @@ -286,6 +288,14 @@ public TagCollection Tags get { return tags; } } + /// + /// Lookup and enumerate stashes in the repository. + /// + public StashCollection Stashes + { + get { return stashes; } + } + /// /// Provides high level information about this repository. /// diff --git a/LibGit2Sharp/Stash.cs b/LibGit2Sharp/Stash.cs new file mode 100644 index 000000000..e31c24b57 --- /dev/null +++ b/LibGit2Sharp/Stash.cs @@ -0,0 +1,40 @@ +namespace LibGit2Sharp +{ + /// + /// A Stash + /// A stash is a snapshot of the dirty state of the working directory (i.e. the modified tracked files and staged changes) + /// + public class Stash : ReferenceWrapper + { + /// + /// Needed for mocking purposes. + /// + protected Stash() + { } + + internal Stash(Repository repo, ObjectId targetId, int index) + : base(repo, new DirectReference(string.Format("stash@{{{0}}}", index), repo, targetId), r => r.CanonicalName) + { } + + /// + /// Gets the that this stash points to. + /// + public virtual Commit Target + { + get { return TargetObject; } + } + + /// + /// Gets the message associated to this . + /// + public virtual string Message + { + get { return Target.Message; } + } + + protected override string Shorten() + { + return CanonicalName; + } + } +} diff --git a/LibGit2Sharp/StashCollection.cs b/LibGit2Sharp/StashCollection.cs new file mode 100644 index 000000000..ed6b776b2 --- /dev/null +++ b/LibGit2Sharp/StashCollection.cs @@ -0,0 +1,89 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// The collection of es in a + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class StashCollection : IEnumerable + { + internal readonly Repository repo; + + /// + /// Needed for mocking purposes. + /// + protected StashCollection() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The repo. + internal StashCollection(Repository repo) + { + this.repo = repo; + } + + #region Implementation of IEnumerable + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return Proxy.git_stash_foreach(repo.Handle, + (index, message, commitId) => new Stash(repo, new ObjectId(commitId), index)).GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + /// + /// Creates a stash with the specified message. + /// + /// The of the user who stashes + /// The message of the stash. + /// A combination of flags + /// the newly created + public virtual Stash Add(Signature stasher, string message = null, StashOptions options = StashOptions.Default) + { + Ensure.ArgumentNotNull(stasher, "stasher"); + + string prettifiedMessage = Proxy.git_message_prettify(string.IsNullOrEmpty(message) ? string.Empty : message); + + ObjectId oid = Proxy.git_stash_save(repo.Handle, stasher, prettifiedMessage, options); + + // in case there is nothing to stash + if(oid == null) + { + return null; + } + + return new Stash(repo, oid, 0); + } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "Count = {0}", this.Count()); + } + } + } +} diff --git a/LibGit2Sharp/StashOptions.cs b/LibGit2Sharp/StashOptions.cs new file mode 100644 index 000000000..9933b27f2 --- /dev/null +++ b/LibGit2Sharp/StashOptions.cs @@ -0,0 +1,34 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// Options controlling Stash behavior. + /// + [Flags] + public enum StashOptions + { + /// + /// Default + /// + Default = 0, + + /// + /// All changes already added to the index + /// are left intact in the working directory + /// + KeepIndex = (1 << 0), + + /// + /// All untracked files are also stashed and then + /// cleaned up from the working directory + /// + IncludeUntracked = (1 << 1), + + /// + /// All ignored files are also stashed and then + /// cleaned up from the working directory + /// + IncludeIgnored = (1 << 2), + } +}