Skip to content

Commit bf88875

Browse files
redoznulltoken
authored andcommitted
Add CommitCollection.FindCommonAncestor()
Fix issue #149
1 parent 1c23443 commit bf88875

File tree

5 files changed

+257
-1
lines changed

5 files changed

+257
-1
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Linq;
3+
using LibGit2Sharp.Tests.TestHelpers;
4+
using Xunit;
5+
6+
namespace LibGit2Sharp.Tests
7+
{
8+
public class CommitAncestorFixture : BaseFixture
9+
{
10+
/*
11+
* BareTestRepoPath structure
12+
*
13+
* * commit 4c062a6361ae6959e06292c1fa5e2822d9c96345
14+
* |
15+
* * commit be3563ae3f795b2b4353bcce3a527ad0a4f7f644
16+
* |\
17+
* | |
18+
* | * commit c47800c7266a2be04c571c04d5a6614691ea99bd
19+
* | |
20+
* * | commit 9fd738e8f7967c078dceed8190330fc8648ee56a
21+
* | |
22+
* * | commit 4a202b346bb0fb0db7eff3cffeb3c70babbd2045
23+
* |/
24+
* |
25+
* * commit 5b5b025afb0b4c913b4c338a42934a3863bf3644
26+
* |
27+
* * commit 8496071c1b46c854b31185ea97743be6a877447
28+
*
29+
*/
30+
31+
[Fact]
32+
public void CanFindCommonAncestorForTwoCommits()
33+
{
34+
using (var repo = new Repository(BareTestRepoPath))
35+
{
36+
var first = repo.Lookup<Commit>("c47800c7266a2be04c571c04d5a6614691ea99bd");
37+
var second = repo.Lookup<Commit>("9fd738e8f7967c078dceed8190330fc8648ee56a");
38+
39+
Commit ancestor = repo.Commits.FindCommonAncestor(first, second);
40+
41+
Assert.NotNull(ancestor);
42+
ancestor.Id.Sha.ShouldEqual("5b5b025afb0b4c913b4c338a42934a3863bf3644");
43+
}
44+
}
45+
46+
[Fact]
47+
public void CanFindCommonAncestorForTwoCommitsAsEnumerable()
48+
{
49+
using (var repo = new Repository(BareTestRepoPath))
50+
{
51+
var first = repo.Lookup<Commit>("c47800c7266a2be04c571c04d5a6614691ea99bd");
52+
var second = repo.Lookup<Commit>("9fd738e8f7967c078dceed8190330fc8648ee56a");
53+
54+
Commit ancestor = repo.Commits.FindCommonAncestor(new[] { first, second });
55+
56+
Assert.NotNull(ancestor);
57+
ancestor.Id.Sha.ShouldEqual("5b5b025afb0b4c913b4c338a42934a3863bf3644");
58+
}
59+
}
60+
61+
[Fact]
62+
public void CanFindCommonAncestorForSeveralCommits()
63+
{
64+
using (var repo = new Repository(BareTestRepoPath))
65+
{
66+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
67+
var second = repo.Lookup<Commit>("be3563ae3f795b2b4353bcce3a527ad0a4f7f644");
68+
var third = repo.Lookup<Commit>("c47800c7266a2be04c571c04d5a6614691ea99bd");
69+
var fourth = repo.Lookup<Commit>("5b5b025afb0b4c913b4c338a42934a3863bf3644");
70+
71+
Commit ancestor = repo.Commits.FindCommonAncestor(new[] { first, second, third, fourth });
72+
73+
Assert.NotNull(ancestor);
74+
ancestor.Id.Sha.ShouldEqual("5b5b025afb0b4c913b4c338a42934a3863bf3644");
75+
}
76+
}
77+
78+
[Fact]
79+
public void CannotFindAncestorForTwoCommmitsWithoutCommonAncestor()
80+
{
81+
var scd = BuildTemporaryCloneOfTestRepo();
82+
83+
using (var repo = new Repository(scd.RepositoryPath))
84+
{
85+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
86+
var second = repo.Lookup<Commit>("be3563ae3f795b2b4353bcce3a527ad0a4f7f644");
87+
var third = repo.Lookup<Commit>("c47800c7266a2be04c571c04d5a6614691ea99bd");
88+
var fourth = repo.Lookup<Commit>("5b5b025afb0b4c913b4c338a42934a3863bf3644");
89+
90+
Commit orphanedCommit = CreateOrphanedCommit(repo);
91+
92+
Commit ancestor = repo.Commits.FindCommonAncestor(new[] { first, second, orphanedCommit, third, fourth });
93+
Assert.Null(ancestor);
94+
}
95+
}
96+
97+
[Fact]
98+
public void CannotFindCommonAncestorForSeveralCommmitsWithoutCommonAncestor()
99+
{
100+
var scd = BuildTemporaryCloneOfTestRepo();
101+
102+
using (var repo = new Repository(scd.RepositoryPath))
103+
{
104+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
105+
106+
var orphanedCommit = CreateOrphanedCommit(repo);
107+
108+
Commit ancestor = repo.Commits.FindCommonAncestor(first, orphanedCommit);
109+
Assert.Null(ancestor);
110+
}
111+
}
112+
113+
private static Commit CreateOrphanedCommit(Repository repo)
114+
{
115+
Commit random = repo.Head.Tip;
116+
117+
Commit orphanedCommit = repo.ObjectDatabase.CreateCommit(
118+
"This is a test commit created by 'CommitFixture.CannotFindCommonAncestorForCommmitsWithoutCommonAncestor'",
119+
random.Author,
120+
random.Committer,
121+
random.Tree,
122+
Enumerable.Empty<Commit>());
123+
124+
return orphanedCommit;
125+
}
126+
127+
[Fact]
128+
public void FindCommonAncestorForSingleCommitThrows()
129+
{
130+
using (var repo = new Repository(BareTestRepoPath))
131+
{
132+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
133+
134+
Assert.Throws<ArgumentException>(() => repo.Commits.FindCommonAncestor(new[] { first }));
135+
}
136+
}
137+
138+
[Fact]
139+
public void FindCommonAncestorForEnumerableWithNullCommitThrows()
140+
{
141+
using (var repo = new Repository(BareTestRepoPath))
142+
{
143+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
144+
var second = repo.Lookup<Commit>("be3563ae3f795b2b4353bcce3a527ad0a4f7f644");
145+
146+
Assert.Throws<ArgumentException>(() => repo.Commits.FindCommonAncestor(new[] { first, second, null }));
147+
}
148+
}
149+
150+
[Fact]
151+
public void FindCommonAncestorForWithNullCommitThrows()
152+
{
153+
using (var repo = new Repository(BareTestRepoPath))
154+
{
155+
var first = repo.Lookup<Commit>("4c062a6361ae6959e06292c1fa5e2822d9c96345");
156+
157+
Assert.Throws<ArgumentNullException>(() => repo.Commits.FindCommonAncestor(first, null));
158+
Assert.Throws<ArgumentNullException>(() => repo.Commits.FindCommonAncestor(null, first));
159+
}
160+
}
161+
}
162+
}

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<ItemGroup>
4646
<Compile Include="ConfigurationFixture.cs" />
4747
<Compile Include="AttributesFixture.cs" />
48+
<Compile Include="CommitAncestorFixture.cs" />
4849
<Compile Include="DiffBlobToBlobFixture.cs" />
4950
<Compile Include="ObjectDatabaseFixture.cs" />
5051
<Compile Include="DiffTreeToTreeFixture.cs" />

