Thanks to visit codestin.com
Credit goes to github.com

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)