Skip to content
Merged
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
240 changes: 240 additions & 0 deletions LibGit2Sharp.Tests/CloneFixture.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibGit2Sharp.Handlers;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
using Xunit.Extensions;
Expand Down Expand Up @@ -242,5 +244,243 @@ public void CloningWithoutUrlThrows()

Assert.Throws<ArgumentNullException>(() => Repository.Clone(null, scd.DirectoryPath));
}

/// <summary>
/// Private helper to record the callbacks that were called as part of a clone.
/// </summary>
private class CloneCallbackInfo
{
/// <summary>
/// Was checkout progress called.
/// </summary>
public bool CheckoutProgressCalled { get; set; }

/// <summary>
/// The reported remote URL.
/// </summary>
public string RemoteUrl { get; set; }

/// <summary>
/// Was remote ref update called.
/// </summary>
public bool RemoteRefUpdateCalled { get; set; }

/// <summary>
/// Was the transition callback called when starting
/// work on this repository.
/// </summary>
public bool StartingWorkInRepositoryCalled { get; set; }

/// <summary>
/// Was the transition callback called when finishing
/// work on this repository.
/// </summary>
public bool FinishedWorkInRepositoryCalled { get; set; }

/// <summary>
/// The reported recursion depth.
/// </summary>
public int RecursionDepth { get; set; }
}

[Fact]
public void CanRecursivelyCloneSubmodules()
{
var uri = new Uri(Path.GetFullPath(SandboxSubmoduleSmallTestRepo()));
var scd = BuildSelfCleaningDirectory();
string relativeSubmodulePath = "submodule_target_wd";

// Construct the expected URL the submodule will clone from.
string expectedSubmoduleUrl = Path.Combine(Path.GetDirectoryName(uri.AbsolutePath), relativeSubmodulePath);
expectedSubmoduleUrl = expectedSubmoduleUrl.Replace('\\', '/');

Dictionary<string, CloneCallbackInfo> callbacks = new Dictionary<string, CloneCallbackInfo>();

CloneCallbackInfo currentEntry = null;
bool unexpectedOrderOfCallbacks = false;

CheckoutProgressHandler checkoutProgressHandler = (x, y, z) =>
{
if (currentEntry != null)
{
currentEntry.CheckoutProgressCalled = true;
}
else
{
// Should not be called if there is not a current
// callbackInfo entry.
unexpectedOrderOfCallbacks = true;
}
};

UpdateTipsHandler remoteRefUpdated = (x, y, z) =>
{
if (currentEntry != null)
{
currentEntry.RemoteRefUpdateCalled = true;
}
else
{
// Should not be called if there is not a current
// callbackInfo entry.
unexpectedOrderOfCallbacks = true;
}

return true;
};

RepositoryOperationStarting repositoryOperationStarting = (x) =>
{
if (currentEntry != null)
{
// Should not be called if there is a current
// callbackInfo entry.
unexpectedOrderOfCallbacks = true;
}

currentEntry = new CloneCallbackInfo();
currentEntry.StartingWorkInRepositoryCalled = true;
currentEntry.RecursionDepth = x.RecursionDepth;
currentEntry.RemoteUrl = x.RemoteUrl;
callbacks.Add(x.RepositoryPath, currentEntry);

return true;
};

RepositoryOperationCompleted repositoryOperationCompleted = (x) =>
{
if (currentEntry != null)
{
currentEntry.FinishedWorkInRepositoryCalled = true;
currentEntry = null;
}
else
{
// Should not be called if there is not a current
// callbackInfo entry.
unexpectedOrderOfCallbacks = true;
}
};

CloneOptions options = new CloneOptions()
{
RecurseSubmodules = true,
OnCheckoutProgress = checkoutProgressHandler,
OnUpdateTips = remoteRefUpdated,
RepositoryOperationStarting = repositoryOperationStarting,
RepositoryOperationCompleted = repositoryOperationCompleted,
};

string clonedRepoPath = Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options);
string workDirPath;

