Skip to content

Commit b304c07

Browse files
jamillnulltoken
authored andcommitted
Teach clone the ability to recurse into submodules
Add property to CloneOptions to allow clone to recurse into submodules.
1 parent 2e3b534 commit b304c07

File tree

11 files changed

+556
-6
lines changed

11 files changed

+556
-6
lines changed

LibGit2Sharp.Tests/CloneFixture.cs

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Linq;
5+
using LibGit2Sharp.Handlers;
46
using LibGit2Sharp.Tests.TestHelpers;
57
using Xunit;
68
using Xunit.Extensions;
@@ -242,5 +244,243 @@ public void CloningWithoutUrlThrows()
242244

243245
Assert.Throws<ArgumentNullException>(() => Repository.Clone(null, scd.DirectoryPath));
244246
}
247+
248+
/// <summary>
249+
/// Private helper to record the callbacks that were called as part of a clone.
250+
/// </summary>
251+
private class CloneCallbackInfo
252+
{
253+
/// <summary>
254+
/// Was checkout progress called.
255+
/// </summary>
256+
public bool CheckoutProgressCalled { get; set; }
257+
258+
/// <summary>
259+
/// The reported remote URL.
260+
/// </summary>
261+
public string RemoteUrl { get; set; }
262+
263+
/// <summary>
264+
/// Was remote ref update called.
265+
/// </summary>
266+
public bool RemoteRefUpdateCalled { get; set; }
267+
268+
/// <summary>
269+
/// Was the transition callback called when starting
270+
/// work on this repository.
271+
/// </summary>
272+
public bool StartingWorkInRepositoryCalled { get; set; }
273+
274+
/// <summary>
275+
/// Was the transition callback called when finishing
276+
/// work on this repository.
277+
/// </summary>
278+
public bool FinishedWorkInRepositoryCalled { get; set; }
279+
280+
/// <summary>
281+
/// The reported recursion depth.
282+
/// </summary>
283+
public int RecursionDepth { get; set; }
284+
}
285+
286+
[Fact]
287+
public void CanRecursivelyCloneSubmodules()
288+
{
289+
var uri = new Uri(Path.GetFullPath(SandboxSubmoduleSmallTestRepo()));
290+
var scd = BuildSelfCleaningDirectory();
291+
string relativeSubmodulePath = "submodule_target_wd";
292+
293+
// Construct the expected URL the submodule will clone from.
294+
string expectedSubmoduleUrl = Path.Combine(Path.GetDirectoryName(uri.AbsolutePath), relativeSubmodulePath);
295+
expectedSubmoduleUrl = expectedSubmoduleUrl.Replace('\\', '/');
296+
297+
Dictionary<string, CloneCallbackInfo> callbacks = new Dictionary<string, CloneCallbackInfo>();
298+
299+
CloneCallbackInfo currentEntry = null;
300+
bool unexpectedOrderOfCallbacks = false;
301+
302+
CheckoutProgressHandler checkoutProgressHandler = (x, y, z) =>
303+
{
304+
if (currentEntry != null)
305+
{
306+
currentEntry.CheckoutProgressCalled = true;
307+
}
308+
else
309+
{
310+
// Should not be called if there is not a current
311+
// callbackInfo entry.
312+
unexpectedOrderOfCallbacks = true;
313+
}
314+
};
315+
316+
UpdateTipsHandler remoteRefUpdated = (x, y, z) =>
317+
{
318+
if (currentEntry != null)
319+
{
320+
currentEntry.RemoteRefUpdateCalled = true;
321+
}
322+
else
323+
{
324+
// Should not be called if there is not a current
325+
// callbackInfo entry.
326+
unexpectedOrderOfCallbacks = true;
327+
}
328+
329+
return true;
330+
};
331+
332+
RepositoryOperationStarting repositoryOperationStarting = (x) =>
333+
{
334+
if (currentEntry != null)
335+
{
336+
// Should not be called if there is a current
337+
// callbackInfo entry.
338+
unexpectedOrderOfCallbacks = true;
339+
}
340+
341+
currentEntry = new CloneCallbackInfo();
342+
currentEntry.StartingWorkInRepositoryCalled = true;
343+
currentEntry.RecursionDepth = x.RecursionDepth;
344+
currentEntry.RemoteUrl = x.RemoteUrl;
345+
callbacks.Add(x.RepositoryPath, currentEntry);
346+
347+
return true;
348+
};
349+
350+
RepositoryOperationCompleted repositoryOperationCompleted = (x) =>
351+
{
352+
if (currentEntry != null)
353+
{
354+
currentEntry.FinishedWorkInRepositoryCalled = true;
355+
currentEntry = null;
356+
}
357+
else
358+
{
359+
// Should not be called if there is not a current
360+
// callbackInfo entry.
361+
unexpectedOrderOfCallbacks = true;
362+
}
363+
};
364+
365+
CloneOptions options = new CloneOptions()
366+
{
367+
RecurseSubmodules = true,
368+
OnCheckoutProgress = checkoutProgressHandler,
369+
OnUpdateTips = remoteRefUpdated,
370+
RepositoryOperationStarting = repositoryOperationStarting,
371+
RepositoryOperationCompleted = repositoryOperationCompleted,
372+
};
373+
374+
string clonedRepoPath = Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options);
375+
string workDirPath;
376+
377+
using(Repository repo = new Repository(clonedRepoPath))
378+
{
379+
workDirPath = repo.Info.WorkingDirectory.TrimEnd(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
380+
}
381+
382+
// Verification:
383+
// Verify that no callbacks were called in an unexpected order.
384+
Assert.False(unexpectedOrderOfCallbacks);
385+
386+
Dictionary<string, CloneCallbackInfo> expectedCallbackInfo = new Dictionary<string, CloneCallbackInfo>();
387+
expectedCallbackInfo.Add(workDirPath, new CloneCallbackInfo()
388+
{
389+
RecursionDepth = 0,
390+
RemoteUrl = uri.AbsolutePath,
391+
StartingWorkInRepositoryCalled = true,
392+
FinishedWorkInRepositoryCalled = true,
393+
CheckoutProgressCalled = true,
394+
RemoteRefUpdateCalled = true,
395+
});
396+
397+
expectedCallbackInfo.Add(Path.Combine(workDirPath, relativeSubmodulePath), new CloneCallbackInfo()
398+
{
399+
RecursionDepth = 1,
400+
RemoteUrl = expectedSubmoduleUrl,
401+
StartingWorkInRepositoryCalled = true,
402+
FinishedWorkInRepositoryCalled = true,
403+
CheckoutProgressCalled = true,
404+
RemoteRefUpdateCalled = true,
405+
});
406+
407+
// Callbacks for each expected repository that is cloned
408+
foreach (KeyValuePair<string, CloneCallbackInfo> kvp in expectedCallbackInfo)
409+
{
410+
CloneCallbackInfo entry = null;
411+
Assert.True(callbacks.TryGetValue(kvp.Key, out entry), string.Format("{0} was not found in callbacks.", kvp.Key));
412+
413+
Assert.Equal(kvp.Value.RemoteUrl, entry.RemoteUrl);
414+
Assert.Equal(kvp.Value.RecursionDepth, entry.RecursionDepth);
415+
Assert.Equal(kvp.Value.StartingWorkInRepositoryCalled, entry.StartingWorkInRepositoryCalled);
416+
Assert.Equal(kvp.Value.FinishedWorkInRepositoryCalled, entry.FinishedWorkInRepositoryCalled);
417+
Assert.Equal(kvp.Value.CheckoutProgressCalled, entry.CheckoutProgressCalled);
418+
Assert.Equal(kvp.Value.RemoteRefUpdateCalled, entry.RemoteRefUpdateCalled);
419+
}
420+
421+
// Verify the state of the submodule
422+
using(Repository repo = new Repository(clonedRepoPath))
423+
{
424+
var sm = repo.Submodules[relativeSubmodulePath];
425+
Assert.True(sm.RetrieveStatus().HasFlag(SubmoduleStatus.InWorkDir |
426+
SubmoduleStatus.InConfig |
427+
SubmoduleStatus.InIndex |
428+
SubmoduleStatus.InHead));
429+
430+
Assert.NotNull(sm.HeadCommitId);
431+
Assert.Equal("480095882d281ed676fe5b863569520e54a7d5c0", sm.HeadCommitId.Sha);
432+
433+
Assert.False(repo.RetrieveStatus().IsDirty);
434+
}
435+
}
436+
437+
[Fact]
438+
public void CanCancelRecursiveClone()
439+
{
440+
var uri = new Uri(Path.GetFullPath(SandboxSubmoduleSmallTestRepo()));
441+
var scd = BuildSelfCleaningDirectory();
442+
string relativeSubmodulePath = "submodule_target_wd";
443+
444+
int cancelDepth = 0;
445+
446+
RepositoryOperationStarting repositoryOperationStarting = (x) =>
447+
{
448+
return !(x.RecursionDepth >= cancelDepth);
449+
};
450+
451+
CloneOptions options = new CloneOptions()
452+
{
453+
RecurseSubmodules = true,
454+
RepositoryOperationStarting = repositoryOperationStarting,
455+
};
456+
457+
Assert.Throws<UserCancelledException>(() =>
458+
Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options));
459+
460+
// Cancel after super repository is cloned, but before submodule is cloned.
461+
cancelDepth = 1;
462+
463+
string clonedRepoPath = null;
464+
465+
try
466+
{
467+
Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options);
468+
}
469+
catch(RecurseSubmodulesException ex)
470+
{
471+
Assert.NotNull(ex.InnerException);
472+
Assert.Equal(typeof(UserCancelledException), ex.InnerException.GetType());
473+
clonedRepoPath = ex.InitialRepositoryPath;
474+
}
475+
476+
// Verify that the submodule was not initialized.
477+
using(Repository repo = new Repository(clonedRepoPath))
478+
{
479+
var submoduleStatus = repo.Submodules[relativeSubmodulePath].RetrieveStatus();
480+
Assert.Equal(SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.WorkDirUninitialized,
481+
submoduleStatus);
482+
483+
}
484+
}
245485
}
246486
}

LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ static BaseFixture()
4040
public static string SubmoduleTestRepoWorkingDirPath { get; private set; }
4141
private static string SubmoduleTargetTestRepoWorkingDirPath { get; set; }
4242
private static string AssumeUnchangedRepoWorkingDirPath { get; set; }
43-
private static string SubmoduleSmallTestRepoWorkingDirPath { get; set; }
43+
public static string SubmoduleSmallTestRepoWorkingDirPath { get; set; }
4444

4545
public static DirectoryInfo ResourcesDirectory { get; private set; }
4646

@@ -71,7 +71,7 @@ private static void SetUpTestEnvironment()
7171
SubmoduleTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_wd");
7272
SubmoduleTargetTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_target_wd");
7373
AssumeUnchangedRepoWorkingDirPath = Path.Combine(sourceRelativePath, "assume_unchanged_wd");
74-
SubmoduleSmallTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "submodule_small_wd");
74+
SubmoduleSmallTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_small_wd");
7575
}
7676

7777
private static bool IsFileSystemCaseSensitiveInternal()
@@ -159,8 +159,7 @@ public string SandboxAssumeUnchangedTestRepo()
159159

160160
public string SandboxSubmoduleSmallTestRepo()
161161
{
162-
var submoduleTarget = Path.Combine(ResourcesDirectory.FullName, "submodule_target_wd");
163-
var path = Sandbox(SubmoduleSmallTestRepoWorkingDirPath, submoduleTarget);
162+
var path = Sandbox(SubmoduleSmallTestRepoWorkingDirPath, SubmoduleTargetTestRepoWorkingDirPath);
164163
Directory.CreateDirectory(Path.Combine(path, "submodule_target_wd"));
165164

166165
return path;

LibGit2Sharp/CloneOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public CloneOptions()
3333
/// </summary>
3434
public string BranchName { get; set; }
3535

36+
/// <summary>
37+
/// Recursively clone submodules.
38+
/// </summary>
39+
public bool RecurseSubmodules { get; set; }
40+
3641
/// <summary>
3742
/// Handler for checkout progress information.
3843
/// </summary>

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,12 @@ internal static extern int git_submodule_lookup(
13611361
RepositorySafeHandle repo,
13621362
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath name);
13631363

1364+
[DllImport(libgit2)]
1365+
internal static extern int git_submodule_resolve_url(
1366+
GitBuf buf,
1367+
RepositorySafeHandle repo,
1368+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url);
1369+
13641370
[DllImport(libgit2)]
13651371
internal static extern int git_submodule_update(
13661372
SubmoduleSafeHandle sm,

LibGit2Sharp/Core/Proxy.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2811,6 +2811,18 @@ public static SubmoduleSafeHandle git_submodule_lookup(RepositorySafeHandle repo
28112811
}
28122812
}
28132813

2814+
public static string git_submodule_resolve_url(RepositorySafeHandle repo, string url)
2815+
{
2816+
using (ThreadAffinity())
2817+
using (var buf = new GitBuf())
2818+
{
2819+
int res = NativeMethods.git_submodule_resolve_url(buf, repo, url);
2820+
2821+
Ensure.ZeroResult(res);
2822+
return LaxUtf8Marshaler.FromNative(buf.ptr);
2823+
}
2824+
}
2825+
28142826
public static ICollection<TResult> git_submodule_foreach<TResult>(RepositorySafeHandle repo, Func<IntPtr, IntPtr, TResult> resultSelector)
28152827
{
28162828
return git_foreach(resultSelector, c => NativeMethods.git_submodule_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero));

LibGit2Sharp/FetchOptionsBase.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,15 @@ internal FetchOptionsBase()
3333
/// Handler to generate <see cref="LibGit2Sharp.Credentials"/> for authentication.
3434
/// </summary>
3535
public CredentialsHandler CredentialsProvider { get; set; }
36+
37+
/// <summary>
38+
/// Starting to operate on a new repository.
39+
/// </summary>
40+
public RepositoryOperationStarting RepositoryOperationStarting { get; set; }
41+
42+
/// <summary>
43+
/// Completed operating on the current repository.
44+
/// </summary>
45+
public RepositoryOperationCompleted RepositoryOperationCompleted { get; set; }
3646
}
3747
}

