Skip to content

Add MergeCommitsIntoIndex to ObjectDatabase #1534

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 2 commits into from
Mar 5, 2018
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
46 changes: 46 additions & 0 deletions LibGit2Sharp.Tests/MergeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,52 @@ public void CanIgnoreWhitespaceChangeMergeConflict(string branchName)
}
}

[Fact]
public void CanMergeIntoIndex()
{
string path = SandboxMergeTestRepo();
using (var repo = new Repository(path))
{
var master = repo.Lookup<Commit>("master");

using (TransientIndex index = repo.ObjectDatabase.MergeCommitsIntoIndex(master, master, null))
{
var tree = index.WriteToTree();
Assert.Equal(master.Tree.Id, tree.Id);
}
}
}

[Fact]
public void CanMergeIntoIndexWithConflicts()
{
string path = SandboxMergeTestRepo();
using (var repo = new Repository(path))
{
var master = repo.Lookup<Commit>("master");
var branch = repo.Lookup<Commit>("conflicts");

using (TransientIndex index = repo.ObjectDatabase.MergeCommitsIntoIndex(branch, master, null))
{
Assert.False(index.IsFullyMerged);

var conflict = index.Conflicts.First();

//Resolve the conflict by taking the blob from branch
var blob = repo.Lookup<Blob>(conflict.Ours.Id);
//Add() does not remove conflict entries for the same path, so they must be explicitly removed first.
index.Remove(conflict.Ours.Path);
index.Add(blob, conflict.Ours.Path, Mode.NonExecutableFile);

Assert.True(index.IsFullyMerged);
var tree = index.WriteToTree();

//Since we took the conflicted blob from the branch, the merged result should be the same as the branch.
Assert.Equal(branch.Tree.Id, tree.Id);
}
}
}

private Commit AddFileCommitToRepo(IRepository repository, string filename, string content = null)
{
Touch(repository.Info.WorkingDirectory, filename, content);
Expand Down
20 changes: 17 additions & 3 deletions LibGit2Sharp/Index.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ public class Index : IEnumerable<IndexEntry>
protected Index()
{ }

internal Index(Repository repo)
internal Index(IndexHandle handle, Repository repo)
{
this.repo = repo;

handle = Proxy.git_repository_index(repo.Handle);
this.handle = handle;
conflicts = new ConflictCollection(this);
}

internal Index(Repository repo)
: this(Proxy.git_repository_index(repo.Handle), repo)
{
repo.RegisterForCleanup(handle);
}

Expand Down Expand Up @@ -305,5 +308,16 @@ public virtual void Write()
{
Proxy.git_index_write(handle);
}

/// <summary>
/// Write the contents of this <see cref="Index"/> to a tree
/// </summary>
/// <returns></returns>
public virtual Tree WriteToTree()
{
var treeId = Proxy.git_index_write_tree_to(this.handle, this.repo.Handle);
var result = this.repo.Lookup<Tree>(treeId);
return result;
}
}
}
117 changes: 90 additions & 27 deletions LibGit2Sharp/ObjectDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -755,47 +755,36 @@ public virtual Commit FindMergeBase(IEnumerable<Commit> commits, MergeBaseFindin

/// <summary>
/// Perform a three-way merge of two commits, looking up their
/// commit ancestor. The returned index will contain the results
/// of the merge and can be examined for conflicts. The returned
/// index must be disposed.
/// commit ancestor. The returned <see cref="MergeTreeResult"/> will contain the results
/// of the merge and can be examined for conflicts.
/// </summary>
/// <param name="ours">The first tree</param>
/// <param name="theirs">The second tree</param>
/// <param name="ours">The first commit</param>
/// <param name="theirs">The second commit</param>
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
/// <returns>The <see cref="Index"/> containing the merged trees and any conflicts</returns>
/// <returns>The <see cref="MergeTreeResult"/> containing the merged trees and any conflicts</returns>
public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTreeOptions options)
{
Ensure.ArgumentNotNull(ours, "ours");
Ensure.ArgumentNotNull(theirs, "theirs");

options = options ?? new MergeTreeOptions();
var modifiedOptions = 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;
}
modifiedOptions.SkipReuc = true;


