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
123 changes: 123 additions & 0 deletions LibGit2Sharp.Tests/SubmoduleFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,129 @@ public void CanInitSubmodule()
}
}

[Fact]
public void CanAddSubmodule()
{
string configPath = CreateConfigurationWithDummyUser(Constants.Identity);
var options = new RepositoryOptions { GlobalConfigurationLocation = configPath };

//var path = SandboxSubmoduleTestRepo();
var path = SandboxStandardTestRepo();
var pathSubRepoOrigin = SandboxStandardTestRepo();

string submoduleSubPath = "submodule_target_wd";
string expectedSubmodulePath = Path.GetFullPath(Path.Combine(path, submoduleSubPath));
string expectedSubmoduleUrl = pathSubRepoOrigin.Replace('\\', '/');
ObjectId expectedCommitId = (ObjectId)"32eab9cb1f450b5fe7ab663462b77d7f4b703344";

using (var repo = new Repository(path, options))
{
// check on adding config entry
var configEntryBeforeAdd = repo.Config.Get<string>(string.Format("submodule.{0}.url", submoduleSubPath));
Assert.Null(configEntryBeforeAdd);

// add submodule
Submodule submodule = repo.Submodules.Add(pathSubRepoOrigin, submoduleSubPath);
Assert.NotNull(submodule);

// check that the expected commit is checked out, but not set in parent repo until committed
Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].WorkDirCommitId);
Assert.Null(repo.Submodules[submoduleSubPath].HeadCommitId);

// check status
var submoduleStatus = submodule.RetrieveStatus();
Assert.True((submoduleStatus & SubmoduleStatus.InIndex) == SubmoduleStatus.InIndex);
Assert.True((submoduleStatus & SubmoduleStatus.InConfig) == SubmoduleStatus.InConfig);
Assert.True((submoduleStatus & SubmoduleStatus.InWorkDir) == SubmoduleStatus.InWorkDir);
Assert.True((submoduleStatus & SubmoduleStatus.IndexAdded) == SubmoduleStatus.IndexAdded);

// check that config entry was added with the correct url
var configEntryAfterAdd = repo.Config.Get<string>(string.Format("submodule.{0}.url", submoduleSubPath));
Assert.NotNull(configEntryAfterAdd);
Assert.Equal(expectedSubmoduleUrl, configEntryAfterAdd.Value);

// check on directory being added and repository directory
Assert.True(Directory.Exists(expectedSubmodulePath));
Assert.True(Directory.Exists(Path.Combine(expectedSubmodulePath, ".git")));

// manually check commit by opening submodule as a repository
using (var repo2 = new Repository(expectedSubmodulePath))
{
Assert.False(repo2.Info.IsHeadDetached);
Assert.False(repo2.Info.IsHeadUnborn);
Commit headCommit = repo2.Head.Tip;
Assert.Equal(headCommit.Id, expectedCommitId);
}

// commit parent repository, then verify it reports the correct CommitId for the submodule
Signature signature = repo.Config.BuildSignature(DateTimeOffset.Now);
repo.Commit("Added submodule " + submoduleSubPath, signature, signature);
Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].HeadCommitId);
}
}


[Fact]
public void CanAddSubmoduleWithManualClone()
{
string configPath = CreateConfigurationWithDummyUser(Constants.Identity);
var options = new RepositoryOptions { GlobalConfigurationLocation = configPath };

//var path = SandboxSubmoduleTestRepo();
var path = SandboxStandardTestRepo();
var pathSubRepoOrigin = SandboxStandardTestRepo();

string submoduleSubPath = "submodule_target_wd";
string expectedSubmodulePath = Path.GetFullPath(Path.Combine(path, submoduleSubPath));
string expectedSubmoduleUrl = pathSubRepoOrigin.Replace('\\', '/');
ObjectId expectedCommitId = (ObjectId)"32eab9cb1f450b5fe7ab663462b77d7f4b703344";

using (var repo = new Repository(path, options))
{
// check on adding config entry
var configEntryBeforeAdd = repo.Config.Get<string>(string.Format("submodule.{0}.url", submoduleSubPath));
Assert.Null(configEntryBeforeAdd);

// add submodule
Submodule submodule = repo.Submodules.Add(pathSubRepoOrigin, submoduleSubPath, x => Repository.Clone(pathSubRepoOrigin, x, new CloneOptions() { } ));
Assert.NotNull(submodule);

// check that the expected commit is checked out, but not set in parent repo until committed
Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].WorkDirCommitId);
Assert.Null(repo.Submodules[submoduleSubPath].HeadCommitId);

// check status
var submoduleStatus = submodule.RetrieveStatus();
Assert.True((submoduleStatus & SubmoduleStatus.InIndex) == SubmoduleStatus.InIndex);
Assert.True((submoduleStatus & SubmoduleStatus.InConfig) == SubmoduleStatus.InConfig);
Assert.True((submoduleStatus & SubmoduleStatus.InWorkDir) == SubmoduleStatus.InWorkDir);
Assert.True((submoduleStatus & SubmoduleStatus.IndexAdded) == SubmoduleStatus.IndexAdded);

// check that config entry was added with the correct url
var configEntryAfterAdd = repo.Config.Get<string>(string.Format("submodule.{0}.url", submoduleSubPath));
Assert.NotNull(configEntryAfterAdd);
Assert.Equal(expectedSubmoduleUrl, configEntryAfterAdd.Value);

// check on directory being added and repository directory
Assert.True(Directory.Exists(expectedSubmodulePath));
Assert.True(Directory.Exists(Path.Combine(expectedSubmodulePath, ".git")));

