Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b399005

Browse files
committed
Introduce Repository.CherryPick.
1 parent e4e9ab7 commit b399005

File tree

10 files changed

+419
-5
lines changed

10 files changed

+419
-5
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System.IO;
2+
using System.Linq;
3+
using LibGit2Sharp.Tests.TestHelpers;
4+
using Xunit;
5+
using Xunit.Extensions;
6+
using System;
7+
8+
namespace LibGit2Sharp.Tests
9+
{
10+
public class CherryPickFixture : BaseFixture
11+
{
12+
[Theory]
13+
[InlineData(true)]
14+
[InlineData(false)]
15+
public void CanCherryPick(bool fromDetachedHead)
16+
{
17+
string path = CloneMergeTestRepo();
18+
using (var repo = new Repository(path))
19+
{
20+
if (fromDetachedHead)
21+
{
22+
repo.Checkout(repo.Head.Tip.Id.Sha);
23+
}
24+
25+
Commit commitToMerge = repo.Branches["fast_forward"].Tip;
26+
27+
CherryPickResult result = repo.CherryPick(commitToMerge, Constants.Signature);
28+
29+
Assert.Equal(CherryPickStatus.CherryPicked, result.Status);
30+
Assert.Equal(cherryPickedCommitId, result.Commit.Id.Sha);
31+
Assert.False(repo.Index.RetrieveStatus().Any());
32+
Assert.Equal(fromDetachedHead, repo.Info.IsHeadDetached);
33+
Assert.Equal(commitToMerge.Author, result.Commit.Author);
34+
Assert.Equal(Constants.Signature, result.Commit.Committer);
35+
}
36+
}
37+
38+
[Fact]
39+
public void CherryPickWithConflictDoesNotCommit()
40+
{
41+
const string firstBranchFileName = "first branch file.txt";
42+
const string secondBranchFileName = "second branch file.txt";
43+
const string sharedBranchFileName = "first+second branch file.txt";
44+
45+
string path = CloneStandardTestRepo();
46+
using (var repo = new Repository(path))
47+
{
48+
var firstBranch = repo.CreateBranch("FirstBranch");
49+
firstBranch.Checkout();
50+
51+
// Commit with ONE new file to both first & second branch (SecondBranch is created on this commit).
52+
AddFileCommitToRepo(repo, sharedBranchFileName);
53+
54+
var secondBranch = repo.CreateBranch("SecondBranch");
55+
// Commit with ONE new file to first branch (FirstBranch moves forward as it is checked out, SecondBranch stays back one).
56+
AddFileCommitToRepo(repo, firstBranchFileName);
57+
AddFileCommitToRepo(repo, sharedBranchFileName, "The first branches comment"); // Change file in first branch
58+
59+
secondBranch.Checkout();
60+
// Commit with ONE new file to second branch (FirstBranch and SecondBranch now point to separate commits that both have the same parent commit).
61+
AddFileCommitToRepo(repo, secondBranchFileName);
62+
AddFileCommitToRepo(repo, sharedBranchFileName, "The second branches comment"); // Change file in second branch
63+
64+
CherryPickResult cherryPickResult = repo.CherryPick(repo.Branches["FirstBranch"].Tip, Constants.Signature);
65+
66+
Assert.Equal(CherryPickStatus.Conflicts, cherryPickResult.Status);
67+
68+
Assert.Null(cherryPickResult.Commit);
69+
Assert.Equal(1, repo.Index.Conflicts.Count());
70+
71+
var conflict = repo.Index.Conflicts.First();
72+
var changes = repo.Diff.Compare(repo.Lookup<Blob>(conflict.Theirs.Id), repo.Lookup<Blob>(conflict.Ours.Id));
73+
74+
Assert.False(changes.IsBinaryComparison);
75+
}
76+
}
77+
78+
[Theory]
79+
[InlineData(CheckoutFileConflictStrategy.Ours)]
80+
[InlineData(CheckoutFileConflictStrategy.Theirs)]
81+
public void CanSpecifyConflictFileStrategy(CheckoutFileConflictStrategy conflictStrategy)
82+
{
83+
const string conflictFile = "a.txt";
84+
const string conflictBranchName = "conflicts";
85+
86+
string path = CloneMergeTestRepo();
87+
using (var repo = new Repository(path))
88+
{
89+
Branch branch = repo.Branches[conflictBranchName];
90+
Assert.NotNull(branch);
91+
92+
CherryPickOptions cherryPickOptions = new CherryPickOptions()
93+
{
94+
FileConflictStrategy = conflictStrategy,
95+
};
96+
97+
CherryPickResult result = repo.CherryPick(branch.Tip, Constants.Signature, cherryPickOptions);
98+
Assert.Equal(CherryPickStatus.Conflicts, result.Status);
99+
100+
// Get the information on the conflict.
101+
Conflict conflict = repo.Index.Conflicts[conflictFile];
102+
103+
Assert.NotNull(conflict);
104+
Assert.NotNull(conflict.Theirs);
105+
Assert.NotNull(conflict.Ours);
106+
107+
// Get the blob containing the expected content.
108+
Blob expectedBlob = null;
109+
switch (conflictStrategy)
110+
{
111+
case CheckoutFileConflictStrategy.Theirs:
112+
expectedBlob = repo.Lookup<Blob>(conflict.Theirs.Id);
113+
break;
114+
case CheckoutFileConflictStrategy.Ours:
115+
expectedBlob = repo.Lookup<Blob>(conflict.Ours.Id);
116+
break;
117+
default:
118+
throw new Exception("Unexpected FileConflictStrategy");
119+
}
120+
121+
Assert.NotNull(expectedBlob);
122+
123+
// Check the content of the file on disk matches what is expected.
124+
string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictFile));
125+
Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictFile)));
126+
}
127+
}
128+
129+
private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
130+
{
131+
Touch(repository.Info.WorkingDirectory, filename, content);
132+
133+
repository.Index.Stage(filename);
134+
135+
return repository.Commit("New commit", Constants.Signature, Constants.Signature);
136+
}
137+
138+
// Commit IDs of the checked in merge_testrepo
139+
private const string cherryPickedCommitId = "74b37f366b6e1c682c1c9fe0c6b006cbe909cf91";
140+
}
141+
}

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<Compile Include="BlameFixture.cs" />
6363
<Compile Include="ArchiveTarFixture.cs" />
6464
<Compile Include="CheckoutFixture.cs" />
65+
<Compile Include="CherryPickFixture.cs" />
6566
<Compile Include="GlobalSettingsFixture.cs" />
6667
<Compile Include="PatchStatsFixture.cs" />
6768
<Compile Include="RefSpecFixture.cs" />