var mergeOptions = new GitMergeOpts
if (options != null)
{
Version = 1,
MergeFileFavorFlags = options.MergeFileFavor,
MergeTreeFlags = mergeFlags,
RenameThreshold = (uint)options.RenameThreshold,
TargetLimit = (uint)options.TargetLimit,
};
modifiedOptions.FailOnConflict = options.FailOnConflict;
modifiedOptions.FindRenames = options.FindRenames;
modifiedOptions.IgnoreWhitespaceChange = options.IgnoreWhitespaceChange;
modifiedOptions.MergeFileFavor = options.MergeFileFavor;
modifiedOptions.RenameThreshold = options.RenameThreshold;
modifiedOptions.TargetLimit = options.TargetLimit;
}

bool earlyStop;
using (var oneHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Commit))
using (var twoHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Commit))
using (var indexHandle = Proxy.git_merge_commits(repo.Handle, oneHandle, twoHandle, mergeOptions, out earlyStop))
using (var indexHandle = MergeCommits(ours, theirs, modifiedOptions, out earlyStop))
{
MergeTreeResult mergeResult;

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

/// <summary>
/// Perform a three-way merge of two commits, looking up their
/// commit ancestor. The returned index will contain the results
/// of the merge and can be examined for conflicts.
/// </summary>
/// <param name="ours">The first tree</param>
/// <param name="theirs">The second tree</param>
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
/// <returns>The <see cref="TransientIndex"/> containing the merged trees and any conflicts, or null if the merge stopped early due to conflicts.
/// The index must be disposed by the caller.</returns>
public virtual TransientIndex MergeCommitsIntoIndex(Commit ours, Commit theirs, MergeTreeOptions options)
{
Ensure.ArgumentNotNull(ours, "ours");
Ensure.ArgumentNotNull(theirs, "theirs");

options = options ?? new MergeTreeOptions();

bool earlyStop;
var indexHandle = MergeCommits(ours, theirs, options, out earlyStop);
if (earlyStop)
{
if (indexHandle != null)
{
indexHandle.Dispose();
}
return null;
}
var result = new TransientIndex(indexHandle, repo);
return result;
}

/// <summary>
/// Perform a three-way merge of two commits, looking up their
/// commit ancestor. The returned index will contain the results
/// of the merge and can be examined for conflicts.
/// </summary>
/// <param name="ours">The first tree</param>
/// <param name="theirs">The second tree</param>
/// <param name="options">The <see cref="MergeTreeOptions"/> controlling the merge</param>
/// <param name="earlyStop">True if the merge stopped early due to conflicts</param>
/// <returns>The <see cref="IndexHandle"/> containing the merged trees and any conflicts</returns>
private IndexHandle MergeCommits(Commit ours, Commit theirs, MergeTreeOptions options, out bool earlyStop)
{
GitMergeFlag mergeFlags = GitMergeFlag.GIT_MERGE_NORMAL;
if (options.SkipReuc)
{
mergeFlags |= 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 mergeOptions = new GitMergeOpts
{
Version = 1,
MergeFileFavorFlags = options.MergeFileFavor,
MergeTreeFlags = mergeFlags,
RenameThreshold = (uint)options.RenameThreshold,
TargetLimit = (uint)options.TargetLimit,
};
using (var oneHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Commit))
using (var twoHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Commit))
{
var indexHandle = Proxy.git_merge_commits(repo.Handle, oneHandle, twoHandle, mergeOptions, out earlyStop);
return indexHandle;
}
}


/// <summary>
/// Packs objects in the <see cref="ObjectDatabase"/> and write a pack (.pack) and index (.idx) files for them.
/// For internal use only.
Expand Down
31 changes: 31 additions & 0 deletions LibGit2Sharp/TransientIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using LibGit2Sharp.Core.Handles;

namespace LibGit2Sharp
{
/// <summary>
/// An implementation of <see cref="Index"/> with disposal managed by the caller
/// (instead of automatically disposing when the repository is disposed)
/// </summary>
public class TransientIndex: Index, IDisposable
{
/// <summary>
/// Needed for mocking purposes.
/// </summary>
protected TransientIndex()
{ }

internal TransientIndex(IndexHandle handle, Repository repo)
: base(handle, repo)
{
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.Handle.SafeDispose();
}
}
}