From 73f831d6ed56f878061ce29834d97f309193916d Mon Sep 17 00:00:00 2001 From: Jason Goemaat Date: Mon, 9 Nov 2015 20:40:38 -0600 Subject: [PATCH 1/4] SubmoduleCollection.Add() using clone SubmoduleCollection.Add(url, subdir) acts like the command line 'git submodule add url subdir'. It calls git_submodule_add_setup, then deletes the directory created by that and clones into the same directory and calls git_submodule_add_finalize. The end result is that the .gitmodules file and the 'file' for the submodule directory are staged and ready to commit. --- LibGit2Sharp.Tests/SubmoduleFixture.cs | 58 ++++++++++++++++++++++++++ LibGit2Sharp/Core/NativeMethods.cs | 12 ++++++ LibGit2Sharp/Core/Proxy.cs | 15 +++++++ LibGit2Sharp/SubmoduleCollection.cs | 44 +++++++++++++++++++ 4 files changed, 129 insertions(+) diff --git a/LibGit2Sharp.Tests/SubmoduleFixture.cs b/LibGit2Sharp.Tests/SubmoduleFixture.cs index 58c8a830a..59cb1581b 100644 --- a/LibGit2Sharp.Tests/SubmoduleFixture.cs +++ b/LibGit2Sharp.Tests/SubmoduleFixture.cs @@ -206,6 +206,64 @@ public void CanInitSubmodule() } } + [Fact] + public void CanAddSubmodule() + { + //var path = SandboxSubmoduleTestRepo(); + var path = SandboxStandardTestRepo(); + var pathSubRepoOrigin = SandboxStandardTestRepo(); + + string submoduleSubPath = "submodule_target_wd"; + string expectedSubmodulePath = Path.GetFullPath(Path.Combine(path, submoduleSubPath)); + string expectedSubmoduleUrl = pathSubRepoOrigin.Replace('\\', '/'); + ObjectId expectedCommitId = (ObjectId)"32eab9cb1f450b5fe7ab663462b77d7f4b703344"; + + using (var repo = new Repository(path)) + { + // check on adding config entry + var configEntryBeforeAdd = repo.Config.Get(string.Format("submodule.{0}.url", submoduleSubPath)); + Assert.Null(configEntryBeforeAdd); + + // add submodule + Submodule submodule = repo.Submodules.Add(pathSubRepoOrigin, submoduleSubPath); + Assert.NotNull(submodule); + + // check that the expected commit is checked out, but not set in parent repo until committed + Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].WorkDirCommitId); + Assert.Null(repo.Submodules[submoduleSubPath].HeadCommitId); + + // check status + var submoduleStatus = submodule.RetrieveStatus(); + Assert.True((submoduleStatus & SubmoduleStatus.InIndex) == SubmoduleStatus.InIndex); + Assert.True((submoduleStatus & SubmoduleStatus.InConfig) == SubmoduleStatus.InConfig); + Assert.True((submoduleStatus & SubmoduleStatus.InWorkDir) == SubmoduleStatus.InWorkDir); + Assert.True((submoduleStatus & SubmoduleStatus.IndexAdded) == SubmoduleStatus.IndexAdded); + + // check that config entry was added with the correct url + var configEntryAfterAdd = repo.Config.Get(string.Format("submodule.{0}.url", submoduleSubPath)); + Assert.NotNull(configEntryAfterAdd); + Assert.Equal(expectedSubmoduleUrl, configEntryAfterAdd.Value); + + // check on directory being added and repository directory + Assert.True(Directory.Exists(expectedSubmodulePath)); + Assert.True(Directory.Exists(Path.Combine(expectedSubmodulePath, ".git"))); + + // manually check commit by opening submodule as a repository + using (var repo2 = new Repository(expectedSubmodulePath)) + { + Assert.False(repo2.Info.IsHeadDetached); + Assert.False(repo2.Info.IsHeadUnborn); + Commit headCommit = repo2.Head.Tip; + Assert.Equal(headCommit.Id, expectedCommitId); + } + + // commit parent repository, then verify it reports the correct CommitId for the submodule + Signature signature = repo.Config.BuildSignature(DateTimeOffset.Now); + repo.Commit("Added submodule " + submoduleSubPath, signature, signature); + Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].HeadCommitId); + } + } + [Fact] public void UpdatingUninitializedSubmoduleThrows() { diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 0e3d6b3fc..08ea5837d 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1557,6 +1557,18 @@ internal static extern void git_status_list_free( internal static extern void git_strarray_free( ref GitStrArray array); + [DllImport(libgit2)] + internal static extern int git_submodule_add_setup( + out SubmoduleSafeHandle reference, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, + bool use_gitlink); + + [DllImport(libgit2)] + internal static extern int git_submodule_add_finalize( + SubmoduleSafeHandle submodule); + [DllImport(libgit2)] internal static extern int git_submodule_lookup( out SubmoduleSafeHandle reference, diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index dec711d24..43be9c1e4 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -2924,6 +2924,21 @@ public static ICollection git_submodule_foreach(RepositorySafe return git_foreach(resultSelector, c => NativeMethods.git_submodule_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero)); } + public static SubmoduleSafeHandle git_submodule_add_setup(RepositorySafeHandle repo, string url, FilePath path, bool useGitLink) + { + SubmoduleSafeHandle sub; + var res = NativeMethods.git_submodule_add_setup(out sub, repo, url, path, useGitLink); + Ensure.ZeroResult(res); + return sub; + } + + public static void git_submodule_add_finalize(SubmoduleSafeHandle submodule) + { + // This should be called on a submodule once you have called add setup and done the clone of the submodule. This adds the .gitmodules file and the newly cloned submodule to the index to be ready to be committed (but doesn't actually do the commit). + var res = NativeMethods.git_submodule_add_finalize(submodule); + Ensure.ZeroResult(res); + } + public static void git_submodule_add_to_index(SubmoduleSafeHandle submodule, bool write_index) { var res = NativeMethods.git_submodule_add_to_index(submodule, write_index); diff --git a/LibGit2Sharp/SubmoduleCollection.cs b/LibGit2Sharp/SubmoduleCollection.cs index 2ad23f6df..63158d0d6 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -47,6 +47,50 @@ public virtual Submodule this[string name] } } + /// + /// Adds a new submodule, cloning into the new directory and staging it and + /// .gitmodules to the parent repository just like the command line 'git submodule add' + /// + /// The url of the remote repository + /// The path of the submodule inside of the parent repository, which will also become the submodule name. + /// + public virtual Submodule Add(string url, string relativePath) + { + Ensure.ArgumentNotNullOrEmptyString(relativePath, "relativePath"); + + Ensure.ArgumentNotNullOrEmptyString(url, "url"); + + using (SubmoduleSafeHandle handle = Proxy.git_submodule_add_setup(repo.Handle, url, relativePath, false)) + { + // get the full path to the submodule directory + string subPath = System.IO.Path.Combine(repo.Info.WorkingDirectory, relativePath); + + // the directory is created now and has a .git folder with no commits and I could fetch from + // the remote and checkout some branch, but I want to do a full clone to create all the tracking + // branches and checkout whatever the remote HEAD is, which seems hard to find. + System.IO.Directory.Delete(subPath, true); + + // now clone + //Proxy.git_submodule_init(handle, true); + //GitSubmoduleOptions options = new GitSubmoduleOptions(); + //Proxy.git_submodule_update(handle, true, ref options); + string result = Repository.Clone(url, subPath, new CloneOptions() { } ); + + //using (Repository subRep = new Repository(subPath)) + //{ + // subRep.Fetch("origin"); + // var refs = subRep.Network.ListReferences(subRep.Network.Remotes["origin"]); + // //Branch b = subRep.Checkout(dr.CanonicalName); + // var fhs = subRep.Network.FetchHeads.Select(_ => new { CN = _.CanonicalName, RCN = _.RemoteCanonicalName }).ToArray(); + // //string defbranch = subRep.Network.Remotes["origin"].DefaultBranch; + //} + + Proxy.git_submodule_add_finalize(handle); + } + + return this[relativePath]; + } + /// /// Initialize specified submodule. /// From 514eb8ee0a19566a0fd2de95d0fc4a93b903b10e Mon Sep 17 00:00:00 2001 From: Jason Goemaat Date: Wed, 11 Nov 2015 22:19:46 -0600 Subject: [PATCH 2/4] Hopefully fixed test error that didn't occur on development machine --- LibGit2Sharp.Tests/SubmoduleFixture.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/LibGit2Sharp.Tests/SubmoduleFixture.cs b/LibGit2Sharp.Tests/SubmoduleFixture.cs index 59cb1581b..f6e512861 100644 --- a/LibGit2Sharp.Tests/SubmoduleFixture.cs +++ b/LibGit2Sharp.Tests/SubmoduleFixture.cs @@ -209,6 +209,9 @@ public void CanInitSubmodule() [Fact] public void CanAddSubmodule() { + string configPath = CreateConfigurationWithDummyUser(Constants.Identity); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + //var path = SandboxSubmoduleTestRepo(); var path = SandboxStandardTestRepo(); var pathSubRepoOrigin = SandboxStandardTestRepo(); @@ -218,7 +221,7 @@ public void CanAddSubmodule() string expectedSubmoduleUrl = pathSubRepoOrigin.Replace('\\', '/'); ObjectId expectedCommitId = (ObjectId)"32eab9cb1f450b5fe7ab663462b77d7f4b703344"; - using (var repo = new Repository(path)) + using (var repo = new Repository(path, options)) { // check on adding config entry var configEntryBeforeAdd = repo.Config.Get(string.Format("submodule.{0}.url", submoduleSubPath)); From 7b64825087274769ebb06a5a0cfc5e11a7e57981 Mon Sep 17 00:00:00 2001 From: Jason Goemaat Date: Sat, 21 Nov 2015 06:05:31 -0600 Subject: [PATCH 3/4] Add option to clone manually when adding a submodule --- LibGit2Sharp.Tests/SubmoduleFixture.cs | 62 ++++++++++++++++++++++++++ LibGit2Sharp/SubmoduleCollection.cs | 26 +++++------ 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/LibGit2Sharp.Tests/SubmoduleFixture.cs b/LibGit2Sharp.Tests/SubmoduleFixture.cs index f6e512861..bddfc76cf 100644 --- a/LibGit2Sharp.Tests/SubmoduleFixture.cs +++ b/LibGit2Sharp.Tests/SubmoduleFixture.cs @@ -267,6 +267,68 @@ public void CanAddSubmodule() } } + + [Fact] + public void CanAddSubmoduleWithManualClone() + { + string configPath = CreateConfigurationWithDummyUser(Constants.Identity); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + + //var path = SandboxSubmoduleTestRepo(); + var path = SandboxStandardTestRepo(); + var pathSubRepoOrigin = SandboxStandardTestRepo(); + + string submoduleSubPath = "submodule_target_wd"; + string expectedSubmodulePath = Path.GetFullPath(Path.Combine(path, submoduleSubPath)); + string expectedSubmoduleUrl = pathSubRepoOrigin.Replace('\\', '/'); + ObjectId expectedCommitId = (ObjectId)"32eab9cb1f450b5fe7ab663462b77d7f4b703344"; + + using (var repo = new Repository(path, options)) + { + // check on adding config entry + var configEntryBeforeAdd = repo.Config.Get(string.Format("submodule.{0}.url", submoduleSubPath)); + Assert.Null(configEntryBeforeAdd); + + // add submodule + Submodule submodule = repo.Submodules.Add(pathSubRepoOrigin, submoduleSubPath, x => Repository.Clone(pathSubRepoOrigin, x, new CloneOptions() { } )); + Assert.NotNull(submodule); + + // check that the expected commit is checked out, but not set in parent repo until committed + Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].WorkDirCommitId); + Assert.Null(repo.Submodules[submoduleSubPath].HeadCommitId); + + // check status + var submoduleStatus = submodule.RetrieveStatus(); + Assert.True((submoduleStatus & SubmoduleStatus.InIndex) == SubmoduleStatus.InIndex); + Assert.True((submoduleStatus & SubmoduleStatus.InConfig) == SubmoduleStatus.InConfig); + Assert.True((submoduleStatus & SubmoduleStatus.InWorkDir) == SubmoduleStatus.InWorkDir); + Assert.True((submoduleStatus & SubmoduleStatus.IndexAdded) == SubmoduleStatus.IndexAdded); + + // check that config entry was added with the correct url + var configEntryAfterAdd = repo.Config.Get(string.Format("submodule.{0}.url", submoduleSubPath)); + Assert.NotNull(configEntryAfterAdd); + Assert.Equal(expectedSubmoduleUrl, configEntryAfterAdd.Value); + + // check on directory being added and repository directory + Assert.True(Directory.Exists(expectedSubmodulePath)); + Assert.True(Directory.Exists(Path.Combine(expectedSubmodulePath, ".git"))); + + // manually check commit by opening submodule as a repository + using (var repo2 = new Repository(expectedSubmodulePath)) + { + Assert.False(repo2.Info.IsHeadDetached); + Assert.False(repo2.Info.IsHeadUnborn); + Commit headCommit = repo2.Head.Tip; + Assert.Equal(headCommit.Id, expectedCommitId); + } + + // commit parent repository, then verify it reports the correct CommitId for the submodule + Signature signature = repo.Config.BuildSignature(DateTimeOffset.Now); + repo.Commit("Added submodule " + submoduleSubPath, signature, signature); + Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].HeadCommitId); + } + } + [Fact] public void UpdatingUninitializedSubmoduleThrows() { diff --git a/LibGit2Sharp/SubmoduleCollection.cs b/LibGit2Sharp/SubmoduleCollection.cs index 63158d0d6..3f1117e9e 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -53,8 +53,10 @@ public virtual Submodule this[string name] /// /// The url of the remote repository /// The path of the submodule inside of the parent repository, which will also become the submodule name. + /// A method that takes the full path to where we expect the repository to be cloned to so the caller + /// can clone it themselves. If not specified or if null, Add() will perform the clone using Repository.Clone(url, subPath, new CloneOptions() { } ). /// - public virtual Submodule Add(string url, string relativePath) + public virtual Submodule Add(string url, string relativePath, Action cloneMethod = null) { Ensure.ArgumentNotNullOrEmptyString(relativePath, "relativePath"); @@ -70,20 +72,14 @@ public virtual Submodule Add(string url, string relativePath) // branches and checkout whatever the remote HEAD is, which seems hard to find. System.IO.Directory.Delete(subPath, true); - // now clone - //Proxy.git_submodule_init(handle, true); - //GitSubmoduleOptions options = new GitSubmoduleOptions(); - //Proxy.git_submodule_update(handle, true, ref options); - string result = Repository.Clone(url, subPath, new CloneOptions() { } ); - - //using (Repository subRep = new Repository(subPath)) - //{ - // subRep.Fetch("origin"); - // var refs = subRep.Network.ListReferences(subRep.Network.Remotes["origin"]); - // //Branch b = subRep.Checkout(dr.CanonicalName); - // var fhs = subRep.Network.FetchHeads.Select(_ => new { CN = _.CanonicalName, RCN = _.RemoteCanonicalName }).ToArray(); - // //string defbranch = subRep.Network.Remotes["origin"].DefaultBranch; - //} + // now clone the repository, or let the caller do it if an action was specified + if (cloneMethod != null) + { + cloneMethod(subPath); + } else + { + string result = Repository.Clone(url, subPath, new CloneOptions() { }); + } Proxy.git_submodule_add_finalize(handle); } From 7e0003fd2514d8fc5184622c9d838c1862d7459a Mon Sep 17 00:00:00 2001 From: Jason Goemaat Date: Tue, 1 Dec 2015 14:03:52 -0600 Subject: [PATCH 4/4] Change SubmoduleCollection.Add() to have an overloaded method --- LibGit2Sharp/SubmoduleCollection.cs | 39 +++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/LibGit2Sharp/SubmoduleCollection.cs b/LibGit2Sharp/SubmoduleCollection.cs index 3f1117e9e..30556be2e 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -48,15 +48,43 @@ public virtual Submodule this[string name] } /// - /// Adds a new submodule, cloning into the new directory and staging it and - /// .gitmodules to the parent repository just like the command line 'git submodule add' + /// Adds a new submodule, calling the passed action to allow the caller + /// to clone the submodule into the passed path. The submodule ends + /// up being staged along with the .gitmodules just like the command + /// line 'git submodule add' /// /// The url of the remote repository /// The path of the submodule inside of the parent repository, which will also become the submodule name. /// A method that takes the full path to where we expect the repository to be cloned to so the caller /// can clone it themselves. If not specified or if null, Add() will perform the clone using Repository.Clone(url, subPath, new CloneOptions() { } ). - /// - public virtual Submodule Add(string url, string relativePath, Action cloneMethod = null) + /// The new Submodule + public virtual Submodule Add(string url, string relativePath, Action cloneMethod) + { + return MainAddSubmodule(url, relativePath, cloneMethod); + } + + /// + /// Adds a new submodule and clones it using the passed url. The + /// url must be supported for cloning by LibGit2Sharp. + /// + /// The url of the remote repository + /// The path of the submodule inside of the parent repository, which will also become the submodule name. + /// The new Submodule + public virtual Submodule Add(string url, string relativePath) + { + return MainAddSubmodule(url, relativePath); + } + + /// + /// Method that actually adds a submodule, called by overloaded Add() + /// methods. + /// + /// The url of the remote repository + /// The path of the submodule inside of the parent repository, which will also become the submodule name. + /// A method that takes the full path to where we expect the repository to be cloned to so the caller + /// can clone it themselves. If not specified or if null, Add() will perform the clone using Repository.Clone(url, subPath, new CloneOptions() { } ). + /// The new Submodule + Submodule MainAddSubmodule(string url, string relativePath, Action cloneMethod = null) { Ensure.ArgumentNotNullOrEmptyString(relativePath, "relativePath"); @@ -76,7 +104,8 @@ public virtual Submodule Add(string url, string relativePath, Action clo if (cloneMethod != null) { cloneMethod(subPath); - } else + } + else { string result = Repository.Clone(url, subPath, new CloneOptions() { }); }