LibGit2Sharp/CherryPickOptions.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using LibGit2Sharp.Core;
2+
using LibGit2Sharp.Handlers;
3+
4+
namespace LibGit2Sharp
5+
{
6+
/// <summary>
7+
/// Options controlling CherryPick behavior.
8+
/// </summary>
9+
public sealed class CherryPickOptions : IConvertableToGitCheckoutOpts
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="CherryPickOptions"/> class.
13+
/// By default the cherry pick will be committed if there are no conflicts.
14+
/// </summary>
15+
public CherryPickOptions()
16+
{
17+
CommitOnSuccess = true;
18+
19+
FindRenames = true;
20+
21+
// TODO: libgit2 should provide reasonable defaults for these
22+
// values, but it currently does not.
23+
RenameThreshold = 50;
24+
TargetLimit = 200;
25+
}
26+
27+
/// <summary>
28+
/// The Flags specifying what conditions are
29+
/// reported through the OnCheckoutNotify delegate.
30+
/// </summary>
31+
public CheckoutNotifyFlags CheckoutNotifyFlags { get; set; }
32+
33+
/// <summary>
34+
/// Delegate that checkout progress will be reported through.
35+
/// </summary>
36+
public CheckoutProgressHandler OnCheckoutProgress { get; set; }
37+
38+
/// <summary>
39+
/// Delegate that checkout will notify callers of
40+
/// certain conditions. The conditions that are reported is
41+
/// controlled with the CheckoutNotifyFlags property.
42+
/// </summary>
43+
public CheckoutNotifyHandler OnCheckoutNotify { get; set; }
44+
45+
/// <summary>
46+
/// Commit the cherry pick if the cherry pick is successful.
47+
/// </summary>
48+
public bool CommitOnSuccess { get; set; }
49+
50+
/// <summary>
51+
/// When cherry picking a merge commit, the parent number to consider as
52+
/// mainline, starting from offset 1.
53+
/// <para>
54+
/// As a merge commit has multiple parents, cherry picking a merge commit
55+
/// will reverse all the changes brought in by the merge except for
56+
/// one parent's line of commits. The parent to preserve is called the
57+
/// mainline, and must be specified by its number (i.e. offset).
58+
/// </para>
59+
/// </summary>
60+
public int Mainline { get; set; }
61+
62+
/// <summary>
63+
/// How to handle conflicts encountered during a merge.
64+
/// </summary>
65+
public MergeFileFavor MergeFileFavor { get; set; }
66+
67+
/// <summary>
68+
/// How Checkout should handle writing out conflicting index entries.
69+
/// </summary>
70+
public CheckoutFileConflictStrategy FileConflictStrategy { get; set; }
71+
72+
/// <summary>
73+
/// Find renames. Default is true.
74+
/// </summary>
75+
public bool FindRenames { get; set; }
76+
77+
/// <summary>
78+
/// Similarity to consider a file renamed (default 50). If
79+
/// `FindRenames` is enabled, added files will be compared
80+
/// with deleted files to determine their similarity. Files that are
81+
/// more similar than the rename threshold (percentage-wise) will be
82+
/// treated as a rename.
83+
/// </summary>
84+
public int RenameThreshold;
85+
86+
/// <summary>
87+
/// Maximum similarity sources to examine for renames (default 200).
88+
/// If the number of rename candidates (add / delete pairs) is greater
89+
/// than this value, inexact rename detection is aborted.
90+
///
91+
/// This setting overrides the `merge.renameLimit` configuration value.
92+
/// </summary>
93+
public int TargetLimit;
94+
95+
#region IConvertableToGitCheckoutOpts
96+
97+
CheckoutCallbacks IConvertableToGitCheckoutOpts.GenerateCallbacks()
98+
{
99+
return CheckoutCallbacks.From(OnCheckoutProgress, OnCheckoutNotify);
100+
}
101+
102+
CheckoutStrategy IConvertableToGitCheckoutOpts.CheckoutStrategy
103+
{
104+
get
105+
{
106+
return CheckoutStrategy.GIT_CHECKOUT_SAFE |
107+
CheckoutStrategy.GIT_CHECKOUT_ALLOW_CONFLICTS |
108+
GitCheckoutOptsWrapper.CheckoutStrategyFromFileConflictStrategy(FileConflictStrategy);
109+
}
110+
}
111+
112+
#endregion IConvertableToGitCheckoutOpts
113+
}
114+
}

