Skip to content

Commit 82ca2a0

Browse files
authored
Merge pull request libgit2#1534 from tyrielv/indexformerge
Add MergeCommitsIntoIndex to ObjectDatabase
2 parents 6bfae0d + 4b09c8c commit 82ca2a0

File tree

4 files changed

+184
-30
lines changed

4 files changed

+184
-30
lines changed

LibGit2Sharp.Tests/MergeFixture.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,52 @@ public void CanIgnoreWhitespaceChangeMergeConflict(string branchName)
902902
}
903903
}
904904

905+
[Fact]
906+
public void CanMergeIntoIndex()
907+
{
908+
string path = SandboxMergeTestRepo();
909+
using (var repo = new Repository(path))
910+
{
911+
var master = repo.Lookup<Commit>("master");
912+
913+
using (TransientIndex index = repo.ObjectDatabase.MergeCommitsIntoIndex(master, master, null))
914+
{
915+
var tree = index.WriteToTree();
916+
Assert.Equal(master.Tree.Id, tree.Id);
917+
}
918+
}
919+
}
920+
921+
[Fact]
922+
public void CanMergeIntoIndexWithConflicts()
923+
{
924+
string path = SandboxMergeTestRepo();
925+
using (var repo = new Repository(path))
926+
{
927+
var master = repo.Lookup<Commit>("master");
928+
var branch = repo.Lookup<Commit>("conflicts");
929+
930+
using (TransientIndex index = repo.ObjectDatabase.MergeCommitsIntoIndex(branch, master, null))
931+
{
932+
Assert.False(index.IsFullyMerged);
933+
934+
var conflict = index.Conflicts.First();
935+
936+
//Resolve the conflict by taking the blob from branch
937+
var blob = repo.Lookup<Blob>(conflict.Ours.Id);
938+
//Add() does not remove conflict entries for the same path, so they must be explicitly removed first.
939+
index.Remove(conflict.Ours.Path);
940+
index.Add(blob, conflict.Ours.Path, Mode.NonExecutableFile);
941+
942+
Assert.True(index.IsFullyMerged);
943+
var tree = index.WriteToTree();
944+
945+
//Since we took the conflicted blob from the branch, the merged result should be the same as the branch.
946+
Assert.Equal(branch.Tree.Id, tree.Id);
947+
}
948+
}
949+
}
950+
905951
private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
906952
{
907953
Touch(repository.Info.WorkingDirectory, filename, content);

LibGit2Sharp/Index.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ public class Index : IEnumerable<IndexEntry>
2626
protected Index()
2727
{ }
2828

29-
internal Index(Repository repo)
29+
internal Index(IndexHandle handle, Repository repo)
3030
{
3131
this.repo = repo;
32-
33-
handle = Proxy.git_repository_index(repo.Handle);
32+
this.handle = handle;
3433
conflicts = new ConflictCollection(this);
34+
}
3535

36+
internal Index(Repository repo)
37+
: this(Proxy.git_repository_index(repo.Handle), repo)
38+
{
3639
repo.RegisterForCleanup(handle);
3740
}
3841

@@ -305,5 +308,16 @@ public virtual void Write()
305308
{
306309
Proxy.git_index_write(handle);
307310
}
311+
312+
/// <summary>
313+
/// Write the contents of this <see cref="Index"/> to a tree
314+
/// </summary>
315+
/// <returns></returns>
316+
public virtual Tree WriteToTree()
317+
{
318+
var treeId = Proxy.git_index_write_tree_to(this.handle, this.repo.Handle);
319+
var result = this.repo.Lookup<Tree>(treeId);
320+
return result;
321+
}
308322
}
309323
}

LibGit2Sharp/ObjectDatabase.cs

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -755,47 +755,36 @@ public virtual Commit FindMergeBase(IEnumerable<Commit> commits, MergeBaseFindin
755755

756756
/// <summary>
757757
/// Perform a three-way merge of two commits, looking up their
758-
/// commit ancestor. The returned index will contain the results
759-
/// of the merge and can be examined for conflicts. The returned
760-
/// index must be disposed.
758+
/// commit ancestor. The returned <see cref="MergeTreeResult"/> will contain the results
759+
/// of the merge and can be examined for conflicts.
761760
/// </summary>
762-
/// <param name="ours">The first tree</param>
763-
/// <param name="theirs">The second tree</param>
761+
/// <param name="ours">The first commit</param>
762+
/// <param name="theirs">The second commit</param>
764763
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
765-
/// <returns>The <see cref="Index"/> containing the merged trees and any conflicts</returns>
764+
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
766765
public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTreeOptions options)
767766
{
768767
Ensure.ArgumentNotNull(ours, "ours");
769768
Ensure.ArgumentNotNull(theirs, "theirs");
770769

771-
options = options ?? new MergeTreeOptions();
770+
var modifiedOptions = new MergeTreeOptions();
772771

773772
// We throw away the index after looking at the conflicts, so we'll never need the REUC
774773
// entries to be there
775-
GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL | GitMergeFlag.GIT_MERGE_SKIP_REUC;
776-
if (options.FindRenames)
777-
{
778-
mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES;
779-
}
780-
if (options.FailOnConflict)
781-
{
782-
mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT;
783-
}
774+
modifiedOptions.SkipReuc = true;
784775

785-
786-
var mergeOptions = new GitMergeOpts
776+
if (options != null)
787777
{
788-
Version = 1,
789-
MergeFileFavorFlags = options.MergeFileFavor,
790-
MergeTreeFlags = mergeFlags,
791-
RenameThreshold = (uint)options.RenameThreshold,
792-
TargetLimit = (uint)options.TargetLimit,
793-
};
778+
modifiedOptions.FailOnConflict = options.FailOnConflict;
779+
modifiedOptions.FindRenames = options.FindRenames;
780+
modifiedOptions.IgnoreWhitespaceChange = options.IgnoreWhitespaceChange;
781+
modifiedOptions.MergeFileFavor = options.MergeFileFavor;
782+
modifiedOptions.RenameThreshold = options.RenameThreshold;
783+
modifiedOptions.TargetLimit = options.TargetLimit;
784+
}
794785

