diff --git a/LibGit2Sharp.Tests/CherryPickFixture.cs b/LibGit2Sharp.Tests/CherryPickFixture.cs index 96232b4af..1bbff1428 100644 --- a/LibGit2Sharp.Tests/CherryPickFixture.cs +++ b/LibGit2Sharp.Tests/CherryPickFixture.cs @@ -126,6 +126,42 @@ public void CanSpecifyConflictFileStrategy(CheckoutFileConflictStrategy conflict } } + [Fact] + public void CanCherryPickCommit() + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + var ours = repo.Head.Tip; + + Commit commitToMerge = repo.Branches["fast_forward"].Tip; + + var result = repo.ObjectDatabase.CherryPickCommit(commitToMerge, ours, 0, null); + + Assert.Equal(MergeTreeStatus.Succeeded, result.Status); + Assert.Equal(0, result.Conflicts.Count()); + } + } + + [Fact] + public void CherryPickWithConflictsReturnsConflicts() + { + const string conflictBranchName = "conflicts"; + + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + Branch branch = repo.Branches[conflictBranchName]; + Assert.NotNull(branch); + + var result = repo.ObjectDatabase.CherryPickCommit(branch.Tip, repo.Head.Tip, 0, null); + + Assert.Equal(MergeTreeStatus.Conflicts, result.Status); + Assert.NotEmpty(result.Conflicts); + + } + } + private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null) { Touch(repository.Info.WorkingDirectory, filename, content); diff --git a/LibGit2Sharp.Tests/RevertFixture.cs b/LibGit2Sharp.Tests/RevertFixture.cs index 3fe517345..cf17dcfe1 100644 --- a/LibGit2Sharp.Tests/RevertFixture.cs +++ b/LibGit2Sharp.Tests/RevertFixture.cs @@ -458,5 +458,70 @@ public void RevertOrphanedBranchThrows() Assert.Throws(() => repo.Revert(commitToRevert, Constants.Signature)); } } + + [Fact] + public void RevertWithNothingToRevertInObjectDatabaseSucceeds() + { + // The branch name to perform the revert on + const string revertBranchName = "refs/heads/revert"; + + string path = SandboxRevertTestRepo(); + using (var repo = new Repository(path)) + { + // Checkout the revert branch. + Branch branch = Commands.Checkout(repo, revertBranchName); + Assert.NotNull(branch); + + Commit commitToRevert = repo.Head.Tip; + + // Revert tip commit. + RevertResult result = repo.Revert(commitToRevert, Constants.Signature); + Assert.NotNull(result); + Assert.Equal(RevertStatus.Reverted, result.Status); + + var revertResult = repo.ObjectDatabase.RevertCommit(commitToRevert, repo.Branches[revertBranchName].Tip, 0, null); + + Assert.NotNull(revertResult); + Assert.Equal(MergeTreeStatus.Succeeded, revertResult.Status); + } + } + + [Fact] + public void RevertWithConflictReportsConflict() + { + // The branch name to perform the revert on, + // and the file whose contents we expect to be reverted. + const string revertBranchName = "refs/heads/revert"; + + string path = SandboxRevertTestRepo(); + using (var repo = new Repository(path)) + { + // The commit to revert - we know that reverting this + // specific commit will generate conflicts. + Commit commitToRevert = repo.Lookup("cb4f7f0eca7a0114cdafd8537332aa17de36a4e9"); + Assert.NotNull(commitToRevert); + + // Perform the revert and verify there were conflicts. + var result = repo.ObjectDatabase.RevertCommit(commitToRevert, repo.Branches[revertBranchName].Tip, 0, null); + Assert.NotNull(result); + Assert.Equal(MergeTreeStatus.Conflicts, result.Status); + Assert.Null(result.Tree); + } + } + + [Fact] + public void CanRevertInObjectDatabase() + { + // The branch name to perform the revert on + const string revertBranchName = "refs/heads/revert"; + + string path = SandboxRevertTestRepo(); + using (var repo = new Repository(path)) + { + // Revert tip commit. + var result = repo.ObjectDatabase.RevertCommit(repo.Branches[revertBranchName].Tip, repo.Branches[revertBranchName].Tip, 0, null); + Assert.Equal(MergeTreeStatus.Succeeded, result.Status); + } + } } } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 846617f2c..fada41672 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1503,6 +1503,15 @@ internal static extern unsafe int git_revert( git_object* commit, GitRevertOpts opts); + [DllImport(libgit2)] + internal static extern unsafe int git_revert_commit( + out git_index* index, + git_repository* repo, + git_object* revert_commit, + git_object* our_commit, + uint mainline, + ref GitMergeOpts opts); + [DllImport(libgit2)] internal static extern unsafe int git_revparse_ext( out git_object* obj, @@ -1862,6 +1871,14 @@ internal static extern unsafe int git_treebuilder_insert( [DllImport(libgit2)] internal static extern unsafe int git_cherrypick(git_repository* repo, git_object* commit, GitCherryPickOptions options); + [DllImport(libgit2)] + internal static extern unsafe int git_cherrypick_commit(out git_index* index, + git_repository* repo, + git_object* cherrypick_commit, + git_object* our_commit, + uint mainline, + ref GitMergeOpts options); + [DllImport(libgit2)] internal static extern int git_transaction_commit(IntPtr txn); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index fbf7d2b3e..cc3030663 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -325,6 +325,22 @@ internal static unsafe void git_cherrypick(RepositoryHandle repo, ObjectId commi Ensure.ZeroResult(res); } } + + internal static unsafe IndexHandle git_cherrypick_commit(RepositoryHandle repo, ObjectHandle cherrypickCommit, ObjectHandle ourCommit, uint mainline, GitMergeOpts opts, out bool earlyStop) + { + git_index* index; + int res = NativeMethods.git_cherrypick_commit(out index, repo, cherrypickCommit, ourCommit, mainline, ref opts); + if (res == (int)GitErrorCode.MergeConflict) + { + earlyStop = true; + } + else + { + earlyStop = false; + Ensure.ZeroResult(res); + } + return new IndexHandle(index, true); + } #endregion #region git_clone_ @@ -2654,6 +2670,21 @@ public static unsafe void git_revert( } } + internal static unsafe IndexHandle git_revert_commit(RepositoryHandle repo, ObjectHandle revertCommit, ObjectHandle ourCommit, uint mainline, GitMergeOpts opts, out bool earlyStop) + { + git_index* index; + int res = NativeMethods.git_revert_commit(out index, repo, revertCommit, ourCommit, mainline, ref opts); + if (res == (int)GitErrorCode.MergeConflict) + { + earlyStop = true; + } + else + { + earlyStop = false; + Ensure.ZeroResult(res); + } + return new IndexHandle(index, true); + } #endregion #region git_revparse_ diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 1b737bf3e..679d67561 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -393,4 +393,4 @@ --> - + \ No newline at end of file diff --git a/LibGit2Sharp/ObjectDatabase.cs b/LibGit2Sharp/ObjectDatabase.cs index 920811157..c662a478c 100644 --- a/LibGit2Sharp/ObjectDatabase.cs +++ b/LibGit2Sharp/ObjectDatabase.cs @@ -541,6 +541,80 @@ public virtual HistoryDivergence CalculateHistoryDivergence(Commit one, Commit a return new HistoryDivergence(repo, one, another); } + /// + /// Performs a cherry-pick of onto commit. + /// + /// The commit to cherry-pick. + /// The commit to cherry-pick onto. + /// Which commit to consider the parent for the diff when cherry-picking a merge commit. + /// The options for the merging in the cherry-pick operation. + /// A result containing a if the cherry-pick was successful and a list of s if it is not. + public virtual MergeTreeResult CherryPickCommit(Commit cherryPickCommit, Commit cherryPickOnto, int mainline, MergeTreeOptions options) + { + Ensure.ArgumentNotNull(cherryPickCommit, "cherryPickCommit"); + Ensure.ArgumentNotNull(cherryPickOnto, "ours"); + + options = options ?? new MergeTreeOptions(); + + // We throw away the index after looking at the conflicts, so we'll never need the REUC + // entries to be there + GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL | GitMergeFlag.GIT_MERGE_SKIP_REUC; + if (options.FindRenames) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES; + } + if (options.FailOnConflict) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT; + } + + + var opts = new GitMergeOpts + { + Version = 1, + MergeFileFavorFlags = options.MergeFileFavor, + MergeTreeFlags = mergeFlags, + RenameThreshold = (uint)options.RenameThreshold, + TargetLimit = (uint)options.TargetLimit + }; + + bool earlyStop; + + using (var cherryPickOntoHandle = Proxy.git_object_lookup(repo.Handle, cherryPickOnto.Id, GitObjectType.Commit)) + using (var cherryPickCommitHandle = Proxy.git_object_lookup(repo.Handle, cherryPickCommit.Id, GitObjectType.Commit)) + using (var indexHandle = Proxy.git_cherrypick_commit(repo.Handle, cherryPickCommitHandle, cherryPickOntoHandle, (uint)mainline, opts, out earlyStop)) + { + MergeTreeResult cherryPickResult; + + // Stopped due to FailOnConflict so there's no index or conflict list + if (earlyStop) + { + return new MergeTreeResult(new Conflict[] { }); + } + + if (Proxy.git_index_has_conflicts(indexHandle)) + { + List conflicts = new List(); + Conflict conflict; + using (ConflictIteratorHandle iterator = Proxy.git_index_conflict_iterator_new(indexHandle)) + { + while ((conflict = Proxy.git_index_conflict_next(iterator)) != null) + { + conflicts.Add(conflict); + } + } + cherryPickResult = new MergeTreeResult(conflicts); + } + else + { + var treeId = Proxy.git_index_write_tree_to(indexHandle, repo.Handle); + cherryPickResult = new MergeTreeResult(this.repo.Lookup(treeId)); + } + + return cherryPickResult; + } + } + /// /// Calculates the current shortest abbreviated /// string representation for a . @@ -806,5 +880,79 @@ private PackBuilderResults InternalPack(PackBuilderOptions options, Action + /// Performs a revert of onto commit. + /// + /// The commit to revert. + /// The commit to revert onto. + /// Which commit to consider the parent for the diff when reverting a merge commit. + /// The options for the merging in the revert operation. + /// A result containing a if the revert was successful and a list of s if it is not. + public virtual MergeTreeResult RevertCommit(Commit revertCommit, Commit revertOnto, int mainline, MergeTreeOptions options) + { + Ensure.ArgumentNotNull(revertCommit, "revertCommit"); + Ensure.ArgumentNotNull(revertOnto, "revertOnto"); + + options = options ?? new MergeTreeOptions(); + + // We throw away the index after looking at the conflicts, so we'll never need the REUC + // entries to be there + GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL | GitMergeFlag.GIT_MERGE_SKIP_REUC; + if (options.FindRenames) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES; + } + if (options.FailOnConflict) + { + mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT; + } + + + var opts = new GitMergeOpts + { + Version = 1, + MergeFileFavorFlags = options.MergeFileFavor, + MergeTreeFlags = mergeFlags, + RenameThreshold = (uint)options.RenameThreshold, + TargetLimit = (uint)options.TargetLimit + }; + + bool earlyStop; + + using (var revertOntoHandle = Proxy.git_object_lookup(repo.Handle, revertOnto.Id, GitObjectType.Commit)) + using (var revertCommitHandle = Proxy.git_object_lookup(repo.Handle, revertCommit.Id, GitObjectType.Commit)) + using (var indexHandle = Proxy.git_revert_commit(repo.Handle, revertCommitHandle, revertOntoHandle, (uint)mainline, opts, out earlyStop)) + { + MergeTreeResult revertTreeResult; + + // Stopped due to FailOnConflict so there's no index or conflict list + if (earlyStop) + { + return new MergeTreeResult(new Conflict[] { }); + } + + if (Proxy.git_index_has_conflicts(indexHandle)) + { + List conflicts = new List(); + Conflict conflict; + using (ConflictIteratorHandle iterator = Proxy.git_index_conflict_iterator_new(indexHandle)) + { + while ((conflict = Proxy.git_index_conflict_next(iterator)) != null) + { + conflicts.Add(conflict); + } + } + revertTreeResult = new MergeTreeResult(conflicts); + } + else + { + var treeId = Proxy.git_index_write_tree_to(indexHandle, repo.Handle); + revertTreeResult = new MergeTreeResult(this.repo.Lookup(treeId)); + } + + return revertTreeResult; + } + } } }