LibGit2Sharp/CherryPickResult.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace LibGit2Sharp
5+
{
6+
/// <summary>
7+
/// Class to report the result of a cherry picked.
8+
/// </summary>
9+
public class CherryPickResult
10+
{
11+
/// <summary>
12+
/// Needed for mocking purposes.
13+
/// </summary>
14+
protected CherryPickResult()
15+
{ }
16+
17+
internal CherryPickResult(CherryPickStatus status, Commit commit = null)
18+
{
19+
Commit = commit;
20+
Status = status;
21+
}
22+
23+
/// <summary>
24+
/// The resulting commit of the cherry pick.
25+
/// <para>
26+
/// This will return <code>null</code> if the cherry pick was not committed.
27+
/// This can happen if:
28+
/// 1) The cherry pick resulted in conflicts.
29+
/// 2) The option to not commit on success is set.
30+
/// </para>
31+
/// </summary>
32+
public virtual Commit Commit { get; private set; }
33+
34+
/// <summary>
35+
/// The status of the cherry pick.
36+
/// </summary>
37+
public virtual CherryPickStatus Status { get; private set; }
38+
}
39+
40+
/// <summary>
41+
/// The status of what happened as a result of a cherry-pick.
42+
/// </summary>
43+
public enum CherryPickStatus
44+
{
45+
/// <summary>
46+
/// The commit was successfully cherry picked.
47+
/// </summary>
48+
CherryPicked,
49+
50+
/// <summary>
51+
/// The cherry pick resulted in conflicts.
52+
/// </summary>
53+
Conflicts
54+
}
55+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace LibGit2Sharp.Core
5+
{
6+
[StructLayout(LayoutKind.Sequential)]
7+
internal class GitCherryPickOptions
8+
{
9+
public uint Version = 1;
10+
11+
// For merge commits, the "mainline" is treated as the parent
12+
public uint Mainline = 0;
13+
14+
public GitMergeOpts MergeOpts = new GitMergeOpts { Version = 1 };
15+
16+
public GitCheckoutOpts CheckoutOpts = new GitCheckoutOpts { version = 1 };
17+
}
18+
}

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,9 @@ internal static extern int git_treebuilder_insert(
14941494

14951495
[DllImport(libgit2)]
14961496
internal static extern int git_blob_is_binary(GitObjectSafeHandle blob);
1497+
1498+
[DllImport(libgit2)]
1499+
internal static extern int git_cherry_pick(RepositorySafeHandle repo, GitObjectSafeHandle commit, GitCherryPickOptions options);
14971500
}
14981501
}
14991502
// ReSharper restore InconsistentNaming