795786
bool earlyStop;
796-
using (var oneHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Commit))
797-
using (var twoHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Commit))
798-
using (var indexHandle = Proxy.git_merge_commits(repo.Handle, oneHandle, twoHandle, mergeOptions, out earlyStop))
787+
using (var indexHandle = MergeCommits(ours, theirs, modifiedOptions, out earlyStop))
799788
{
800789
MergeTreeResult mergeResult;
801790

@@ -859,6 +848,80 @@ public virtual PackBuilderResults Pack(PackBuilderOptions options, Action<PackBu
859848
return InternalPack(options, packDelegate);
860849
}
861850

851+
/// <summary>
852+
/// Perform a three-way merge of two commits, looking up their
853+
/// commit ancestor. The returned index will contain the results
854+
/// of the merge and can be examined for conflicts.
855+
/// </summary>
856+
/// <param name="ours">The first tree</param>
857+
/// <param name="theirs">The second tree</param>
858+
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
859+
/// <returns>The <see cref="TransientIndex"/> containing the merged trees and any conflicts, or null if the merge stopped early due to conflicts.
860+
/// The index must be disposed by the caller.</returns>
861+
public virtual TransientIndex MergeCommitsIntoIndex(Commit ours, Commit theirs, MergeTreeOptions options)
862+
{
863+
Ensure.ArgumentNotNull(ours, "ours");
864+
Ensure.ArgumentNotNull(theirs, "theirs");
865+
866+
options = options ?? new MergeTreeOptions();
867+
868+
bool earlyStop;
869+
var indexHandle = MergeCommits(ours, theirs, options, out earlyStop);
870+
if (earlyStop)
871+
{
872+
if (indexHandle != null)
873+
{
874+
indexHandle.Dispose();
875+
}
876+
return null;
877+
}
878+
var result = new TransientIndex(indexHandle, repo);
879+
return result;
880+
}
881+
882+
/// <summary>
883+
/// Perform a three-way merge of two commits, looking up their
884+
/// commit ancestor. The returned index will contain the results
885+
/// of the merge and can be examined for conflicts.
886+
/// </summary>
887+
/// <param name="ours">The first tree</param>
888+
/// <param name="theirs">The second tree</param>
889+
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
890+
/// <param name="earlyStop">True if the merge stopped early due to conflicts</param>
891+
/// <returns>The <see cref="IndexHandle"/> containing the merged trees and any conflicts</returns>
892+
private IndexHandle MergeCommits(Commit ours, Commit theirs, MergeTreeOptions options, out bool earlyStop)
893+
{
894+
GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL;
895+
if (options.SkipReuc)
896+
{
897+
mergeFlags |= GitMergeFlag.GIT_MERGE_SKIP_REUC;
898+
}
899+
if (options.FindRenames)
900+
{
901+
mergeFlags |= GitMergeFlag.GIT_MERGE_FIND_RENAMES;
902+
}
903+
if (options.FailOnConflict)
904+
{
905+
mergeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT;
906+
}
907+
908+
var mergeOptions = new GitMergeOpts
909+
{
910+
Version = 1,
911+
MergeFileFavorFlags = options.MergeFileFavor,
912+
MergeTreeFlags = mergeFlags,
913+
RenameThreshold = (uint)options.RenameThreshold,
914+
TargetLimit = (uint)options.TargetLimit,
915+
};
916+
using (var oneHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Commit))
917+
using (var twoHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Commit))
918+
{
919+
var indexHandle = Proxy.git_merge_commits(repo.Handle, oneHandle, twoHandle, mergeOptions, out earlyStop);
920+
return indexHandle;
921+
}
922+
}
923+
924+
862925
/// <summary>
863926
/// Packs objects in the <see cref="ObjectDatabase"/> and write a pack (.pack) and index (.idx) files for them.
864927
/// For internal use only.

LibGit2Sharp/TransientIndex.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using LibGit2Sharp.Core.Handles;
3+
4+
namespace LibGit2Sharp
5+
{
6+
/// <summary>
7+
/// An implementation of <see cref="Index"/> with disposal managed by the caller
8+
/// (instead of automatically disposing when the repository is disposed)
9+
/// </summary>
10+
public class TransientIndex: Index, IDisposable
11+
{
12+
/// <summary>
13+
/// Needed for mocking purposes.
14+
/// </summary>
15+
protected TransientIndex()
16+
{ }
17+
18+
internal TransientIndex(IndexHandle handle, Repository repo)
19+
: base(handle, repo)
20+
{
21+
}
22+
23+
/// <summary>
24+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
25+
/// </summary>
26+
public void Dispose()
27+
{
28+
this.Handle.SafeDispose();
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)