// manually check commit by opening submodule as a repository
using (var repo2 = new Repository(expectedSubmodulePath))
{
Assert.False(repo2.Info.IsHeadDetached);
Assert.False(repo2.Info.IsHeadUnborn);
Commit headCommit = repo2.Head.Tip;
Assert.Equal(headCommit.Id, expectedCommitId);
}

// commit parent repository, then verify it reports the correct CommitId for the submodule
Signature signature = repo.Config.BuildSignature(DateTimeOffset.Now);
repo.Commit("Added submodule " + submoduleSubPath, signature, signature);
Assert.Equal(expectedCommitId, repo.Submodules[submoduleSubPath].HeadCommitId);
}
}

[Fact]
public void UpdatingUninitializedSubmoduleThrows()
{
Expand Down
12 changes: 12 additions & 0 deletions LibGit2Sharp/Core/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,18 @@ internal static extern void git_status_list_free(
internal static extern void git_strarray_free(
ref GitStrArray array);

[DllImport(libgit2)]
internal static extern int git_submodule_add_setup(
out SubmoduleSafeHandle reference,
RepositorySafeHandle repo,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url,
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path,
bool use_gitlink);

[DllImport(libgit2)]
internal static extern int git_submodule_add_finalize(
SubmoduleSafeHandle submodule);

[DllImport(libgit2)]
internal static extern int git_submodule_lookup(
out SubmoduleSafeHandle reference,
Expand Down
15 changes: 15 additions & 0 deletions LibGit2Sharp/Core/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,21 @@ public static ICollection<TResult> git_submodule_foreach<TResult>(RepositorySafe
return git_foreach(resultSelector, c => NativeMethods.git_submodule_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero));
}

public static SubmoduleSafeHandle git_submodule_add_setup(RepositorySafeHandle repo, string url, FilePath path, bool useGitLink)
{
SubmoduleSafeHandle sub;
var res = NativeMethods.git_submodule_add_setup(out sub, repo, url, path, useGitLink);
Ensure.ZeroResult(res);
return sub;
}

public static void git_submodule_add_finalize(SubmoduleSafeHandle submodule)
{
// This should be called on a submodule once you have called add setup and done the clone of the submodule. This adds the .gitmodules file and the newly cloned submodule to the index to be ready to be committed (but doesn't actually do the commit).
var res = NativeMethods.git_submodule_add_finalize(submodule);
Ensure.ZeroResult(res);
}

public static void git_submodule_add_to_index(SubmoduleSafeHandle submodule, bool write_index)
{
var res = NativeMethods.git_submodule_add_to_index(submodule, write_index);
Expand Down
69 changes: 69 additions & 0 deletions LibGit2Sharp/SubmoduleCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,75 @@ public virtual Submodule this[string name]
}
}

/// <summary>
/// Adds a new submodule, calling the passed action to allow the caller
/// to clone the submodule into the passed path. The submodule ends
/// up being staged along with the .gitmodules just like the command
/// line 'git submodule add'
/// </summary>
/// <param name="url">The url of the remote repository</param>
/// <param name="relativePath">The path of the submodule inside of the parent repository, which will also become the submodule name.</param>
/// <param name="cloneMethod">A method that takes the full path to where we expect the repository to be cloned to so the caller
/// can clone it themselves. If not specified or if null, Add() will perform the clone using Repository.Clone(url, subPath, new CloneOptions() { } ).</param>
/// <returns>The new Submodule</returns>
public virtual Submodule Add(string url, string relativePath, Action<string> cloneMethod)
{
return MainAddSubmodule(url, relativePath, cloneMethod);
}

/// <summary>
/// Adds a new submodule and clones it using the passed url. The
/// url must be supported for cloning by LibGit2Sharp.
/// </summary>
/// <param name="url">The url of the remote repository</param>
/// <param name="relativePath">The path of the submodule inside of the parent repository, which will also become the submodule name.</param>
/// <returns>The new Submodule</returns>
public virtual Submodule Add(string url, string relativePath)
{
return MainAddSubmodule(url, relativePath);
}

/// <summary>
/// Method that actually adds a submodule, called by overloaded Add()
/// methods.
/// </summary>
/// <param name="url">The url of the remote repository</param>
/// <param name="relativePath">The path of the submodule inside of the parent repository, which will also become the submodule name.</param>
/// <param name="cloneMethod">A method that takes the full path to where we expect the repository to be cloned to so the caller
/// can clone it themselves. If not specified or if null, Add() will perform the clone using Repository.Clone(url, subPath, new CloneOptions() { } ).</param>
/// <returns>The new Submodule</returns>
Submodule MainAddSubmodule(string url, string relativePath, Action<string> cloneMethod = null)
{
Ensure.ArgumentNotNullOrEmptyString(relativePath, "relativePath");

Ensure.ArgumentNotNullOrEmptyString(url, "url");

using (SubmoduleSafeHandle handle = Proxy.git_submodule_add_setup(repo.Handle, url, relativePath, false))
{
// get the full path to the submodule directory
string subPath = System.IO.Path.Combine(repo.Info.WorkingDirectory, relativePath);

// the directory is created now and has a .git folder with no commits and I could fetch from
// the remote and checkout some branch, but I want to do a full clone to create all the tracking
// branches and checkout whatever the remote HEAD is, which seems hard to find.
System.IO.Directory.Delete(subPath, true);

// now clone the repository, or let the caller do it if an action was specified
if (cloneMethod != null)
{
cloneMethod(subPath);
}
else
{
string result = Repository.Clone(url, subPath, new CloneOptions() { });
}

Proxy.git_submodule_add_finalize(handle);
}

return this[relativePath];
}

/// <summary>
/// Initialize specified submodule.
/// <para>
Expand Down