LibGit2Sharp/Core/Proxy.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,19 @@ public static void git_checkout_index(RepositorySafeHandle repo, GitObjectSafeHa
268268

269269
#endregion
270270

271+
#region git_cherry_pick_
272+
273+
internal static void git_cherry_pick(RepositorySafeHandle repo, ObjectId commit, GitCherryPickOptions options)
274+
{
275+
using (ThreadAffinity())
276+
using (var nativeCommit = git_object_lookup(repo, commit, GitObjectType.Commit))
277+
{
278+
int res = NativeMethods.git_cherry_pick(repo, nativeCommit, options);
279+
Ensure.ZeroResult(res);
280+
}
281+
}
282+
#endregion
283+
271284
#region git_clone_
272285

273286
public static RepositorySafeHandle git_clone(

LibGit2Sharp/IRepository.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,23 @@ public interface IRepository : IDisposable
215215
MergeResult Merge(Branch branch, Signature merger, MergeOptions options = null);
216216

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

226+
/// <summary>
227+
/// Cherry picks changes from the commit into the branch pointed at by HEAD.
228+
/// </summary>
229+
/// <param name="commit">The commit to cherry pick into branch pointed at by HEAD.</param>
230+
/// <param name="committer">The <see cref="Signature"/> of who is performing the cherry pick.</param>
231+
/// <param name="options">Specifies optional parameters controlling cherry pick behavior; if null, the defaults are used.</param>
232+
/// <returns>The <see cref="MergeResult"/> of the merge.</returns>
233+
CherryPickResult CherryPick(Commit commit, Signature committer, CherryPickOptions options = null);
234+
226235
/// <summary>
227236
/// Manipulate the currently ignored files.
228237
/// </summary>

0 commit comments

Comments
 (0)