Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions LibGit2Sharp.Tests/CloneFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")));
}
}
}
}
1 change: 1 addition & 0 deletions LibGit2Sharp.Tests/Resources/submodules.git/HEAD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ref: refs/heads/master
7 changes: 7 additions & 0 deletions LibGit2Sharp.Tests/Resources/submodules.git/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true
hideDotFiles = dotGitOnly
1 change: 1 addition & 0 deletions LibGit2Sharp.Tests/Resources/submodules.git/description
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
6 changes: 6 additions & 0 deletions LibGit2Sharp.Tests/Resources/submodules.git/info/exclude
Original file line number Diff line number Diff line change
@@ -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]
# *~
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions LibGit2Sharp.Tests/Resources/submodules.git/packed-refs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# pack-refs with: peeled fully-peeled
9d4e659e2429c884eb50193880bd09613a8ddba5 refs/heads/master
Empty file.
2 changes: 2 additions & 0 deletions LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions LibGit2Sharp/CloneOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public CloneOptions()
/// </summary>
public bool Checkout { get; set; }

/// <summary>
/// True to init and update submodules recursively after checkout
/// </summary>
public bool Recursive { get; set; }

/// <summary>
/// Handler for network transfer and indexing progress information
/// </summary>
Expand Down
13 changes: 5 additions & 8 deletions LibGit2Sharp/Core/GitRepositoryInitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
14 changes: 14 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
48 changes: 46 additions & 2 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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_
Expand Down
8 changes: 8 additions & 0 deletions LibGit2Sharp/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
56 changes: 55 additions & 1 deletion LibGit2Sharp/Submodule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Diagnostics;
using System.Globalization;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;
using LibGit2Sharp.Handlers;

namespace LibGit2Sharp
{
Expand Down Expand Up @@ -55,10 +57,20 @@ internal Submodule(Repository repo, string name, string path, string url)
public virtual string Name { get { return name; } }

/// <summary>
/// The path of the submodule.
/// The path of the submodule relative to the workdir of the parent repository.
/// </summary>
public virtual string Path { get { return path; } }

/// <summary>
/// Gets the absolute path to the working directory of the submodule.
/// </summary>
public virtual string WorkingDirectory
{
get {
return System.IO.Path.Combine(repo.Info.WorkingDirectory, Path);
}
}

/// <summary>
/// The URL of the submodule.
/// </summary>
Expand Down Expand Up @@ -155,5 +167,47 @@ private string DebuggerDisplay
"{0} => {1}", Name, Url);
}
}

/// <summary>
/// Inits this submodule
/// </summary>
/// <param name="options"></param>
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to specify a commit sha instead of a branch name here? That would save the call to Update. If I specify the sha, it tries to check out /refs/heads/(unknown)/[sha] (or something alike).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for clone. If you wanted to avoid the checkout step, you could just do a fetch here, but there's a bunch of stuff that the clone api is doing that you'd have to do yourself.

}
}

/// <summary>
/// Checks out the HEAD revision
/// </summary>
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way than resetting? I think it does not check out the files immediately because the files are already indexed as missing. A solution to the comment above (passing head sha to git_clone_into) would fix this as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's odd. If Checkout isn't checking out, that's a bug. What's missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckoutStrategy.GIT_CHECKOUT_NONE in Init() does neither touch workdir nor index, so after that, all files in master are staged as deleted. Then subrepo.Checkout() is called, but in my case with master's tip as commit, so Checkout probably thinks that there is nothing to do. (However, if I do a git checkout [sha] manually with git core, it checks out the files.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, GIT_CHECKOUT_NONE is a dry run. This is probably a case where the default should be GIT_CHECKOUT_FORCE, but allow the caller to override it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand. GIT_CHECKOUT_FORCE will checkout master (or the specified branch), but possibly the target commit for the submodule contains completely different files, so the initial checkout would be a total waste of IO. Or did I miss something?

allow the caller to override it

Cloning recursively should be fully automated, just like git clone --recursive, the caller shouldn't have to override anything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, are we talking about the flag passed to the clone API, or the flag passed to the second-pass checkout? I'd assume that Submodule.Update (which is where this comment is) is where you'd actually want to force the checkout, instead of doing a default pass and a hard reset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, now I see. I think I tried passing CheckoutModifiers.Force to the Checkout method, but I haven't tried calling the native method directly with the flag.

}
}
}
}
17 changes: 17 additions & 0 deletions LibGit2Sharp/SubmoduleCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;
using System.IO;

namespace LibGit2Sharp
{
Expand Down Expand Up @@ -109,5 +110,21 @@ private string DebuggerDisplay
"Count = {0}", this.Count());
}
}

/// <summary>
/// Initializes and updates all submodules, and their submodules recursively
/// </summary>
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);
}
}
}
}
}