diff --git a/LibGit2Sharp.Tests/CloneFixture.cs b/LibGit2Sharp.Tests/CloneFixture.cs index 2ffb5fe19..f6170daa7 100644 --- a/LibGit2Sharp.Tests/CloneFixture.cs +++ b/LibGit2Sharp.Tests/CloneFixture.cs @@ -157,5 +157,29 @@ public void CanCloneWithCredentials() Assert.False(repo.Info.IsBare); } } + + [Fact] + public void CanCloneRecursively() + { + string sourcePath = BareSubmoduleTestRepoPath; + var scd = BuildSelfCleaningDirectory(); + + Repository.Clone(sourcePath, scd.DirectoryPath, new CloneOptions() + { + Recursive = true + }); + + using (var repo = new Repository(scd.DirectoryPath)) + { + Assert.False(repo.Index.RetrieveStatus().IsDirty); + } + + string clonedSubmodulePath = Path.Combine(scd.DirectoryPath, "testrepo"); + using (var repo = new Repository(clonedSubmodulePath)) + { + Assert.False(repo.Index.RetrieveStatus().IsDirty); + Assert.True(File.Exists(Path.Combine(clonedSubmodulePath, "new.txt"))); + } + } } } diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/HEAD b/LibGit2Sharp.Tests/Resources/submodules.git/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/LibGit2Sharp.Tests/Resources/submodules.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/config b/LibGit2Sharp.Tests/Resources/submodules.git/config new file mode 100644 index 000000000..90e16477b --- /dev/null +++ b/LibGit2Sharp.Tests/Resources/submodules.git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = true + symlinks = false + ignorecase = true + hideDotFiles = dotGitOnly diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/description b/LibGit2Sharp.Tests/Resources/submodules.git/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/LibGit2Sharp.Tests/Resources/submodules.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/info/exclude b/LibGit2Sharp.Tests/Resources/submodules.git/info/exclude new file mode 100644 index 000000000..a5196d1be --- /dev/null +++ b/LibGit2Sharp.Tests/Resources/submodules.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/objects/9d/4e659e2429c884eb50193880bd09613a8ddba5 b/LibGit2Sharp.Tests/Resources/submodules.git/objects/9d/4e659e2429c884eb50193880bd09613a8ddba5 new file mode 100644 index 000000000..5b24f60cc Binary files /dev/null and b/LibGit2Sharp.Tests/Resources/submodules.git/objects/9d/4e659e2429c884eb50193880bd09613a8ddba5 differ diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/objects/b4/f28943fad380f4ee3a9c6b95259b28204cc25a b/LibGit2Sharp.Tests/Resources/submodules.git/objects/b4/f28943fad380f4ee3a9c6b95259b28204cc25a new file mode 100644 index 000000000..653238cd8 Binary files /dev/null and b/LibGit2Sharp.Tests/Resources/submodules.git/objects/b4/f28943fad380f4ee3a9c6b95259b28204cc25a differ diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/objects/f8/8c04f3e03ceee53ccd40a5cb16b19c9386162f b/LibGit2Sharp.Tests/Resources/submodules.git/objects/f8/8c04f3e03ceee53ccd40a5cb16b19c9386162f new file mode 100644 index 000000000..8f15858ae Binary files /dev/null and b/LibGit2Sharp.Tests/Resources/submodules.git/objects/f8/8c04f3e03ceee53ccd40a5cb16b19c9386162f differ diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/packed-refs b/LibGit2Sharp.Tests/Resources/submodules.git/packed-refs new file mode 100644 index 000000000..c59426e3f --- /dev/null +++ b/LibGit2Sharp.Tests/Resources/submodules.git/packed-refs @@ -0,0 +1,2 @@ +# pack-refs with: peeled fully-peeled +9d4e659e2429c884eb50193880bd09613a8ddba5 refs/heads/master diff --git a/LibGit2Sharp.Tests/Resources/submodules.git/refs/.gitkeep b/LibGit2Sharp.Tests/Resources/submodules.git/refs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index c4514e3c1..11fc9b616 100644 --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs +++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs @@ -31,6 +31,7 @@ static BaseFixture() public static string MergedTestRepoWorkingDirPath { get; private set; } public static string MergeTestRepoWorkingDirPath { get; private set; } public static string SubmoduleTestRepoWorkingDirPath { get; private set; } + public static string BareSubmoduleTestRepoPath { get; private set; } public static DirectoryInfo ResourcesDirectory { get; private set; } public static bool IsFileSystemCaseSensitive { get; private set; } @@ -64,6 +65,7 @@ private static void SetUpTestEnvironment() MergedTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "mergedrepo_wd"); MergeTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "merge_testrepo_wd"); SubmoduleTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "submodule_wd"); + BareSubmoduleTestRepoPath = Path.Combine(ResourcesDirectory.FullName, "submodules.git"); } private static bool IsFileSystemCaseSensitiveInternal() diff --git a/LibGit2Sharp/CloneOptions.cs b/LibGit2Sharp/CloneOptions.cs index 09e513f75..1977ed18e 100644 --- a/LibGit2Sharp/CloneOptions.cs +++ b/LibGit2Sharp/CloneOptions.cs @@ -26,6 +26,11 @@ public CloneOptions() /// public bool Checkout { get; set; } + /// + /// True to init and update submodules recursively after checkout + /// + public bool Recursive { get; set; } + /// /// Handler for network transfer and indexing progress information /// diff --git a/LibGit2Sharp/Core/GitRepositoryInitOptions.cs b/LibGit2Sharp/Core/GitRepositoryInitOptions.cs index c42bc83f4..8efab5d19 100644 --- a/LibGit2Sharp/Core/GitRepositoryInitOptions.cs +++ b/LibGit2Sharp/Core/GitRepositoryInitOptions.cs @@ -16,26 +16,23 @@ internal class GitRepositoryInitOptions : IDisposable public IntPtr InitialHead; public IntPtr OriginUrl; - public static GitRepositoryInitOptions BuildFrom(FilePath workdirPath, bool isBare) + public static GitRepositoryInitOptions BuildFrom(FilePath workdirPath, GitRepositoryInitFlags flags) { + flags |= GitRepositoryInitFlags.GIT_REPOSITORY_INIT_MKPATH; + var opts = new GitRepositoryInitOptions { - Flags = GitRepositoryInitFlags.GIT_REPOSITORY_INIT_MKPATH, + Flags = flags, Mode = 0 /* GIT_REPOSITORY_INIT_SHARED_UMASK */ }; if (workdirPath != null) { - Debug.Assert(!isBare); + Debug.Assert((flags & GitRepositoryInitFlags.GIT_REPOSITORY_INIT_BARE) == 0); opts.WorkDirPath = StrictFilePathMarshaler.FromManaged(workdirPath); } - if (isBare) - { - opts.Flags |= GitRepositoryInitFlags.GIT_REPOSITORY_INIT_BARE; - } - return opts; } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 84dbee9be..1862370af 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -239,6 +239,14 @@ internal static extern int git_clone( [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string origin_url, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath workdir_path, ref GitCloneOptions opts); + + [DllImport(libgit2)] + internal static extern int git_clone_into( + RepositorySafeHandle repo, + RemoteSafeHandle remote, + ref GitCheckoutOpts co_opts, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string branch, + SignatureSafeHandle signature); [DllImport(libgit2)] internal static extern IntPtr git_commit_author(GitObjectSafeHandle commit); @@ -1360,6 +1368,12 @@ internal static extern int git_submodule_reload( internal static extern int git_submodule_status( out SubmoduleStatus status, SubmoduleSafeHandle submodule); + + [DllImport(libgit2)] + internal static extern int git_submodule_resolve_url( + GitBuf buffer, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url); [DllImport(libgit2)] internal static extern int git_tag_annotation_create( diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index e8a682455..24dfe743c 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -289,6 +289,23 @@ public static RepositorySafeHandle git_clone( } } + public static RepositorySafeHandle git_clone_into( + RepositorySafeHandle repo, + RemoteSafeHandle remote, + GitCheckoutOpts checkoutOptions, + string branch, + Signature signature) + { + using (ThreadAffinity()) + using (SignatureSafeHandle signatureHandle = signature.BuildHandle()) + { + int res = NativeMethods.git_clone_into(repo, remote, ref checkoutOptions, + branch, signatureHandle); + Ensure.ZeroResult(res); + return repo; + } + } + #endregion #region git_commit_ @@ -1987,10 +2004,10 @@ public static IndexSafeHandle git_repository_index(RepositorySafeHandle repo) public static RepositorySafeHandle git_repository_init_ext( FilePath workdirPath, FilePath gitdirPath, - bool isBare) + GitRepositoryInitFlags flags) { using (ThreadAffinity()) - using (var opts = GitRepositoryInitOptions.BuildFrom(workdirPath, isBare)) + using (var opts = GitRepositoryInitOptions.BuildFrom(workdirPath, flags)) { RepositorySafeHandle repo; int res = NativeMethods.git_repository_init_ext(out repo, gitdirPath, opts); @@ -2000,6 +2017,20 @@ public static RepositorySafeHandle git_repository_init_ext( } } + public static RepositorySafeHandle git_repository_init_ext( + FilePath workdirPath, + FilePath gitdirPath, + bool isBare) + { + GitRepositoryInitFlags flags = 0; + if (isBare) + { + flags |= GitRepositoryInitFlags.GIT_REPOSITORY_INIT_BARE; + } + + return git_repository_init_ext(workdirPath, gitdirPath, flags); + } + public static bool git_repository_is_bare(RepositorySafeHandle repo) { return RepositoryStateChecker(repo, NativeMethods.git_repository_is_bare); @@ -2557,6 +2588,19 @@ public static SubmoduleStatus git_submodule_status(SubmoduleSafeHandle submodule } } + public static string git_submodule_resolve_url(RepositorySafeHandle repo, string relativeURL) + { + using (ThreadAffinity()) + { + using (GitBuf buffer = new GitBuf()) + { + int result = NativeMethods.git_submodule_resolve_url(buffer, repo, relativeURL); + Ensure.ZeroResult(result); + return LaxUtf8Marshaler.FromNative(buffer.ptr); + } + } + } + #endregion #region git_tag_ diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 5523fb6de..7fec4c264 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -578,6 +578,14 @@ public static string Clone(string sourceUrl, string workdirPath, repoPath = Proxy.git_repository_path(repo); } + if (options.Recursive && !options.IsBare) + { + using (Repository repo = new Repository(repoPath.ToString())) + { + repo.submodules.InitAndUpdateRecursively(options); + } + } + return repoPath.Native; } diff --git a/LibGit2Sharp/Submodule.cs b/LibGit2Sharp/Submodule.cs index 3d0b35629..217c2740b 100644 --- a/LibGit2Sharp/Submodule.cs +++ b/LibGit2Sharp/Submodule.cs @@ -2,6 +2,8 @@ using System.Diagnostics; using System.Globalization; using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; +using LibGit2Sharp.Handlers; namespace LibGit2Sharp { @@ -55,10 +57,20 @@ internal Submodule(Repository repo, string name, string path, string url) public virtual string Name { get { return name; } } /// - /// The path of the submodule. + /// The path of the submodule relative to the workdir of the parent repository. /// public virtual string Path { get { return path; } } + /// + /// Gets the absolute path to the working directory of the submodule. + /// + public virtual string WorkingDirectory + { + get { + return System.IO.Path.Combine(repo.Info.WorkingDirectory, Path); + } + } + /// /// The URL of the submodule. /// @@ -155,5 +167,47 @@ private string DebuggerDisplay "{0} => {1}", Name, Url); } } + + /// + /// Inits this submodule + /// + /// + internal void Init(CloneOptions options) + { + var remoteCallbacks = new RemoteCallbacks(null, options.OnTransferProgress, null, + options.Credentials); + GitRemoteCallbacks gitRemoteCallbacks = remoteCallbacks.GenerateCallbacks(); + + string gitdirPath = System.IO.Path.Combine(System.IO.Path.Combine(repo.Info.Path, "modules"), Path); + string remoteURL = Proxy.git_submodule_resolve_url(repo.Handle, Url); + string fetchRefspec = "+refs/heads/*:refs/remotes/origin/*"; + Signature signature = repo.Config.BuildSignature(DateTimeOffset.Now); + + GitCheckoutOpts opts = new GitCheckoutOpts() { + version = 1, + checkout_strategy = CheckoutStrategy.GIT_CHECKOUT_NONE + }; + + using (RepositorySafeHandle subrepo = Proxy.git_repository_init_ext( + WorkingDirectory, gitdirPath, GitRepositoryInitFlags.GIT_REPOSITORY_INIT_NO_DOTGIT_DIR)) + using (RemoteSafeHandle remote = Proxy.git_remote_create_anonymous(subrepo, remoteURL, fetchRefspec)) + { + Proxy.git_remote_set_callbacks(remote, ref gitRemoteCallbacks); + Proxy.git_clone_into(subrepo, remote, opts, null /* no branch */, signature); + } + } + + /// + /// Checks out the HEAD revision + /// + internal void Update(CloneOptions options) + { + using (Repository subrepo = new Repository(WorkingDirectory)) + { + subrepo.Checkout(HeadCommitId.Sha, CheckoutModifiers.None, options.OnCheckoutProgress, null); + // This is required because Checkout() does not actually checkout the files + subrepo.Reset(ResetMode.Hard); + } + } } } diff --git a/LibGit2Sharp/SubmoduleCollection.cs b/LibGit2Sharp/SubmoduleCollection.cs index 8713e3e4c..d53ef5ed8 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -6,6 +6,7 @@ using System.Linq; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; +using System.IO; namespace LibGit2Sharp { @@ -109,5 +110,21 @@ private string DebuggerDisplay "Count = {0}", this.Count()); } } + + /// + /// Initializes and updates all submodules, and their submodules recursively + /// + internal void InitAndUpdateRecursively(CloneOptions options) + { + foreach (Submodule module in this) + { + module.Init(options); + module.Update(options); + using (Repository subrepo = new Repository(module.WorkingDirectory)) + { + subrepo.Submodules.InitAndUpdateRecursively(options); + } + } + } } }