Skip to content

Introduce Repository.CherryPick. #756

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 1 commit into from
Jun 11, 2014
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
141 changes: 141 additions & 0 deletions LibGit2Sharp.Tests/CherryPickFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.IO;
using System.Linq;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
using Xunit.Extensions;
using System;

namespace LibGit2Sharp.Tests
{
public class CherryPickFixture : BaseFixture
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanCherryPick(bool fromDetachedHead)
{
string path = CloneMergeTestRepo();
using (var repo = new Repository(path))
{
if (fromDetachedHead)
{
repo.Checkout(repo.Head.Tip.Id.Sha);
}

Commit commitToMerge = repo.Branches["fast_forward"].Tip;

CherryPickResult result = repo.CherryPick(commitToMerge, Constants.Signature);

Assert.Equal(CherryPickStatus.CherryPicked, result.Status);
Assert.Equal(cherryPickedCommitId, result.Commit.Id.Sha);
Assert.False(repo.Index.RetrieveStatus().Any());
Assert.Equal(fromDetachedHead, repo.Info.IsHeadDetached);
Assert.Equal(commitToMerge.Author, result.Commit.Author);
Assert.Equal(Constants.Signature, result.Commit.Committer);
}
}

[Fact]
public void CherryPickWithConflictDoesNotCommit()
{
const string firstBranchFileName = "first branch file.txt";
const string secondBranchFileName = "second branch file.txt";
const string sharedBranchFileName = "first+second branch file.txt";

string path = CloneStandardTestRepo();
using (var repo = new Repository(path))
{
var firstBranch = repo.CreateBranch("FirstBranch");
firstBranch.Checkout();

// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
AddFileCommitToRepo(repo, sharedBranchFileName);

var secondBranch = repo.CreateBranch("SecondBranch");
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
AddFileCommitToRepo(repo, firstBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "The first branches comment"); // Change file in first branch

secondBranch.Checkout();
// Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
AddFileCommitToRepo(repo, secondBranchFileName);
AddFileCommitToRepo(repo, sharedBranchFileName, "The second branches comment"); // Change file in second branch

CherryPickResult cherryPickResult = repo.CherryPick(repo.Branches["FirstBranch"].Tip, Constants.Signature);

Assert.Equal(CherryPickStatus.Conflicts, cherryPickResult.Status);

Assert.Null(cherryPickResult.Commit);
Assert.Equal(1, repo.Index.Conflicts.Count());

var conflict = repo.Index.Conflicts.First();
var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));

Assert.False(changes.IsBinaryComparison);
}
}

[Theory]
[InlineData(CheckoutFileConflictStrategy.Ours)]
[InlineData(CheckoutFileConflictStrategy.Theirs)]
public void CanSpecifyConflictFileStrategy(CheckoutFileConflictStrategy conflictStrategy)
{
const string conflictFile = "a.txt";
const string conflictBranchName = "conflicts";

string path = CloneMergeTestRepo();
using (var repo = new Repository(path))
{
Branch branch = repo.Branches[conflictBranchName];
Assert.NotNull(branch);

CherryPickOptions cherryPickOptions = new CherryPickOptions()
{
FileConflictStrategy = conflictStrategy,
};

CherryPickResult result = repo.CherryPick(branch.Tip, Constants.Signature, cherryPickOptions);
Assert.Equal(CherryPickStatus.Conflicts, result.Status);

// Get the information on the conflict.
Conflict conflict = repo.Index.Conflicts[conflictFile];

Assert.NotNull(conflict);
Assert.NotNull(conflict.Theirs);
Assert.NotNull(conflict.Ours);

// Get the blob containing the expected content.
Blob expectedBlob = null;
switch (conflictStrategy)
{
case CheckoutFileConflictStrategy.Theirs:
expectedBlob = repo.Lookup<Blob>(conflict.Theirs.Id);
break;
case CheckoutFileConflictStrategy.Ours:
expectedBlob = repo.Lookup<Blob>(conflict.Ours.Id);
break;
default:
throw new Exception("Unexpected FileConflictStrategy");
}

Assert.NotNull(expectedBlob);

// Check the content of the file on disk matches what is expected.
string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictFile));
Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictFile)));
}
}

private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
{
Touch(repository.Info.WorkingDirectory, filename, content);

repository.Index.Stage(filename);

return repository.Commit("New commit", Constants.Signature, Constants.Signature);
}

