From cc4bb2da1de35122860993cb1f5c9fae7a724f59 Mon Sep 17 00:00:00 2001 From: crumblycake Date: Thu, 5 Dec 2013 22:31:18 +1100 Subject: [PATCH] Initial merge functionality. Bring initial merge functionality to LibGit2Sharp. --- LibGit2Sharp.Tests/MergeFixture.cs | 190 +++++++++++++++++- LibGit2Sharp/Core/GitMergeOpts.cs | 33 +++ LibGit2Sharp/Core/GitMergeResult.cs | 47 +++++ LibGit2Sharp/Core/GitMergeTreeOpts.cs | 57 ++++++ .../Core/Handles/GitMergeHeadHandle.cs | 13 ++ .../Core/Handles/GitMergeResultHandle.cs | 13 ++ LibGit2Sharp/Core/NativeMethods.cs | 49 +++++ LibGit2Sharp/Core/Proxy.cs | 83 ++++++++ LibGit2Sharp/IRepository.cs | 7 + LibGit2Sharp/LibGit2Sharp.csproj | 6 + LibGit2Sharp/MergeResult.cs | 71 +++++++ LibGit2Sharp/Repository.cs | 74 +++++++ 12 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 LibGit2Sharp/Core/GitMergeOpts.cs create mode 100644 LibGit2Sharp/Core/GitMergeResult.cs create mode 100644 LibGit2Sharp/Core/GitMergeTreeOpts.cs create mode 100644 LibGit2Sharp/Core/Handles/GitMergeHeadHandle.cs create mode 100644 LibGit2Sharp/Core/Handles/GitMergeResultHandle.cs create mode 100644 LibGit2Sharp/MergeResult.cs diff --git a/LibGit2Sharp.Tests/MergeFixture.cs b/LibGit2Sharp.Tests/MergeFixture.cs index 7bb5d54dd..32bef84e9 100644 --- a/LibGit2Sharp.Tests/MergeFixture.cs +++ b/LibGit2Sharp.Tests/MergeFixture.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -80,5 +81,192 @@ public void CanRetrieveTheBranchBeingMerged() Assert.Null(mergedHeads[1].Tip); } } + + [Fact] + public void CanMergeRepoNonFastForward() + { + 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(); + var originalTreeCount = firstBranch.Tip.Tree.Count; + + // 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); + + 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); + + MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature); + + Assert.Equal(MergeStatus.NonFastForward, mergeResult.Status); + + Assert.Equal(repo.Head.Tip, mergeResult.Commit); + Assert.Equal(originalTreeCount + 3, mergeResult.Commit.Tree.Count); // Expecting original tree count plussed by the 3 added files. + Assert.Equal(2, mergeResult.Commit.Parents.Count()); // Merge commit should have 2 parents + } + } + + [Fact] + public void IsUpToDateMerge() + { + 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"); + + secondBranch.Checkout(); + + MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature); + + Assert.Equal(MergeStatus.UpToDate, mergeResult.Status); + } + } + + [Fact] + public void CanFastForwardRepos() + { + const string firstBranchFileName = "first branch file.txt"; + const string sharedBranchFileName = "first+second branch file.txt"; + + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + // Reset the index and the working tree. + repo.Reset(ResetMode.Hard); + + // Clean the working directory. + repo.RemoveUntrackedFiles(); + + 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); + + secondBranch.Checkout(); + + MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature); + + Assert.Equal(MergeStatus.FastForward, mergeResult.Status); + Assert.Equal(repo.Branches["FirstBranch"].Tip, mergeResult.Commit); + Assert.Equal(repo.Branches["FirstBranch"].Tip, repo.Head.Tip); + Assert.Equal(0, repo.Index.RetrieveStatus().Count()); + } + } + + [Fact] + public void ConflictingMergeRepos() + { + 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 + + MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature); + + Assert.Equal(MergeStatus.Conflicts, mergeResult.Status); + + Assert.Null(mergeResult.Commit); + Assert.Equal(1, repo.Index.Conflicts.Count()); + + var conflict = repo.Index.Conflicts.First(); + var changes = repo.Diff.Compare(repo.Lookup(conflict.Theirs.Id), repo.Lookup(conflict.Ours.Id)); + + Assert.False(changes.IsBinaryComparison); + } + } + + [Fact] + public void ConflictingMergeReposBinary() + { + const string firstBranchFileName = "first branch file.bin"; + const string secondBranchFileName = "second branch file.bin"; + const string sharedBranchFileName = "first+second branch file.bin"; + + 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, "\0The first branches comment\0"); // 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, "\0The second branches comment\0"); // Change file in second branch + + MergeResult mergeResult = repo.Merge(repo.Branches["FirstBranch"].Tip, Constants.Signature); + + Assert.Equal(MergeStatus.Conflicts, mergeResult.Status); + + Assert.Equal(1, repo.Index.Conflicts.Count()); + + Conflict conflict = repo.Index.Conflicts.First(); + + var changes = repo.Diff.Compare(repo.Lookup(conflict.Theirs.Id), repo.Lookup(conflict.Ours.Id)); + + Assert.True(changes.IsBinaryComparison); + } + } + + 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); + } } } diff --git a/LibGit2Sharp/Core/GitMergeOpts.cs b/LibGit2Sharp/Core/GitMergeOpts.cs new file mode 100644 index 000000000..1b371a371 --- /dev/null +++ b/LibGit2Sharp/Core/GitMergeOpts.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + internal enum GitMergeFlags + { + /// + /// Default + /// + GIT_MERGE_DEFAULT = 0, + + /// + /// Do not fast-forward. + /// + GIT_MERGE_NO_FASTFORWARD = 1, + + /// + /// Only perform fast-forward. + /// + GIT_MERGE_FASTFORWARD_ONLY = 2, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct GitMergeOpts + { + public uint Version; + + public GitMergeFlags MergeFlags; + public GitMergeTreeOpts MergeTreeOpts; + public GitCheckoutOpts CheckoutOpts; + } +} diff --git a/LibGit2Sharp/Core/GitMergeResult.cs b/LibGit2Sharp/Core/GitMergeResult.cs new file mode 100644 index 000000000..e335cabfa --- /dev/null +++ b/LibGit2Sharp/Core/GitMergeResult.cs @@ -0,0 +1,47 @@ +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + internal class GitMergeResult + { + internal GitMergeResult(GitMergeResultHandle handle) + { + IsUpToDate = Proxy.git_merge_result_is_uptodate(handle); + IsFastForward = Proxy.git_merge_result_is_fastforward(handle); + + if (IsFastForward) + { + FastForwardId = Proxy.git_merge_result_fastforward_oid(handle); + } + } + + public virtual bool IsUpToDate { get; private set; } + + public virtual bool IsFastForward { get; private set; } + + /// + /// The ID that a fast-forward merge should advance to. + /// + public virtual ObjectId FastForwardId { get; private set; } + + public virtual MergeStatus Status + { + get + { + if (IsUpToDate) + { + return MergeStatus.UpToDate; + } + else if (IsFastForward) + { + return MergeStatus.FastForward; + } + else + { + return MergeStatus.NonFastForward; + } + } + } + } +} diff --git a/LibGit2Sharp/Core/GitMergeTreeOpts.cs b/LibGit2Sharp/Core/GitMergeTreeOpts.cs new file mode 100644 index 000000000..dfcbe5970 --- /dev/null +++ b/LibGit2Sharp/Core/GitMergeTreeOpts.cs @@ -0,0 +1,57 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [Flags] + internal enum GitMergeTreeFlags + { + /// + /// No options. + /// + GIT_MERGE_TREE_NORMAL = 0, + + /// + /// GIT_MERGE_TREE_FIND_RENAMES in libgit2 + /// + GIT_MERGE_TREE_FIND_RENAMES = (1 << 0), + } + + internal enum GitMergeAutomergeFlags + { + GIT_MERGE_AUTOMERGE_NORMAL = 0, + GIT_MERGE_AUTOMERGE_NONE = 1, + GIT_MERGE_AUTOMERGE_FAVOR_OURS = 2, + GIT_MERGE_AUTOMERGE_FAVOR_THEIRS = 3, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct GitMergeTreeOpts + { + public uint Version; + + public GitMergeTreeFlags MergeTreeFlags; + + /// + /// Similarity to consider a file renamed. + /// + public uint RenameThreshold; + + /// + /// Maximum similarity sources to examine (overrides + /// 'merge.renameLimit' config (default 200) + /// + public uint TargetLimit; + + /// + /// Pluggable similarityMetric; pass IntPtr.Zero + /// to use internal metric. + /// + public IntPtr SimilarityMetric; + + /// + /// Flags for automerging content. + /// + public GitMergeAutomergeFlags MergeAutomergeFlags; + } +} diff --git a/LibGit2Sharp/Core/Handles/GitMergeHeadHandle.cs b/LibGit2Sharp/Core/Handles/GitMergeHeadHandle.cs new file mode 100644 index 000000000..f49e30e54 --- /dev/null +++ b/LibGit2Sharp/Core/Handles/GitMergeHeadHandle.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core.Handles +{ + internal class GitMergeHeadHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_merge_head_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/Handles/GitMergeResultHandle.cs b/LibGit2Sharp/Core/Handles/GitMergeResultHandle.cs new file mode 100644 index 000000000..f13b03e67 --- /dev/null +++ b/LibGit2Sharp/Core/Handles/GitMergeResultHandle.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core.Handles +{ + internal class GitMergeResultHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_merge_result_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 6b1accb2b..9b2f9215e 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -576,6 +576,55 @@ internal static extern int git_merge_base( GitObjectSafeHandle one, GitObjectSafeHandle two); + [DllImport(libgit2)] + internal static extern int git_merge_head_from_ref( + out GitMergeHeadHandle mergehead, + RepositorySafeHandle repo, + ReferenceSafeHandle reference); + + [DllImport(libgit2)] + internal static extern int git_merge_head_from_fetchhead( + out GitMergeHeadHandle mergehead, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string branch_name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string remote_url, + ref GitOid oid); + + [DllImport(libgit2)] + internal static extern int git_merge_head_from_oid( + out GitMergeHeadHandle mergehead, + RepositorySafeHandle repo, + ref GitOid oid); + + [DllImport(libgit2)] + internal static extern int git_merge( + out GitMergeResultHandle mergeResult, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] [In] IntPtr[] their_heads, + UIntPtr their_heads_len, + ref GitMergeOpts given_opts); + + [DllImport(libgit2)] + internal static extern int git_merge_result_is_uptodate( + GitMergeResultHandle merge_result); + + [DllImport(libgit2)] + internal static extern int git_merge_result_is_fastforward( + GitMergeResultHandle merge_result); + + [DllImport(libgit2)] + internal static extern int git_merge_result_fastforward_oid( + out GitOid oid, + GitMergeResultHandle merge_result); + + [DllImport(libgit2)] + internal static extern void git_merge_result_free( + IntPtr merge_result); + + [DllImport(libgit2)] + internal static extern void git_merge_head_free( + IntPtr merge_head); + [DllImport(libgit2)] internal static extern int git_message_prettify( byte[] message_out, // NB: This is more properly a StringBuilder, but it's UTF8 diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index 78c5a2beb..01377e043 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -956,6 +956,89 @@ public static ObjectId git_merge_base(RepositorySafeHandle repo, Commit first, C } } + public static GitMergeHeadHandle git_merge_head_from_oid(RepositorySafeHandle repo, GitOid oid) + { + using (ThreadAffinity()) + { + GitMergeHeadHandle their_head; + + int res = NativeMethods.git_merge_head_from_oid(out their_head, repo, ref oid); + + Ensure.ZeroResult(res); + + return their_head; + } + } + + public static GitMergeResultHandle git_merge(RepositorySafeHandle repo, GitMergeHeadHandle[] heads, GitMergeOpts options) + { + using (ThreadAffinity()) + { + GitMergeResultHandle ret; + + IntPtr[] their_heads = new IntPtr[heads.Length]; + for (int i = 0; i < heads.Length; i++) + { + their_heads[i] = heads[i].DangerousGetHandle(); + } + + int res = NativeMethods.git_merge( + out ret, + repo, + their_heads, + (UIntPtr)their_heads.Length, + ref options); + + Ensure.ZeroResult(res); + + return ret; + } + } + + public static bool git_merge_result_is_uptodate(GitMergeResultHandle handle) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_merge_result_is_uptodate(handle); + Ensure.BooleanResult(res); + + return (res == 1); + } + } + + public static bool git_merge_result_is_fastforward(GitMergeResultHandle handle) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_merge_result_is_fastforward(handle); + Ensure.BooleanResult(res); + + return (res == 1); + } + } + + public static GitOid git_merge_result_fastforward_oid(GitMergeResultHandle handle) + { + using (ThreadAffinity()) + { + GitOid oid; + int res = NativeMethods.git_merge_result_fastforward_oid(out oid, handle); + Ensure.ZeroResult(res); + + return oid; + } + } + + public static void git_merge_result_free(IntPtr handle) + { + NativeMethods.git_merge_result_free(handle); + } + + public static void git_merge_head_free(IntPtr handle) + { + NativeMethods.git_merge_head_free(handle); + } + #endregion #region git_message_ diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index 9c3c9f31d..952e1f648 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -193,6 +193,13 @@ public interface IRepository : IDisposable /// IEnumerable MergeHeads { get; } + /// + /// Merges the given commit into HEAD. + /// + /// The commit to use as a reference for the changes that should be merged into HEAD. + /// If the merge generates a merge commit (i.e. a non-fast forward merge), the of who made the merge. + MergeResult Merge(Commit commit, Signature merger); + /// /// Manipulate the currently ignored files. /// diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 54fc4b9c1..26e5aec30 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -78,19 +78,25 @@ + + + + + + diff --git a/LibGit2Sharp/MergeResult.cs b/LibGit2Sharp/MergeResult.cs new file mode 100644 index 000000000..fa2093362 --- /dev/null +++ b/LibGit2Sharp/MergeResult.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Class to report the result of a merge. + /// + public class MergeResult + { + /// + /// Needed for mocking purposes. + /// + protected MergeResult() + { } + + internal MergeResult(MergeStatus status, Commit commit = null) + { + this.Status = status; + this.Commit = commit; + } + + /// + /// The status of the merge. + /// + public virtual MergeStatus Status + { + get; + private set; + } + + /// + /// The resulting commit of the merge. For fast-forward merges, this is the + /// commit that merge was fast forwarded to. + /// This will return null if the merge has been unsuccessful due to conflicts. + /// + public virtual Commit Commit + { + get; + private set; + } + } + + /// + /// The status of what happened as a result of a merge. + /// + public enum MergeStatus + { + /// + /// Merge was up-to-date. + /// + UpToDate, + + /// + /// Fast-forward merge. + /// + FastForward, + + /// + /// A non fast-forward merge. + /// + NonFastForward, + + /// + /// Merge resulted in conflicts. + /// + Conflicts, + } +} diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 33f9d795e..092aa38d8 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -1032,6 +1032,80 @@ public IEnumerable MergeHeads } } + /// + /// Merges the given commit into HEAD as well as performing a Fast Forward if possible. + /// + /// The commit to use as a reference for the changes that should be merged into HEAD. + /// If the merge generates a merge commit (i.e. a non-fast forward merge), the of who made the merge. + /// The result of the performed merge . + public MergeResult Merge(Commit commit, Signature merger) + { + using (GitMergeHeadHandle mergeHeadHandle = Proxy.git_merge_head_from_oid(Handle, commit.Id.Oid)) + { + GitMergeOpts opts = new GitMergeOpts() + { + Version = 1, + MergeTreeOpts = { Version = 1 }, + CheckoutOpts = { version = 1 }, + }; + + + // Perform the merge in libgit2 and get the result back. + GitMergeResult gitMergeResult; + using (GitMergeResultHandle mergeResultHandle = Proxy.git_merge(Handle, new GitMergeHeadHandle[] { mergeHeadHandle }, opts)) + { + gitMergeResult = new GitMergeResult(mergeResultHandle); + } + + // Handle the result of the merge performed in libgit2 + // and commit the result / update the working directory as necessary. + MergeResult mergeResult; + switch(gitMergeResult.Status) + { + case MergeStatus.UpToDate: + mergeResult = new MergeResult(MergeStatus.UpToDate); + break; + case MergeStatus.FastForward: + Commit fastForwardCommit = this.Lookup(gitMergeResult.FastForwardId); + FastForward(fastForwardCommit); + mergeResult = new MergeResult(MergeStatus.FastForward, fastForwardCommit); + break; + case MergeStatus.NonFastForward: + { + if (Index.IsFullyMerged) + { + // Commit the merge + Commit mergeCommit = this.Commit(Info.Message, author: merger, committer: merger); + mergeResult = new MergeResult(MergeStatus.NonFastForward, mergeCommit); + } + else + { + mergeResult = new MergeResult(MergeStatus.Conflicts); + } + } + break; + default: + throw new NotImplementedException(string.Format("Unknown MergeStatus: {0}", gitMergeResult.Status)); + } + + return mergeResult; + } + } + + private void FastForward(Commit fastForwardCommit) + { + var checkoutOpts = new CheckoutOptions + { + CheckoutModifiers = CheckoutModifiers.None, + }; + + CheckoutTree(fastForwardCommit.Tree, null, checkoutOpts); + + Refs.UpdateTarget("HEAD", fastForwardCommit.Id.Sha); + + // TODO: Update Reflog... + } + internal StringComparer PathComparer { get { return pathCase.Value.Comparer; }