using(Repository repo = new Repository(clonedRepoPath))
{
workDirPath = repo.Info.WorkingDirectory.TrimEnd(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
}

// Verification:
// Verify that no callbacks were called in an unexpected order.
Assert.False(unexpectedOrderOfCallbacks);

Dictionary<string, CloneCallbackInfo> expectedCallbackInfo = new Dictionary<string, CloneCallbackInfo>();
expectedCallbackInfo.Add(workDirPath, new CloneCallbackInfo()
{
RecursionDepth = 0,
RemoteUrl = uri.AbsolutePath,
StartingWorkInRepositoryCalled = true,
FinishedWorkInRepositoryCalled = true,
CheckoutProgressCalled = true,
RemoteRefUpdateCalled = true,
});

expectedCallbackInfo.Add(Path.Combine(workDirPath, relativeSubmodulePath), new CloneCallbackInfo()
{
RecursionDepth = 1,
RemoteUrl = expectedSubmoduleUrl,
StartingWorkInRepositoryCalled = true,
FinishedWorkInRepositoryCalled = true,
CheckoutProgressCalled = true,
RemoteRefUpdateCalled = true,
});

// Callbacks for each expected repository that is cloned
foreach (KeyValuePair<string, CloneCallbackInfo> kvp in expectedCallbackInfo)
{
CloneCallbackInfo entry = null;
Assert.True(callbacks.TryGetValue(kvp.Key, out entry), string.Format("{0} was not found in callbacks.", kvp.Key));

Assert.Equal(kvp.Value.RemoteUrl, entry.RemoteUrl);
Assert.Equal(kvp.Value.RecursionDepth, entry.RecursionDepth);
Assert.Equal(kvp.Value.StartingWorkInRepositoryCalled, entry.StartingWorkInRepositoryCalled);
Assert.Equal(kvp.Value.FinishedWorkInRepositoryCalled, entry.FinishedWorkInRepositoryCalled);
Assert.Equal(kvp.Value.CheckoutProgressCalled, entry.CheckoutProgressCalled);
Assert.Equal(kvp.Value.RemoteRefUpdateCalled, entry.RemoteRefUpdateCalled);
}

// Verify the state of the submodule
using(Repository repo = new Repository(clonedRepoPath))
{
var sm = repo.Submodules[relativeSubmodulePath];
Assert.True(sm.RetrieveStatus().HasFlag(SubmoduleStatus.InWorkDir |
SubmoduleStatus.InConfig |
SubmoduleStatus.InIndex |
SubmoduleStatus.InHead));

Assert.NotNull(sm.HeadCommitId);
Assert.Equal("480095882d281ed676fe5b863569520e54a7d5c0", sm.HeadCommitId.Sha);

Assert.False(repo.RetrieveStatus().IsDirty);
}
}

[Fact]
public void CanCancelRecursiveClone()
{
var uri = new Uri(Path.GetFullPath(SandboxSubmoduleSmallTestRepo()));
var scd = BuildSelfCleaningDirectory();
string relativeSubmodulePath = "submodule_target_wd";

int cancelDepth = 0;

RepositoryOperationStarting repositoryOperationStarting = (x) =>
{
return !(x.RecursionDepth >= cancelDepth);
};

CloneOptions options = new CloneOptions()
{
RecurseSubmodules = true,
RepositoryOperationStarting = repositoryOperationStarting,
};

Assert.Throws<UserCancelledException>(() =>
Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options));

// Cancel after super repository is cloned, but before submodule is cloned.
cancelDepth = 1;

string clonedRepoPath = null;

try
{
Repository.Clone(uri.AbsolutePath, scd.DirectoryPath, options);
}
catch(RecurseSubmodulesException ex)
{
Assert.NotNull(ex.InnerException);
Assert.Equal(typeof(UserCancelledException), ex.InnerException.GetType());
clonedRepoPath = ex.InitialRepositoryPath;
}

// Verify that the submodule was not initialized.
using(Repository repo = new Repository(clonedRepoPath))
{
var submoduleStatus = repo.Submodules[relativeSubmodulePath].RetrieveStatus();
Assert.Equal(SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.WorkDirUninitialized,
submoduleStatus);

}
}
}
}
7 changes: 3 additions & 4 deletions LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ static BaseFixture()
public static string SubmoduleTestRepoWorkingDirPath { get; private set; }
private static string SubmoduleTargetTestRepoWorkingDirPath { get; set; }
private static string AssumeUnchangedRepoWorkingDirPath { get; set; }
private static string SubmoduleSmallTestRepoWorkingDirPath { get; set; }
public static string SubmoduleSmallTestRepoWorkingDirPath { get; set; }

public static DirectoryInfo ResourcesDirectory { get; private set; }

Expand Down Expand Up @@ -71,7 +71,7 @@ private static void SetUpTestEnvironment()
SubmoduleTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_wd");
SubmoduleTargetTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_target_wd");
AssumeUnchangedRepoWorkingDirPath = Path.Combine(sourceRelativePath, "assume_unchanged_wd");
SubmoduleSmallTestRepoWorkingDirPath = Path.Combine(ResourcesDirectory.FullName, "submodule_small_wd");
SubmoduleSmallTestRepoWorkingDirPath = Path.Combine(sourceRelativePath, "submodule_small_wd");
}