// Commit IDs of the checked in merge_testrepo
private const string cherryPickedCommitId = "74b37f366b6e1c682c1c9fe0c6b006cbe909cf91";
}
}
1 change: 1 addition & 0 deletions LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<Compile Include="BlameFixture.cs" />
<Compile Include="ArchiveTarFixture.cs" />
<Compile Include="CheckoutFixture.cs" />
<Compile Include="CherryPickFixture.cs" />
<Compile Include="GlobalSettingsFixture.cs" />
<Compile Include="PatchStatsFixture.cs" />
<Compile Include="RefSpecFixture.cs" />
Expand Down
114 changes: 114 additions & 0 deletions LibGit2Sharp/CherryPickOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using LibGit2Sharp.Core;
using LibGit2Sharp.Handlers;

namespace LibGit2Sharp
{
/// <summary>
/// Options controlling CherryPick behavior.
/// </summary>
public sealed class CherryPickOptions : IConvertableToGitCheckoutOpts
{
/// <summary>
/// Initializes a new instance of the <see cref="CherryPickOptions"/> class.
/// By default the cherry pick will be committed if there are no conflicts.
/// </summary>
public CherryPickOptions()
{
CommitOnSuccess = true;

FindRenames = true;

// TODO: libgit2 should provide reasonable defaults for these
// values, but it currently does not.
RenameThreshold = 50;
TargetLimit = 200;
}

/// <summary>
/// The Flags specifying what conditions are
/// reported through the OnCheckoutNotify delegate.
/// </summary>
public CheckoutNotifyFlags CheckoutNotifyFlags { get; set; }

/// <summary>
/// Delegate that checkout progress will be reported through.
/// </summary>
public CheckoutProgressHandler OnCheckoutProgress { get; set; }

/// <summary>
/// Delegate that checkout will notify callers of
/// certain conditions. The conditions that are reported is
/// controlled with the CheckoutNotifyFlags property.
/// </summary>
public CheckoutNotifyHandler OnCheckoutNotify { get; set; }

/// <summary>
/// Commit the cherry pick if the cherry pick is successful.
/// </summary>
public bool CommitOnSuccess { get; set; }

/// <summary>
/// When cherry picking a merge commit, the parent number to consider as
/// mainline, starting from offset 1.
/// <para>
/// As a merge commit has multiple parents, cherry picking a merge commit
/// will reverse all the changes brought in by the merge except for
/// one parent's line of commits. The parent to preserve is called the
/// mainline, and must be specified by its number (i.e. offset).
/// </para>
/// </summary>
public int Mainline { get; set; }

/// <summary>
/// How to handle conflicts encountered during a merge.
/// </summary>
public MergeFileFavor MergeFileFavor { get; set; }

/// <summary>
/// How Checkout should handle writing out conflicting index entries.
/// </summary>
public CheckoutFileConflictStrategy FileConflictStrategy { get; set; }

/// <summary>
/// Find renames. Default is true.
/// </summary>
public bool FindRenames { get; set; }

/// <summary>
/// Similarity to consider a file renamed (default 50). If
/// `FindRenames` is enabled, added files will be compared
/// with deleted files to determine their similarity. Files that are
/// more similar than the rename threshold (percentage-wise) will be
/// treated as a rename.
/// </summary>
public int RenameThreshold;

/// <summary>
/// Maximum similarity sources to examine for renames (default 200).
/// If the number of rename candidates (add / delete pairs) is greater
/// than this value, inexact rename detection is aborted.
///
/// This setting overrides the `merge.renameLimit` configuration value.
/// </summary>
public int TargetLimit;

#region IConvertableToGitCheckoutOpts

CheckoutCallbacks IConvertableToGitCheckoutOpts.GenerateCallbacks()
{
return CheckoutCallbacks.From(OnCheckoutProgress, OnCheckoutNotify);
}

CheckoutStrategy IConvertableToGitCheckoutOpts.CheckoutStrategy
{
get
{
return CheckoutStrategy.GIT_CHECKOUT_SAFE |
CheckoutStrategy.GIT_CHECKOUT_ALLOW_CONFLICTS |
GitCheckoutOptsWrapper.CheckoutStrategyFromFileConflictStrategy(FileConflictStrategy);
}
}

#endregion IConvertableToGitCheckoutOpts
}
}
55 changes: 55 additions & 0 deletions LibGit2Sharp/CherryPickResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;