LibGit2Sharp/CommitCollection.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,75 @@ private static bool PointsAtTheHead(string shaOrRefName)
124124
return ("HEAD".Equals(shaOrRefName, StringComparison.Ordinal) || "refs/heads/master".Equals(shaOrRefName, StringComparison.Ordinal));
125125
}
126126

127+
/// <summary>
128+
/// Find the best possible common ancestor given two <see cref = "Commit"/>s.
129+
/// </summary>
130+
/// <param name = "first">The first <see cref = "Commit"/>.</param>
131+
/// <param name = "second">The second <see cref = "Commit"/>.</param>
132+
/// <returns>The common ancestor or null if none found.</returns>
133+
public Commit FindCommonAncestor(Commit first, Commit second)
134+
{
135+
Ensure.ArgumentNotNull(first, "first");
136+
Ensure.ArgumentNotNull(second, "second");
137+
138+
using (var osw1 = new ObjectSafeWrapper(first.Id, repo))
139+
using (var osw2 = new ObjectSafeWrapper(second.Id, repo))
140+
{
141+
GitOid ret;
142+
int result = NativeMethods.git_merge_base(out ret, repo.Handle, osw1.ObjectPtr, osw2.ObjectPtr);
143+
144+
if (result == (int)GitErrorCode.GIT_ENOTFOUND)
145+
{
146+
return null;
147+
}
148+
149+
Ensure.Success(result);
150+
151+
return repo.Lookup<Commit>(new ObjectId(ret));
152+
}
153+
}
154+
155+
/// <summary>
156+
/// Find the best possible common ancestor given two or more <see cref="Commit"/>.
157+
/// </summary>
158+
/// <param name = "commits">The <see cref = "Commit"/>s for which to find the common ancestor.</param>
159+
/// <returns>The common ancestor or null if none found.</returns>
160+
public Commit FindCommonAncestor(IEnumerable<Commit> commits)
161+
{
162+
Ensure.ArgumentNotNull(commits, "commits");
163+
Commit ret = null;
164+
int count = 0;
165+
166+
foreach (var commit in commits)
167+
{
168+
if (commit == null)
169+
{
170+
throw new ArgumentException("Enumerable contains null at position: " + count.ToString(CultureInfo.InvariantCulture), "commits");
171+
}
172+
173+
count++;
174+
175+
if (count == 1)
176+
{
177+
ret = commit;
178+
continue;
179+
}
180+
181+
ret = FindCommonAncestor(ret, commit);
182+
if (ret == null)
183+
{
184+
break;
185+
}
186+
}
187+
188+
if (count < 2)
189+
{
190+
throw new ArgumentException("The enumerable must contains at least two commits.", "commits");
191+
}
192+
193+
return ret;
194+
}
195+
127196
/// <summary>
128197
/// Stores the content of the <see cref = "Repository.Index" /> as a new <see cref = "Commit" /> into the repository.
129198
/// The tip of the <see cref = "Repository.Head"/> will be used as the parent of this new Commit.

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@ public static extern int git_index_open(
330330
[DllImport(libgit2)]
331331
public static extern int git_index_write(IndexSafeHandle index);
332332

333+
[DllImport(libgit2)]
334+
public static extern int git_merge_base(
335+
out GitOid mergeBase,
336+
RepositorySafeHandle repo,
337+
GitObjectSafeHandle one,
338+
GitObjectSafeHandle two);
339+
333340
[DllImport(libgit2)]
334341
public static extern int git_odb_exists(ObjectDatabaseSafeHandle odb, ref GitOid id);
335342

LibGit2Sharp/IQueryableCommitCollection.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace LibGit2Sharp
1+
using System.Collections.Generic;
2+
3+
namespace LibGit2Sharp
24
{
35
public interface IQueryableCommitCollection : ICommitCollection //TODO: Find a name that's more explicit than IQueryableCommitCollection
46
{
@@ -20,5 +22,20 @@ public interface IQueryableCommitCollection : ICommitCollection //TODO: Find a n
2022
/// <param name = "amendPreviousCommit">True to amend the current <see cref = "Commit"/> pointed at by <see cref = "Repository.Head"/>, false otherwise.</param>
2123
/// <returns>The generated <see cref = "Commit" />.</returns>
2224
Commit Create(string message, Signature author, Signature committer, bool amendPreviousCommit);
25+
26+
/// <summary>
27+
/// Find the best possible common ancestor given two <see cref = "Commit"/>s.
28+
/// </summary>
29+
/// <param name = "first">The first <see cref = "Commit"/>.</param>
30+
/// <param name = "second">The second <see cref = "Commit"/>.</param>
31+
/// <returns>The common ancestor or null if none found.</returns>
32+
Commit FindCommonAncestor(Commit first, Commit second);
33+
34+
/// <summary>
35+
/// Find the best possible common ancestor given two or more <see cref = "Commit"/>s.
36+
/// </summary>
37+
/// <param name = "commits">The <see cref = "Commit"/> for which to find the common ancestor.</param>
38+
/// <returns>The common ancestor or null if none found.</returns>
39+
Commit FindCommonAncestor(IEnumerable<Commit> commits);
2340
}
2441
}

0 commit comments

Comments
 (0)