private static bool IsFileSystemCaseSensitiveInternal()
Expand Down Expand Up @@ -159,8 +159,7 @@ public string SandboxAssumeUnchangedTestRepo()

public string SandboxSubmoduleSmallTestRepo()
{
var submoduleTarget = Path.Combine(ResourcesDirectory.FullName, "submodule_target_wd");
var path = Sandbox(SubmoduleSmallTestRepoWorkingDirPath, submoduleTarget);
var path = Sandbox(SubmoduleSmallTestRepoWorkingDirPath, SubmoduleTargetTestRepoWorkingDirPath);
Directory.CreateDirectory(Path.Combine(path, "submodule_target_wd"));

return path;
Expand Down
5 changes: 5 additions & 0 deletions LibGit2Sharp/CloneOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public CloneOptions()
/// </summary>
public string BranchName { get; set; }

/// <summary>
/// Recursively clone submodules.
/// </summary>
public bool RecurseSubmodules { get; set; }

/// <summary>
/// Handler for checkout progress information.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,12 @@ internal static extern int git_submodule_lookup(
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath name);

[DllImport(libgit2)]
internal static extern int git_submodule_resolve_url(
GitBuf buf,
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url);

[DllImport(libgit2)]
internal static extern int git_submodule_update(
SubmoduleSafeHandle sm,
Expand Down
12 changes: 12 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2811,6 +2811,18 @@ public static SubmoduleSafeHandle git_submodule_lookup(RepositorySafeHandle repo
}
}

public static string git_submodule_resolve_url(RepositorySafeHandle repo, string url)
{
using (ThreadAffinity())
using (var buf = new GitBuf())
{
int res = NativeMethods.git_submodule_resolve_url(buf, repo, url);

Ensure.ZeroResult(res);
return LaxUtf8Marshaler.FromNative(buf.ptr);
}
}

public static ICollection<TResult> git_submodule_foreach<TResult>(RepositorySafeHandle repo, Func<IntPtr, IntPtr, TResult> resultSelector)
{
return git_foreach(resultSelector, c => NativeMethods.git_submodule_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero));
Expand Down
10 changes: 10 additions & 0 deletions LibGit2Sharp/FetchOptionsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,15 @@ internal FetchOptionsBase()
/// Handler to generate <see cref="LibGit2Sharp.Credentials"/> for authentication.
/// </summary>
public CredentialsHandler CredentialsProvider { get; set; }

/// <summary>
/// Starting to operate on a new repository.
/// </summary>
public RepositoryOperationStarting RepositoryOperationStarting { get; set; }

/// <summary>
/// Completed operating on the current repository.
/// </summary>
public RepositoryOperationCompleted RepositoryOperationCompleted { get; set; }
}
}
18 changes: 17 additions & 1 deletion LibGit2Sharp/Handlers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace LibGit2Sharp.Handlers
using System;
namespace LibGit2Sharp.Handlers
{
/// <summary>
/// Delegate definition to handle Progress callback.
Expand Down Expand Up @@ -37,6 +38,21 @@
/// <returns>True to continue, false to cancel.</returns>
public delegate bool TransferProgressHandler(TransferProgress progress);

/// <summary>
/// Delegate definition to indicate that a repository is about to be operated on.
/// (In the context of a recursive operation).
/// </summary>
/// <param name="context">Context on the repository that is being operated on.</param>
/// <returns>true to continue, false to cancel.</returns>
public delegate bool RepositoryOperationStarting(RepositoryOperationContext context);

/// <summary>
/// Delegate definition to indicate that an operation is done in a repository.
/// (In the context of a recursive operation).
/// </summary>
/// <param name="context">Context on the repository that is being operated on.</param>
public delegate void RepositoryOperationCompleted(RepositoryOperationContext context);

/// <summary>
/// Delegate definition for callback reporting push network progress.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions LibGit2Sharp/LibGit2Sharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
<Compile Include="PatchStats.cs" />
<Compile Include="PeelException.cs" />
<Compile Include="PullOptions.cs" />
<Compile Include="RecurseSubmodulesException.cs" />
<Compile Include="RefSpec.cs" />
<Compile Include="RefSpecCollection.cs" />
<Compile Include="Core\EncodingMarshaler.cs" />
Expand All @@ -123,6 +124,7 @@
<Compile Include="Core\GitBuf.cs" />
<Compile Include="FilteringOptions.cs" />
<Compile Include="MergeFetchHeadNotFoundException.cs" />
<Compile Include="RepositoryOperationContext.cs" />
<Compile Include="ResetMode.cs" />
<Compile Include="NoteCollectionExtensions.cs" />
<Compile Include="RefSpecDirection.cs" />
Expand Down
Loading