diff --git a/LibGit2Sharp.Tests/MergeFixture.cs b/LibGit2Sharp.Tests/MergeFixture.cs index 70adeab7f..59043b16e 100644 --- a/LibGit2Sharp.Tests/MergeFixture.cs +++ b/LibGit2Sharp.Tests/MergeFixture.cs @@ -874,6 +874,72 @@ private Commit AddFileCommitToRepo(IRepository repository, string filename, stri return repository.Commit("New commit", Constants.Signature, Constants.Signature); } + + [Theory] + [InlineData("master", MergeAnalysis.Unborn | MergeAnalysis.FastForward, true)] + [InlineData("fast_forward", MergeAnalysis.Normal | MergeAnalysis.FastForward, false)] + [InlineData("conflicts", MergeAnalysis.Normal, false)] + + public void CanAnalyzeMerge(string branchName, MergeAnalysis analysis, bool makeUnborn) + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + if (makeUnborn) + { + repo.Refs.UpdateTarget("HEAD", "refs/heads/unborn"); + } + + // test both Commit and Reference overload + MergeAnalysisResult result = repo.AnalyzeMerge(repo.Branches[branchName].Tip); + MergeAnalysisResult result2 = repo.AnalyzeMerge(repo.Refs["refs/heads/" + branchName]); + + Assert.Equal(analysis, result.Analysis); + Assert.Equal(analysis, result2.Analysis); + } + } + + [Theory] + [InlineData("false", MergePreference.NoFastForward)] + [InlineData("only", MergePreference.FastForwardOnly)] + [InlineData(null, MergePreference.Default)] + public void CanRetrieveMergePreference(string value, MergePreference preference) + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + if (value != null) + { + repo.Config.Set("merge.ff", value); + } + + // it doesn't matter too much which branch we ask about, we want the preference + MergeAnalysisResult result = repo.AnalyzeMerge(repo.Branches["fast_forward"].Tip); + + Assert.Equal(preference, result.Preference); + } + } + + [Fact] + public void CanAnalyzeFastForward() + { + string path = SandboxMergeTestRepo(); + using (var repo = new Repository(path)) + { + // test both Commit and Reference overload + MergeAnalysisResult result = repo.AnalyzeMerge(repo.Branches["fast_forward"].Tip); + MergeAnalysisResult result2 = repo.AnalyzeMerge(repo.Refs["refs/heads/fast_forward"]); + + Assert.True(result.Analysis.HasFlag(MergeAnalysis.FastForward)); + Assert.True(result.Analysis.HasFlag(MergeAnalysis.Normal)); + Assert.False(result.Analysis.HasFlag(MergeAnalysis.Unborn)); + Assert.False(result.Analysis.HasFlag(MergeAnalysis.UpToDate)); + + Assert.Equal(result, result2); + } + } + + // Commit IDs of the checked in merge_testrepo private const string masterBranchInitialId = "83cebf5389a4adbcb80bda6b68513caee4559802"; private const string fastForwardBranchInitialId = "4dfaa1500526214ae7b33f9b2c1144ca8b6b1f53"; diff --git a/LibGit2Sharp.Tests/MetaFixture.cs b/LibGit2Sharp.Tests/MetaFixture.cs index 057c9a1d4..cb86974a0 100644 --- a/LibGit2Sharp.Tests/MetaFixture.cs +++ b/LibGit2Sharp.Tests/MetaFixture.cs @@ -18,6 +18,7 @@ public class MetaFixture private static readonly HashSet explicitOnlyInterfaces = new HashSet { typeof(IBelongToARepository), typeof(IDiffResult), + typeof(IAnnotatedCommit), }; [Fact] diff --git a/LibGit2Sharp/AnnotatedCommit.cs b/LibGit2Sharp/AnnotatedCommit.cs new file mode 100644 index 000000000..b0c2895db --- /dev/null +++ b/LibGit2Sharp/AnnotatedCommit.cs @@ -0,0 +1,65 @@ +using System; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// A commit with information about its source. Input for merge and rebase functions + /// + public class AnnotatedCommit : IDisposable, IAnnotatedCommit + { + internal readonly Repository repository; + internal AnnotatedCommitHandle Handle { get; private set; } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// The repository in which the commit lives + /// A string in extended SHA-1 syntax to look up the object + public AnnotatedCommit(Repository repository, string revspec) + { + this.repository = repository; + this.Handle = Proxy.git_annotated_commit_from_revspec(repository.Handle, revspec); + } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// The repository in which the commit lives + /// A reference pointing to the commit + public AnnotatedCommit(Repository repository, Reference reference) + { + this.repository = repository; + using (var refHandle = Proxy.git_reference_lookup(repository.Handle, reference.CanonicalName, true)) + { + this.Handle = Proxy.git_annotated_commit_from_ref(repository.Handle, refHandle); + } + } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// The repository in which the commit lives + /// A commit + public AnnotatedCommit(Repository repository, Commit commit) + { + this.repository = repository; + this.Handle = Proxy.git_annotated_commit_lookup(repository.Handle, commit.Id.Oid); + } + + /// + /// Releases all resource used by the object. + /// + public void Dispose() + { + Handle.Dispose(); + } + + AnnotatedCommit IAnnotatedCommit.GetAnnotatedCommit() + { + return this; + } + } +} + diff --git a/LibGit2Sharp/Commit.cs b/LibGit2Sharp/Commit.cs index 88b29621d..b5abc1cfe 100644 --- a/LibGit2Sharp/Commit.cs +++ b/LibGit2Sharp/Commit.cs @@ -13,7 +13,7 @@ namespace LibGit2Sharp /// A Commit /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class Commit : GitObject + public class Commit : GitObject, IAnnotatedCommit { private readonly GitObjectLazyGroup group1; private readonly GitObjectLazyGroup group2; @@ -138,6 +138,11 @@ private string DebuggerDisplay } } + AnnotatedCommit IAnnotatedCommit.GetAnnotatedCommit() + { + return repo.LookupAnnotatedCommit(this); + } + private class ParentsCollection : ICollection { private readonly Lazy> _parents; diff --git a/LibGit2Sharp/Core/DisposableArray.cs b/LibGit2Sharp/Core/DisposableArray.cs new file mode 100644 index 000000000..9ebc164f5 --- /dev/null +++ b/LibGit2Sharp/Core/DisposableArray.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace LibGit2Sharp.Core +{ + /// + /// An array containing disposables, for convenience when dealing with handles + /// + internal class DisposableArray : IDisposable where T : IDisposable + { + readonly T[] array; + + /// + /// Create a wrapper for the given array so the contents will be disposed when this class is disposed. + /// + /// The array of dispsables + public DisposableArray(T[] handles) + { + array = handles; + } + + /// + /// Create a wrapper for the given array so the contents will be disposed when this class is disposed. + /// + /// The enumerable is first made into an array + /// + /// + /// Handles. + public DisposableArray(IEnumerable handles) + { + array = handles.ToArray(); + } + + /// + /// The underlying array + /// + public T[] Array { get { return array; } } + + /// + /// Return the underlying array so we can use this wherever the methods expect an array + /// + public static implicit operator T[](DisposableArray da) + { + return da.array; + } + + /// + /// Call Dispose on each of the elements of the array + /// + public void Dispose() + { + foreach (var handle in array) + { + handle.Dispose(); + } + } + } +} + diff --git a/LibGit2Sharp/IAnnotatedCommit.cs b/LibGit2Sharp/IAnnotatedCommit.cs new file mode 100644 index 000000000..ae99152fb --- /dev/null +++ b/LibGit2Sharp/IAnnotatedCommit.cs @@ -0,0 +1,16 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// Interface to retrieve an annotated commit from another type + /// + public interface IAnnotatedCommit + { + /// + /// Retrieve an annotated commit from this object + /// + AnnotatedCommit GetAnnotatedCommit(); + } +} + diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index e7fa9c713..4243422e1 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -226,6 +226,17 @@ public interface IRepository : IDisposable /// The of the merge. MergeResult Merge(string committish, Signature merger, MergeOptions options); + /// + /// Analyze the possibilities of updating HEAD with the given commit(s). + /// + /// It expects objects convertible to annotated commits, so and + /// also work as inputs. + /// + /// + /// Commits to merge into HEAD + /// Which update methods are possible and which preference the user has specified + MergeAnalysisResult AnalyzeMerge(params IAnnotatedCommit[] commits); + /// /// Access to Rebase functionality. /// @@ -421,5 +432,23 @@ public interface IRepository : IDisposable /// The reference mentioned in the revision (if any) /// The object which the revision resolves to void RevParse(string revision, out Reference reference, out GitObject obj); + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A string in extended SHA-1 syntax to look up the object + AnnotatedCommit LookupAnnotatedCommit(string revspec); + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A reference pointing to the commit + AnnotatedCommit LookupAnnotatedCommit(Reference reference); + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A commit + AnnotatedCommit LookupAnnotatedCommit(Commit commit); } } diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 4854ba280..68536c1fd 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -119,10 +119,13 @@ + + + @@ -355,6 +358,9 @@ + + + diff --git a/LibGit2Sharp/MergeAnalysis.cs b/LibGit2Sharp/MergeAnalysis.cs new file mode 100644 index 000000000..a76c51e6a --- /dev/null +++ b/LibGit2Sharp/MergeAnalysis.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Description of the possibilities for merging one or multiple branches into HEAD + /// + [Flags] + public enum MergeAnalysis + { + /// + /// No update possible. This is a dummy to serve as default value. + /// + None = 0, + /// + /// A "normal" merge; both HEAD and the given merge input have diverged + /// from their common ancestor. The divergent commits must be merged. + /// + Normal = (1 << 0), + + /// + /// All given merge inputs are reachable from HEAD, meaning the + /// repository is up-to-date and no merge needs to be performed. + /// + UpToDate = (1 << 1), + + /// + /// The given merge input is a fast-forward from HEAD and no merge + /// needs to be performed. Instead, the client can check out the + /// given merge input. + /// + FastForward = (1 << 2), + + /// + /// The HEAD of the current repository is "unborn" and does not point to + /// a valid commit. No merge can be performed, but the caller may wish + /// to simply set HEAD to the target commit(s). + /// + Unborn = (1 << 3), + } +} diff --git a/LibGit2Sharp/MergeAnalysisResult.cs b/LibGit2Sharp/MergeAnalysisResult.cs new file mode 100644 index 000000000..343840a71 --- /dev/null +++ b/LibGit2Sharp/MergeAnalysisResult.cs @@ -0,0 +1,66 @@ +using System; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// The result of analyzing the possibilities of merging a commit or branch into HEAD. + /// + public struct MergeAnalysisResult + { + /// + /// The result of the analysis done on the commits given. + /// + public MergeAnalysis Analysis; + /// + /// The user's preference for the type of merge to perform. + /// + public MergePreference Preference; + + static MergeAnalysis MergeAnalysisFromGitMergeAnalysis(GitMergeAnalysis analysisIn) + { + MergeAnalysis analysis = default(MergeAnalysis); + + if (analysisIn.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_NORMAL)) + { + analysis |= MergeAnalysis.Normal; + } + if (analysisIn.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_UP_TO_DATE)) + { + analysis |= MergeAnalysis.UpToDate; + } + if (analysisIn.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_FASTFORWARD)) + { + analysis |= MergeAnalysis.FastForward; + } + + if (analysisIn.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_UNBORN)) + { + analysis |= MergeAnalysis.Unborn; + } + + return analysis; + } + + static MergePreference MergePreferenceFromGitMergePreference(GitMergePreference preference) + { + switch (preference) + { + case GitMergePreference.GIT_MERGE_PREFERENCE_NONE: + return MergePreference.Default; + case GitMergePreference.GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY: + return MergePreference.FastForwardOnly; + case GitMergePreference.GIT_MERGE_PREFERENCE_NO_FASTFORWARD: + return MergePreference.NoFastForward; + default: + throw new InvalidOperationException(String.Format("Unknown merge preference: {0}", preference)); + } + } + + internal MergeAnalysisResult(GitMergeAnalysis analysis, GitMergePreference preference) + { + Analysis = MergeAnalysisFromGitMergeAnalysis(analysis); + Preference = MergePreferenceFromGitMergePreference(preference); + } + } +} diff --git a/LibGit2Sharp/MergePreference.cs b/LibGit2Sharp/MergePreference.cs new file mode 100644 index 000000000..b3f4e7819 --- /dev/null +++ b/LibGit2Sharp/MergePreference.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Indicates the user's stated preference for merges + /// + public enum MergePreference + { + /// + /// No configuration was found that suggests a preferred behavior for + /// merge. + /// + Default = 0, + + /// + /// There is a `merge.ff=false` configuration setting, suggesting that + /// the user does not want to allow a fast-forward merge. + /// + NoFastForward = (1 << 0), + + /// + /// There is a `merge.ff=only` configuration setting, suggesting that + /// the user only wants fast-forward merges. + /// + FastForwardOnly = (1 << 1), + } +} diff --git a/LibGit2Sharp/Reference.cs b/LibGit2Sharp/Reference.cs index 40a85f79f..8d0e211ac 100644 --- a/LibGit2Sharp/Reference.cs +++ b/LibGit2Sharp/Reference.cs @@ -10,7 +10,7 @@ namespace LibGit2Sharp /// A Reference to another git object /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public abstract class Reference : IEquatable, IBelongToARepository + public abstract class Reference : IEquatable, IBelongToARepository, IAnnotatedCommit { private static readonly LambdaEqualityHelper equalityHelper = new LambdaEqualityHelper(x => x.CanonicalName, x => x.TargetIdentifier); @@ -255,5 +255,10 @@ IRepository IBelongToARepository.Repository return repo; } } + + AnnotatedCommit IAnnotatedCommit.GetAnnotatedCommit() + { + return repo.LookupAnnotatedCommit(this); + } } } diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index fc8783c76..62502ec2e 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -1412,6 +1412,32 @@ public CherryPickResult CherryPick(Commit commit, Signature committer, CherryPic return result; } + private MergeAnalysisResult AnalyzeMerge(AnnotatedCommitHandle[] annotatedCommits) + { + GitMergeAnalysis mergeAnalysis; + GitMergePreference mergePreference; + + Proxy.git_merge_analysis(Handle, annotatedCommits, out mergeAnalysis, out mergePreference); + return new MergeAnalysisResult(mergeAnalysis, mergePreference); + } + + /// + /// Analyze the possibilities of updating HEAD with the given commit(s). + /// + /// It expects objects convertible to annotated commits, so and + /// also work as inputs. + /// + /// + /// Commits to merge into HEAD + /// Which update methods are possible and which preference the user has specified + public MergeAnalysisResult AnalyzeMerge(params IAnnotatedCommit[] commits) + { + using (var annotated = new DisposableArray(commits.Select(c => c.GetAnnotatedCommit()))) + { + return AnalyzeMerge(annotated.Array.Select(c => c.Handle).ToArray()); + } + } + private FastForwardStrategy FastForwardStrategyFromMergePreference(GitMergePreference preference) { switch (preference) @@ -1888,5 +1914,32 @@ private string DebuggerDisplay Info.IsBare ? Info.Path : Info.WorkingDirectory); } } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A string in extended SHA-1 syntax to look up the object + public AnnotatedCommit LookupAnnotatedCommit(string revspec) + { + return new AnnotatedCommit(this, revspec); + } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A reference pointing to the commit + public AnnotatedCommit LookupAnnotatedCommit(Reference reference) + { + return new AnnotatedCommit(this, reference); + } + + /// + /// Initialize a from extended SHA-1 syntax + /// + /// A commit + public AnnotatedCommit LookupAnnotatedCommit(Commit commit) + { + return new AnnotatedCommit(this, commit); + } } }