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);
+ }
+ }
+ }
}
}