diff --git a/LibGit2Sharp.Tests/SubmoduleFixture.cs b/LibGit2Sharp.Tests/SubmoduleFixture.cs index 58c8a830a..bddfc76cf 100644 --- a/LibGit2Sharp.Tests/SubmoduleFixture.cs +++ b/LibGit2Sharp.Tests/SubmoduleFixture.cs @@ -206,6 +206,129 @@ 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(); + + 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); + 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 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/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..30556be2e 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -47,6 +47,75 @@ public virtual Submodule this[string name] } } + /// + /// 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() { } ). + /// 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"); + + 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 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); + } + + return this[relativePath]; + } + /// /// Initialize specified submodule. ///