Skip to content

Add Cherry-pick and Revert functionality to ObjectDatabase #1323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions LibGit2Sharp.Tests/CherryPickFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
65 changes: 65 additions & 0 deletions LibGit2Sharp.Tests/RevertFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,5 +458,70 @@ public void RevertOrphanedBranchThrows()
Assert.Throws<UnbornBranchException>(() => 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<Commit>("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);
}
}
}
}
17 changes: 17 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
31 changes: 31 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down Expand Up @@ -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_
Expand Down
2 changes: 1 addition & 1 deletion LibGit2Sharp/LibGit2Sharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,4 @@
</Target>
-->
<ItemGroup />
</Project>
</Project>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit harder for generated files - but instead of fighting about the trailing newline, could you go back and add it here?

148 changes: 148 additions & 0 deletions LibGit2Sharp/ObjectDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,80 @@ public virtual HistoryDivergence CalculateHistoryDivergence(Commit one, Commit a
return new HistoryDivergence(repo, one, another);
}

/// <summary>
/// Performs a cherry-pick of <paramref name="cherryPickCommit"/> onto <paramref name="cherryPickOnto"/> commit.
/// </summary>
/// <param name="cherryPickCommit">The commit to cherry-pick.</param>
/// <param name="cherryPickOnto">The commit to cherry-pick onto.</param>
/// <param name="mainline">Which commit to consider the parent for the diff when cherry-picking a merge commit.</param>
/// <param name="options">The options for the merging in the cherry-pick operation.</param>
/// <returns>A result containing a <see cref="Tree"/> if the cherry-pick was successful and a list of <see cref="Conflict"/>s if it is not.</returns>
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<Conflict> conflicts = new List<Conflict>();
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<Tree>(treeId));
}

return cherryPickResult;
}
}

/// <summary>
/// Calculates the current shortest abbreviated <see cref="ObjectId"/>
/// string representation for a <see cref="GitObject"/>.
Expand Down Expand Up @@ -806,5 +880,79 @@ private PackBuilderResults InternalPack(PackBuilderOptions options, Action<PackB

return results;
}

/// <summary>
/// Performs a revert of <paramref name="revertCommit"/> onto <paramref name="revertOnto"/> commit.
/// </summary>
/// <param name="revertCommit">The commit to revert.</param>
/// <param name="revertOnto">The commit to revert onto.</param>
/// <param name="mainline">Which commit to consider the parent for the diff when reverting a merge commit.</param>
/// <param name="options">The options for the merging in the revert operation.</param>
/// <returns>A result containing a <see cref="Tree"/> if the revert was successful and a list of <see cref="Conflict"/>s if it is not.</returns>
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<Conflict> conflicts = new List<Conflict>();
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<Tree>(treeId));
}

return revertTreeResult;
}
}
}
}