Skip to content

Commit ac4a5f6

Browse files
committed
Add Repository.Submodules
Includes submodule support in Index.Stage() to fix #220
1 parent e320904 commit ac4a5f6

File tree

9 files changed

+393
-1
lines changed

9 files changed

+393
-1
lines changed

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
<Compile Include="StashFixture.cs" />
6565
<Compile Include="CloneFixture.cs" />
6666
<Compile Include="ConflictFixture.cs" />
67+
<Compile Include="SubmoduleFixture.cs" />
6768
<Compile Include="IgnoreFixture.cs" />
6869
<Compile Include="FetchHeadFixture.cs" />
6970
<Compile Include="MergeFixture.cs" />
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using System.Linq;
3+
using LibGit2Sharp.Tests.TestHelpers;
4+
using Xunit;
5+
using Xunit.Extensions;
6+
7+
namespace LibGit2Sharp.Tests
8+
{
9+
public class SubmoduleFixture : BaseFixture
10+
{
11+
[Fact]
12+
public void RetrievingSubmoduleForNormalDirectoryReturnsNull()
13+
{
14+
var path = CloneSubmoduleTestRepo();
15+
using (var repo = new Repository(path))
16+
{
17+
var submodule = repo.Submodules["just_a_dir"];
18+
Assert.Null(submodule);
19+
}
20+
}
21+
22+
[Theory]
23+
[InlineData("sm_added_and_uncommited", SubmoduleStatus.InConfig | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.IndexAdded)]
24+
[InlineData("sm_changed_file", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.WorkDirFilesModified)]
25+
[InlineData("sm_changed_head", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.WorkDirModified)]
26+
[InlineData("sm_changed_index", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.WorkDirFilesIndexDirty)]
27+
[InlineData("sm_changed_untracked_file", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.WorkDirFilesUntracked)]
28+
[InlineData("sm_gitmodules_only", SubmoduleStatus.InConfig)]
29+
[InlineData("sm_missing_commits", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir | SubmoduleStatus.WorkDirModified)]
30+
[InlineData("sm_unchanged", SubmoduleStatus.InConfig | SubmoduleStatus.InHead | SubmoduleStatus.InIndex | SubmoduleStatus.InWorkDir)]
31+
public void CanRetrieveTheStatusOfASubmodule(string name, SubmoduleStatus expectedStatus)
32+
{
33+
var path = CloneSubmoduleTestRepo();
34+
using (var repo = new Repository(path))
35+
{
36+
var submodule = repo.Submodules[name];
37+
Assert.NotNull(submodule);
38+
Assert.Equal(name, submodule.Name);
39+
Assert.Equal(name, submodule.Path);
40+
41+
var status = submodule.RetrieveStatus();
42+
Assert.Equal(expectedStatus, status);
43+
}
44+
}
45+
46+
[Theory]
47+
[InlineData("sm_added_and_uncommited", null, "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0")]
48+
[InlineData("sm_changed_file", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0")]
49+
[InlineData("sm_changed_head", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "3d9386c507f6b093471a3e324085657a3c2b4247")]
50+
[InlineData("sm_changed_index", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0")]
51+
[InlineData("sm_changed_untracked_file", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0")]
52+
[InlineData("sm_gitmodules_only", null, null, null)]
53+
[InlineData("sm_missing_commits", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "5e4963595a9774b90524d35a807169049de8ccad")]
54+
[InlineData("sm_unchanged", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0", "480095882d281ed676fe5b863569520e54a7d5c0")]
55+
public void CanRetrieveTheCommitIdsOfASubmodule(string name, string headId, string indexId, string workDirId)
56+
{
57+
var path = CloneSubmoduleTestRepo();
58+
using (var repo = new Repository(path))
59+
{
60+
var submodule = repo.Submodules[name];
61+
Assert.NotNull(submodule);
62+
Assert.Equal(name, submodule.Name);
63+
64+
Assert.Equal((ObjectId)headId, submodule.HeadCommitId);
65+
Assert.Equal((ObjectId)indexId, submodule.IndexCommitId);
66+
Assert.Equal((ObjectId)workDirId, submodule.WorkDirCommitId);
67+
}
68+
}
69+
70+
[Fact]
71+
public void CanEnumerateRepositorySubmodules()
72+
{
73+
var expectedSubmodules = new[]
74+
{
75+
"sm_added_and_uncommited",
76+
"sm_changed_file",
77+
"sm_changed_head",
78+
"sm_changed_index",
79+
"sm_changed_untracked_file",
80+
"sm_gitmodules_only",
81+
"sm_missing_commits",
82+
"sm_unchanged",
83+
};
84+
85+
var path = CloneSubmoduleTestRepo();
86+
using (var repo = new Repository(path))
87+
{
88+
var submodules = repo.Submodules.OrderBy(s => s.Name, StringComparer.Ordinal);
89+
90+
Assert.Equal(expectedSubmodules, submodules.Select(s => s.Name).ToArray());
91+
Assert.Equal(expectedSubmodules, submodules.Select(s => s.Path).ToArray());
92+
Assert.Equal(Enumerable.Repeat("../submodule_target_wd", expectedSubmodules.Length).ToArray(),
93+
submodules.Select(s => s.Url).ToArray());
94+
}
95+
}
96+
97+
[Theory]
98+
[InlineData("sm_changed_head")]
99+
// [InlineData("sm_changed_head/")] // AmbiguousSpecificationException - Do we want to support this?
100+
public void CanStageChangeInSubmoduleViaIndexStage(string submodulePath)
101+
{
102+
var path = CloneSubmoduleTestRepo();
103+
using (var repo = new Repository(path))
104+
{
105+
var submodule = repo.Submodules[submodulePath];
106+
107+
var statusBefore = submodule.RetrieveStatus();
108+
Assert.Equal(SubmoduleStatus.WorkDirModified, statusBefore & SubmoduleStatus.WorkDirModified);
109+
110+
repo.Index.Stage(submodulePath);
111+
112+
var statusAfter = submodule.RetrieveStatus();
113+
Assert.Equal(SubmoduleStatus.IndexModified, statusAfter & SubmoduleStatus.IndexModified);
114+
}
115+
}
116+
}
117+
}

LibGit2Sharp/Core/SubmoduleLazyGroup.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Globalization;
23
using LibGit2Sharp.Core.Handles;
34

45
namespace LibGit2Sharp.Core
@@ -17,6 +18,13 @@ protected override void EvaluateInternal(Action<SubmoduleSafeHandle> evaluator)
1718
{
1819
using (var handle = Proxy.git_submodule_lookup(repo.Handle, name))
1920
{
21+
if (handle == null)
22+
{
23+
throw new LibGit2SharpException(string.Format(
24+
CultureInfo.InvariantCulture,
25+
"Submodule lookup failed for '{0}'.", name));
26+
}
27+
2028
evaluator(handle);
2129
}
2230
}

LibGit2Sharp/IRepository.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public interface IRepository : IDisposable
6666
/// </summary>
6767
NoteCollection Notes { get; }
6868

69+
/// <summary>
70+
/// Submodules in the repository.
71+
/// </summary>
72+
SubmoduleCollection Submodules { get; }
73+
6974
/// <summary>
7075
/// Checkout the specified <see cref = "Branch" />.
7176
/// </summary>

LibGit2Sharp/Index.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public virtual void Stage(IEnumerable<string> paths)
170170
{
171171
RemoveFromIndex(relativePath);
172172
}
173-
else
173+
else if (!repo.Submodules.TryStage(relativePath, true))
174174
{
175175
AddToIndex(relativePath);
176176
}

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
<Compile Include="StashOptions.cs" />
8080
<Compile Include="OrphanedHeadException.cs" />
8181
<Compile Include="StashCollection.cs" />
82+
<Compile Include="Submodule.cs" />
83+
<Compile Include="SubmoduleCollection.cs" />
8284
<Compile Include="SubmoduleExtensions.cs" />
8385
<Compile Include="SubmoduleIgnore.cs" />
8486
<Compile Include="SubmoduleStatus.cs" />

LibGit2Sharp/Repository.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class Repository : IRepository
3636
private readonly Lazy<Network> network;
3737
private readonly Stack<IDisposable> toCleanup = new Stack<IDisposable>();
3838
private readonly Ignore ignore;
39+
private SubmoduleCollection submodules;
3940
private static readonly Lazy<string> versionRetriever = new Lazy<string>(RetrieveVersion);
4041
private readonly Lazy<PathCase> pathCase;
4142

@@ -118,6 +119,7 @@ public Repository(string path, RepositoryOptions options = null)
118119
ignore = new Ignore(this);
119120
network = new Lazy<Network>(() => new Network(this));
120121
pathCase = new Lazy<PathCase>(() => new PathCase(this));
122+
submodules = new SubmoduleCollection(this);
121123

122124
EagerlyLoadTheConfigIfAnyPathHaveBeenPassed(options);
123125
}
@@ -318,6 +320,14 @@ public NoteCollection Notes
318320
get { return notes; }
319321
}
320322

323+
/// <summary>
324+
/// Submodules in the repository.
325+
/// </summary>
326+
public SubmoduleCollection Submodules
327+
{
328+
get { return submodules; }
329+
}
330+
321331
#region IDisposable Members
322332

323333
/// <summary>

LibGit2Sharp/Submodule.cs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Globalization;
4+
using LibGit2Sharp.Core;
5+
6+
namespace LibGit2Sharp
7+
{
8+
/// <summary>
9+
/// A Submodule.
10+
/// </summary>
11+
[DebuggerDisplay("{DebuggerDisplay,nq}")]
12+
public class Submodule : IEquatable<Submodule>
13+
{
14+
private static readonly LambdaEqualityHelper<Submodule> equalityHelper =
15+
new LambdaEqualityHelper<Submodule>(x => x.Name, x => x.HeadCommitId);
16+
17+
private readonly Repository repo;
18+
private readonly string name;
19+
private readonly string path;
20+
private readonly string url;
21+
private readonly ILazy<ObjectId> indexCommitId;
22+
private readonly ILazy<ObjectId> headCommitId;
23+
private readonly ILazy<ObjectId> workdirCommitId;
24+
private readonly ILazy<bool> fetchRecurseSubmodulesRule;
25+
private readonly ILazy<SubmoduleIgnore> ignoreRule;
26+
private readonly ILazy<SubmoduleUpdate> updateRule;
27+
28+
/// <summary>
29+
/// Needed for mocking purposes.
30+
/// </summary>
31+
protected Submodule()
32+
{ }
33+
34+
internal Submodule(Repository repo, string name, string path, string url)
35+
{
36+
this.repo = repo;
37+
this.name = name;
38+
this.path = path;
39+
this.url = url;
40+
41+
var commitIds = new SubmoduleLazyGroup(repo, name);
42+
indexCommitId = commitIds.AddLazy(Proxy.git_submodule_index_id);
43+
headCommitId = commitIds.AddLazy(Proxy.git_submodule_head_id);
44+
workdirCommitId = commitIds.AddLazy(Proxy.git_submodule_wd_id);
45+
46+
var rules = new SubmoduleLazyGroup(repo, name);
47+
fetchRecurseSubmodulesRule = rules.AddLazy(Proxy.git_submodule_fetch_recurse_submodules);
48+
ignoreRule = rules.AddLazy(Proxy.git_submodule_ignore);
49+
updateRule = rules.AddLazy(Proxy.git_submodule_update);
50+
}
51+
52+
/// <summary>
53+
/// The name of the submodule.
54+
/// </summary>
55+
public virtual string Name { get { return name; } }
56+
57+
/// <summary>
58+
/// The path of the submodule.
59+
/// </summary>
60+
public virtual string Path { get { return path; } }
61+
62+
/// <summary>
63+
/// The URL of the submodule.
64+
/// </summary>
65+
public virtual string Url { get { return url; } }
66+
67+
/// <summary>
68+
/// The commit ID for this submodule in the index.
69+
/// </summary>
70+
public virtual ObjectId IndexCommitId { get { return indexCommitId.Value; } }
71+
72+
/// <summary>
73+
/// The commit ID for this submodule in the current HEAD tree.
74+
/// </summary>
75+
public virtual ObjectId HeadCommitId { get { return headCommitId.Value; } }
76+
77+
/// <summary>
78+
/// The commit ID for this submodule in the current working directory.
79+
/// </summary>
80+
public virtual ObjectId WorkDirCommitId { get { return workdirCommitId.Value; } }
81+
82+
/// <summary>
83+
/// The fetchRecurseSubmodules rule for the submodule.
84+
///
85+
/// Note that at this time, LibGit2Sharp does not honor this setting and the
86+
/// fetch functionality current ignores submodules.
87+
/// </summary>
88+
public virtual bool FetchRecurseSubmodulesRule { get { return fetchRecurseSubmodulesRule.Value; } }
89+
90+
/// <summary>
91+
/// The ignore rule of the submodule.
92+
/// </summary>
93+
public virtual SubmoduleIgnore IgnoreRule { get { return ignoreRule.Value; } }
94+
95+
/// <summary>
96+
/// The update rule of the submodule.
97+
/// </summary>
98+
public virtual SubmoduleUpdate UpdateRule { get { return updateRule.Value; } }
99+
100+
/// <summary>
101+
/// Retrieves the state of this submodule in the working directory compared to the staging area and the latest commmit.
102+
/// </summary>
103+
/// <returns></returns>
104+
public virtual SubmoduleStatus RetrieveStatus()
105+
{
106+
using (var handle = Proxy.git_submodule_lookup(repo.Handle, Name))
107+
{
108+
return Proxy.git_submodule_status(handle);
109+
}
110+
}
111+
112+
/// <summary>
113+
/// Determines whether the specified <see cref = "Object" /> is equal to the current <see cref = "Submodule" />.
114+
/// </summary>
115+
/// <param name = "obj">The <see cref = "Object" /> to compare with the current <see cref = "Submodule" />.</param>
116+
/// <returns>True if the specified <see cref = "Object" /> is equal to the current <see cref = "Submodule" />; otherwise, false.</returns>
117+
public override bool Equals(object obj)
118+
{
119+
return Equals(obj as Submodule);
120+
}
121+
122+
/// <summary>
123+
/// Determines whether the specified <see cref = "Submodule" /> is equal to the current <see cref = "Submodule" />.
124+
/// </summary>
125+
/// <param name = "other">The <see cref = "Submodule" /> to compare with the current <see cref = "Submodule" />.</param>
126+
/// <returns>True if the specified <see cref = "Submodule" /> is equal to the current <see cref = "Submodule" />; otherwise, false.</returns>
127+
public bool Equals(Submodule other)
128+
{
129+
return equalityHelper.Equals(this, other);
130+
}
131+
132+
/// <summary>
133+
/// Returns the hash code for this instance.
134+
/// </summary>
135+
/// <returns>A 32-bit signed integer hash code.</returns>
136+
public override int GetHashCode()
137+
{
138+
return equalityHelper.GetHashCode(this);
139+
}
140+
141+
/// <summary>
142+
/// Returns the <see cref = "Name" />, a <see cref = "String" /> representation of the current <see cref = "Submodule" />.
143+
/// </summary>
144+
/// <returns>The <see cref = "Name" /> that represents the current <see cref = "Submodule" />.</returns>
145+
public override string ToString()
146+
{
147+
return Name;
148+
}
149+
150+
private string DebuggerDisplay
151+
{
152+
get
153+
{
154+
return string.Format(CultureInfo.InvariantCulture,
155+
"{0} => {1}", Name, Url);
156+
}
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)