LibGit2Sharp/Handlers.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
namespace LibGit2Sharp.Handlers
1+
using System;
2+
namespace LibGit2Sharp.Handlers
23
{
34
/// <summary>
45
/// Delegate definition to handle Progress callback.
@@ -37,6 +38,21 @@
3738
/// <returns>True to continue, false to cancel.</returns>
3839
public delegate bool TransferProgressHandler(TransferProgress progress);
3940

41+
/// <summary>
42+
/// Delegate definition to indicate that a repository is about to be operated on.
43+
/// (In the context of a recursive operation).
44+
/// </summary>
45+
/// <param name="context">Context on the repository that is being operated on.</param>
46+
/// <returns>true to continue, false to cancel.</returns>
47+
public delegate bool RepositoryOperationStarting(RepositoryOperationContext context);
48+
49+
/// <summary>
50+
/// Delegate definition to indicate that an operation is done in a repository.
51+
/// (In the context of a recursive operation).
52+
/// </summary>
53+
/// <param name="context">Context on the repository that is being operated on.</param>
54+
public delegate void RepositoryOperationCompleted(RepositoryOperationContext context);
55+
4056
/// <summary>
4157
/// Delegate definition for callback reporting push network progress.
4258
/// </summary>

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
<Compile Include="PatchStats.cs" />
108108
<Compile Include="PeelException.cs" />
109109
<Compile Include="PullOptions.cs" />
110+
<Compile Include="RecurseSubmodulesException.cs" />
110111
<Compile Include="RefSpec.cs" />
111112
<Compile Include="RefSpecCollection.cs" />
112113
<Compile Include="Core\EncodingMarshaler.cs" />
@@ -123,6 +124,7 @@
123124
<Compile Include="Core\GitBuf.cs" />
124125
<Compile Include="FilteringOptions.cs" />
125126
<Compile Include="MergeFetchHeadNotFoundException.cs" />
127+
<Compile Include="RepositoryOperationContext.cs" />
126128
<Compile Include="ResetMode.cs" />
127129
<Compile Include="NoteCollectionExtensions.cs" />
128130
<Compile Include="RefSpecDirection.cs" />

0 commit comments

Comments
 (0)