namespace LibGit2Sharp
{
/// <summary>
/// Class to report the result of a cherry picked.
/// </summary>
public class CherryPickResult
{
/// <summary>
/// Needed for mocking purposes.
/// </summary>
protected CherryPickResult()
{ }

internal CherryPickResult(CherryPickStatus status, Commit commit = null)
{
Commit = commit;
Status = status;
}

/// <summary>
/// The resulting commit of the cherry pick.
/// <para>
/// This will return <code>null</code> if the cherry pick was not committed.
/// This can happen if:
/// 1) The cherry pick resulted in conflicts.
/// 2) The option to not commit on success is set.
/// </para>
/// </summary>
public virtual Commit Commit { get; private set; }

/// <summary>
/// The status of the cherry pick.
/// </summary>
public virtual CherryPickStatus Status { get; private set; }
}

/// <summary>
/// The status of what happened as a result of a cherry-pick.
/// </summary>
public enum CherryPickStatus
{
/// <summary>
/// The commit was successfully cherry picked.
/// </summary>
CherryPicked,

/// <summary>
/// The cherry pick resulted in conflicts.
/// </summary>
Conflicts
}
}
18 changes: 18 additions & 0 deletions LibGit2Sharp/Core/GitCherryPickOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Runtime.InteropServices;

namespace LibGit2Sharp.Core
{
[StructLayout(LayoutKind.Sequential)]
internal class GitCherryPickOptions
{
public uint Version = 1;

// For merge commits, the "mainline" is treated as the parent
public uint Mainline = 0;

public GitMergeOpts MergeOpts = new GitMergeOpts { Version = 1 };

public GitCheckoutOpts CheckoutOpts = new GitCheckoutOpts { version = 1 };
}
}
3 changes: 3 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,9 @@ internal static extern int git_treebuilder_insert(

[DllImport(libgit2)]
internal static extern int git_blob_is_binary(GitObjectSafeHandle blob);

[DllImport(libgit2)]
internal static extern int git_cherry_pick(RepositorySafeHandle repo, GitObjectSafeHandle commit, GitCherryPickOptions options);
}
}
// ReSharper restore InconsistentNaming
13 changes: 13 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,19 @@ public static void git_checkout_index(RepositorySafeHandle repo, GitObjectSafeHa

#endregion

#region git_cherry_pick_

internal static void git_cherry_pick(RepositorySafeHandle repo, ObjectId commit, GitCherryPickOptions options)
{
using (ThreadAffinity())
using (var nativeCommit = git_object_lookup(repo, commit, GitObjectType.Commit))
{
int res = NativeMethods.git_cherry_pick(repo, nativeCommit, options);
Ensure.ZeroResult(res);
}
}
#endregion

#region git_clone_

public static RepositorySafeHandle git_clone(
Expand Down
11 changes: 10 additions & 1 deletion LibGit2Sharp/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,23 @@ public interface IRepository : IDisposable
MergeResult Merge(Branch branch, Signature merger, MergeOptions options = null);

/// <summary>
/// Merges changes from the commit into the branch pointed at by HEAD..
/// Merges changes from the commit into the branch pointed at by HEAD.
/// </summary>
/// <param name="committish">The commit to merge into branch pointed at by HEAD.</param>
/// <param name="merger">The <see cref="Signature"/> of who is performing the merge.</param>
/// <param name="options">Specifies optional parameters controlling merge behavior; if null, the defaults are used.</param>
/// <returns>The <see cref="MergeResult"/> of the merge.</returns>
MergeResult Merge(string committish, Signature merger, MergeOptions options = null);

/// <summary>
/// Cherry picks changes from the commit into the branch pointed at by HEAD.
/// </summary>
/// <param name="commit">The commit to cherry pick into branch pointed at by HEAD.</param>
/// <param name="committer">The <see cref="Signature"/> of who is performing the cherry pick.</param>
/// <param name="options">Specifies optional parameters controlling cherry pick behavior; if null, the defaults are used.</param>
/// <returns>The <see cref="MergeResult"/> of the merge.</returns>
CherryPickResult CherryPick(Commit commit, Signature committer, CherryPickOptions options = null);

/// <summary>
/// Manipulate the currently ignored files.
/// </summary>
Expand Down
Loading