diff --git a/LibGit2Sharp.Tests/BranchFixture.cs b/LibGit2Sharp.Tests/BranchFixture.cs index 1691e3443..eda5a63c3 100644 --- a/LibGit2Sharp.Tests/BranchFixture.cs +++ b/LibGit2Sharp.Tests/BranchFixture.cs @@ -88,6 +88,20 @@ public void CanCreateBranchFromCommit() } } + [Fact] + public void CanCreateBranchFromRevparseSpec() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(); + using (var repo = new Repository(path.RepositoryPath)) + { + const string name = "revparse_branch"; + var target = repo.Lookup("master~2"); + Branch newBranch = repo.CreateBranch(name, target); + Assert.NotNull(newBranch); + Assert.Equal("9fd738e8f7967c078dceed8190330fc8648ee56a", newBranch.Tip.Sha); + } + } + [Fact] public void CreatingABranchFromATagPeelsToTheCommit() { @@ -150,15 +164,6 @@ public void CreatingBranchWithUnknownShaTargetThrows() } } - [Fact] - public void CreatingABranchPointingAtANonCanonicalReferenceThrows() - { - using (var repo = new Repository(BareTestRepoPath)) - { - Assert.Throws(() => repo.Branches.Add("nocanonicaltarget", "br2")); - } - } - [Fact] public void CreatingBranchWithBadParamsThrows() { diff --git a/LibGit2Sharp.Tests/RepositoryFixture.cs b/LibGit2Sharp.Tests/RepositoryFixture.cs index c5997dd88..3ec0e900f 100644 --- a/LibGit2Sharp.Tests/RepositoryFixture.cs +++ b/LibGit2Sharp.Tests/RepositoryFixture.cs @@ -296,6 +296,35 @@ public void CanLookupWhithShortIdentifers() } } + [Fact] + public void CanLookupUsingRevparseSyntax() + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Null(repo.Lookup("master^")); + + Assert.NotNull(repo.Lookup("master:new.txt")); + Assert.NotNull(repo.Lookup("master:new.txt")); + Assert.NotNull(repo.Lookup("master^")); + Assert.NotNull(repo.Lookup("master^")); + Assert.NotNull(repo.Lookup("master~3")); + Assert.NotNull(repo.Lookup("HEAD")); + Assert.NotNull(repo.Lookup("refs/heads/br2")); + } + } + + [Fact] + public void CanResolveAmbiguousRevparseSpecs() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var o1 = repo.Lookup("e90810b"); // This resolves to a tag + Assert.Equal("7b4384978d2493e851f9cca7858815fac9b10980", o1.Sha); + var o2 = repo.Lookup("e90810b8"); // This resolves to a commit + Assert.Equal("e90810b8df3e80c413d903f631643c716887138d", o2.Sha); + } + } + [Fact] public void LookingUpWithBadParamsThrows() { @@ -310,7 +339,7 @@ public void LookingUpWithBadParamsThrows() } } - [Fact] + [Fact, SkippableFact(Skip = "libgit2 not ready")] public void LookingUpWithATooShortShaThrows() { using (var repo = new Repository(BareTestRepoPath)) diff --git a/LibGit2Sharp.Tests/TagFixture.cs b/LibGit2Sharp.Tests/TagFixture.cs index ff24043b9..cb0d527e1 100644 --- a/LibGit2Sharp.Tests/TagFixture.cs +++ b/LibGit2Sharp.Tests/TagFixture.cs @@ -53,6 +53,19 @@ public void CanAddALightweightTagFromABranchName() } } + [Fact] + public void CanAddALightweightTagFromARevparseSpec() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(); + using (var repo = new Repository(path.RepositoryPath)) + { + Tag newTag = repo.Tags.Add("i_am_lightweight", "master^1^2"); + Assert.False(newTag.IsAnnotated); + Assert.NotNull(newTag); + Assert.Equal("c47800c7266a2be04c571c04d5a6614691ea99bd", newTag.Target.Sha); + } + } + [Fact] public void CanAddAndOverwriteALightweightTag() { @@ -124,6 +137,19 @@ public void CanAddAnAnnotatedTagFromSha() } } + [Fact] + public void CanAddAnAnnotatedTagFromARevparseSpec() + { + TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(); + using (var repo = new Repository(path.RepositoryPath)) + { + Tag newTag = repo.Tags.Add("unit_test", "master^1^2", signatureTim, "a new tag"); + Assert.NotNull(newTag); + Assert.True(newTag.IsAnnotated); + Assert.Equal("c47800c7266a2be04c571c04d5a6614691ea99bd", newTag.Target.Sha); + } + } + [Fact] // Ported from cgit (https://github.com/git/git/blob/1c08bf50cfcf924094eca56c2486a90e2bf1e6e2/t/t7004-tag.sh#L359) public void CanAddAnAnnotatedTagWithAnEmptyMessage() @@ -201,15 +227,6 @@ public void CreatingATagForAnUnknowReferenceThrows() } } - [Fact] - public void CreatingATagForANonCanonicalReferenceThrows() - { - using (var repo = new Repository(BareTestRepoPath)) - { - Assert.Throws(() => repo.ApplyTag("noncanonicaltarget", "br2")); - } - } - [Fact] // Ported from cgit (https://github.com/git/git/blob/1c08bf50cfcf924094eca56c2486a90e2bf1e6e2/t/t7004-tag.sh#L42) public void CreatingATagForAnUnknowObjectIdThrows() diff --git a/LibGit2Sharp.Tests/TreeFixture.cs b/LibGit2Sharp.Tests/TreeFixture.cs index d06dc3da2..766dcf6eb 100755 --- a/LibGit2Sharp.Tests/TreeFixture.cs +++ b/LibGit2Sharp.Tests/TreeFixture.cs @@ -174,8 +174,15 @@ public void CanRetrieveTreeEntryPath() TreeEntry anotherInstance = tree["branch_file.txt"]; Assert.Equal("branch_file.txt", anotherInstance.Path); + // From a rev-parse statement + var revparseTree = repo.Lookup("master:1"); + TreeEntry yetAnotherInstance = revparseTree["branch_file.txt"]; + Assert.Equal(completePath, yetAnotherInstance.Path); + Assert.Equal(tree, subTree); + Assert.Equal(revparseTree, tree); Assert.Equal(anotherInstance, anInstance); + Assert.Equal(yetAnotherInstance, anotherInstance); Assert.NotEqual(anotherInstance.Path, anInstance.Path); Assert.NotSame(anotherInstance, anInstance); } diff --git a/LibGit2Sharp/BranchCollection.cs b/LibGit2Sharp/BranchCollection.cs index 4146c0235..d37c85674 100644 --- a/LibGit2Sharp/BranchCollection.cs +++ b/LibGit2Sharp/BranchCollection.cs @@ -137,14 +137,14 @@ IEnumerator IEnumerable.GetEnumerator() /// Create a new local branch with the specified name /// /// The name of the branch. - /// The target which can be sha or a canonical reference name. + /// Revparse spec for the target commit. /// True to allow silent overwriting a potentially existing branch, false otherwise. /// - public virtual Branch Add(string name, string shaOrReferenceName, bool allowOverwrite = false) + public virtual Branch Add(string name, string commitish, bool allowOverwrite = false) { Ensure.ArgumentNotNullOrEmptyString(name, "name"); - ObjectId commitId = repo.LookupCommit(shaOrReferenceName).Id; + ObjectId commitId = repo.LookupCommit(commitish).Id; using (var osw = new ObjectSafeWrapper(commitId, repo)) { @@ -159,13 +159,13 @@ public virtual Branch Add(string name, string shaOrReferenceName, bool allowOver /// Create a new local branch with the specified name /// /// The name of the branch. - /// The target which can be sha or a canonical reference name. + /// Revparse spec for the target commit. /// True to allow silent overwriting a potentially existing branch, false otherwise. /// [Obsolete("This method will be removed in the next release. Please use Add() instead.")] - public virtual Branch Create(string name, string shaOrReferenceName, bool allowOverwrite = false) + public virtual Branch Create(string name, string commitish, bool allowOverwrite = false) { - return Add(name, shaOrReferenceName, allowOverwrite); + return Add(name, commitish, allowOverwrite); } /// diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 274f2d72f..cf25a5d63 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -639,6 +639,10 @@ public static bool RepositoryStateChecker(RepositorySafeHandle repositoryPtr, Fu [return : MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(FilePathMarshaler))] public static extern FilePath git_repository_workdir(RepositorySafeHandle repository); + [DllImport(libgit2)] + public static extern int git_revparse_single(out GitObjectSafeHandle obj, RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(Utf8Marshaler))] string spec); + [DllImport(libgit2)] public static extern void git_revwalk_free(IntPtr walker); diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index b1377d7e6..65b079606 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -69,9 +69,9 @@ public interface IRepository : IDisposable /// /// Checkout the specified branch, reference or SHA. /// - /// The sha of the commit, a canonical reference name or the name of the branch to checkout. + /// A revparse spec for the commit or branch to checkout. /// The new HEAD. - Branch Checkout(string shaOrReferenceName); + Branch Checkout(string commitOrBranchSpec); /// /// Try to lookup an object by its and . If no matching object is found, null will be returned. @@ -84,10 +84,10 @@ public interface IRepository : IDisposable /// /// Try to lookup an object by its sha or a reference canonical name and . If no matching object is found, null will be returned. /// - /// The sha or reference canonical name to lookup. + /// A revparse spec for the object to lookup. /// The kind of being looked up /// The or null if it was not found. - GitObject Lookup(string shaOrReferenceName, GitObjectType type = GitObjectType.Any); + GitObject Lookup(string objectish, GitObjectType type = GitObjectType.Any); /// /// Stores the content of the as a new into the repository. diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 395f5bee7..9ed0f38d6 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; using LibGit2Sharp.Core.Handles; @@ -346,61 +347,71 @@ internal GitObject LookupInternal(ObjectId id, GitObjectType type, FilePath know /// /// Try to lookup an object by its sha or a reference canonical name and . If no matching object is found, null will be returned. /// - /// The sha or reference canonical name to lookup. + /// A revparse spec for the object to lookup. /// The kind of being looked up /// The or null if it was not found. - public GitObject Lookup(string shaOrReferenceName, GitObjectType type = GitObjectType.Any) + public GitObject Lookup(string objectish, GitObjectType type = GitObjectType.Any) { - return Lookup(shaOrReferenceName, type, LookUpOptions.None); + return Lookup(objectish, type, LookUpOptions.None); } - internal GitObject Lookup(string shaOrReferenceName, GitObjectType type, LookUpOptions lookUpOptions) + private string PathFromRevparseSpec(string spec) { - ObjectId id; + if (spec.StartsWith(":/")) return null; + if (Regex.IsMatch(spec, @"^:.*:")) return null; - Reference reference = Refs[shaOrReferenceName]; - if (reference != null) - { - id = reference.PeelToTargetObjectId(); - } - else - { - ObjectId.TryParseInternal(shaOrReferenceName, out id, IdentifierSize.Shortest); - } + var m = Regex.Match(spec, @"[^@^ ]*:(.*)"); + return (m.Groups.Count > 1) ? m.Groups[1].Value : null; + } + + internal GitObject Lookup(string objectish, GitObjectType type, LookUpOptions lookUpOptions) + { + Ensure.ArgumentNotNullOrEmptyString(objectish, "commitOrBranchSpec"); + + GitObjectSafeHandle sh; + int result = NativeMethods.git_revparse_single(out sh, Handle, objectish); - if (id == null) + if ((GitErrorCode)result != GitErrorCode.Ok || sh.IsInvalid) { - if (lookUpOptions.Has(LookUpOptions.ThrowWhenNoGitObjectHasBeenFound)) + if (lookUpOptions.Has(LookUpOptions.ThrowWhenNoGitObjectHasBeenFound) && + result == (int)GitErrorCode.NotFound) { - Ensure.GitObjectIsNotNull(null, shaOrReferenceName); + Ensure.GitObjectIsNotNull(null, objectish); + } + + if (result == (int)GitErrorCode.Ambiguous) + { + throw new AmbiguousException(string.Format(CultureInfo.InvariantCulture, "Provided abbreviated ObjectId '{0}' is too short.", objectish)); } return null; } - GitObject gitObj = Lookup(id, type); - - if (lookUpOptions.Has(LookUpOptions.ThrowWhenNoGitObjectHasBeenFound)) + if (type != GitObjectType.Any && NativeMethods.git_object_type(sh) != type) { - Ensure.GitObjectIsNotNull(gitObj, shaOrReferenceName); + sh.SafeDispose(); + return null; } - if (!lookUpOptions.Has(LookUpOptions.DereferenceResultToCommit)) + var obj = GitObject.CreateFromPtr(sh, GitObject.ObjectIdOf(sh), this, PathFromRevparseSpec(objectish)); + sh.SafeDispose(); + + if (lookUpOptions.Has(LookUpOptions.DereferenceResultToCommit)) { - return gitObj; + return obj.DereferenceToCommit(objectish, + lookUpOptions.Has(LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit)); } - - return gitObj.DereferenceToCommit(shaOrReferenceName, lookUpOptions.Has(LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit)); + return obj; } /// /// Lookup a commit by its SHA or name, or throw if a commit is not found. /// - /// The SHA or name of the commit. + /// A revparse spec for the commit. /// The commit. - internal Commit LookupCommit(string shaOrReferenceName) + internal Commit LookupCommit(string commitish) { - return (Commit)Lookup(shaOrReferenceName, GitObjectType.Any, LookUpOptions.ThrowWhenNoGitObjectHasBeenFound | LookUpOptions.DereferenceResultToCommit | LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit); + return (Commit)Lookup(commitish, GitObjectType.Any, LookUpOptions.ThrowWhenNoGitObjectHasBeenFound | LookUpOptions.DereferenceResultToCommit | LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit); } /// @@ -430,20 +441,20 @@ public static string Discover(string startingPath) /// /// Checkout the specified branch, reference or SHA. /// - /// The sha of the commit, a canonical reference name or the name of the branch to checkout. + /// A revparse spec for the commit or branch to checkout. /// The new HEAD. - public Branch Checkout(string shaOrReferenceName) + public Branch Checkout(string commitOrBranchSpec) { // TODO: This does not yet checkout (write) the working directory - var branch = Branches[shaOrReferenceName]; + var branch = Branches[commitOrBranchSpec]; if (branch != null) { return Checkout(branch); } - var commitId = LookupCommit(shaOrReferenceName).Id; + var commitId = LookupCommit(commitOrBranchSpec).Id; Refs.UpdateTarget("HEAD", commitId.Sha); return Head; } @@ -466,17 +477,17 @@ public Branch Checkout(Branch branch) /// the content of the working tree to match. /// /// Flavor of reset operation to perform. - /// The sha or reference canonical name of the target commit object. - public void Reset(ResetOptions resetOptions, string shaOrReferenceName = "HEAD") + /// A revparse spec for the target commit object. + public void Reset(ResetOptions resetOptions, string commitish = "HEAD") { - Ensure.ArgumentNotNullOrEmptyString(shaOrReferenceName, "shaOrReferenceName"); + Ensure.ArgumentNotNullOrEmptyString(commitish, "commitOrBranchSpec"); if (resetOptions.Has(ResetOptions.Mixed) && Info.IsBare) { throw new LibGit2SharpException("Mixed reset is not allowed in a bare repository"); } - Commit commit = LookupCommit(shaOrReferenceName); + Commit commit = LookupCommit(commitish); //TODO: Check for unmerged entries @@ -501,16 +512,16 @@ public void Reset(ResetOptions resetOptions, string shaOrReferenceName = "HEAD") /// /// Replaces entries in the with entries from the specified commit. /// - /// The sha or reference canonical name of the target commit object. + /// A revparse spec for the target commit object. /// The list of paths (either files or directories) that should be considered. - public void Reset(string shaOrReferenceName = "HEAD", IEnumerable paths = null) + public void Reset(string commitish = "HEAD", IEnumerable paths = null) { if (Info.IsBare) { throw new LibGit2SharpException("Reset is not allowed in a bare repository"); } - Commit commit = LookupCommit(shaOrReferenceName); + Commit commit = LookupCommit(commitish); TreeChanges changes = Diff.Compare(commit.Tree, DiffTarget.Index, paths); Index.Reset(changes); diff --git a/LibGit2Sharp/RepositoryExtensions.cs b/LibGit2Sharp/RepositoryExtensions.cs index 379c67f96..01e9bc249 100644 --- a/LibGit2Sharp/RepositoryExtensions.cs +++ b/LibGit2Sharp/RepositoryExtensions.cs @@ -13,11 +13,11 @@ public static class RepositoryExtensions /// /// /// The being looked up. - /// The shaOrRef to lookup. + /// The revparse spec for the object to lookup. /// - public static T Lookup(this IRepository repository, string shaOrRef) where T : GitObject + public static T Lookup(this IRepository repository, string objectish) where T : GitObject { - return (T)repository.Lookup(shaOrRef, GitObject.TypeToTypeMap[typeof(T)]); + return (T)repository.Lookup(objectish, GitObject.TypeToTypeMap[typeof (T)]); } ///