Skip to content

Commit e371486

Browse files
committed
Initial Rebase implementation
1 parent 296ee5d commit e371486

17 files changed

+1233
-2
lines changed

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<Compile Include="DescribeFixture.cs" />
6060
<Compile Include="GlobalSettingsFixture.cs" />
6161
<Compile Include="PatchStatsFixture.cs" />
62+
<Compile Include="RebaseFixture.cs" />
6263
<Compile Include="RefSpecFixture.cs" />
6364
<Compile Include="EqualityFixture.cs" />
6465
<Compile Include="RevertFixture.cs" />
@@ -157,4 +158,4 @@
157158
<Target Name="AfterBuild">
158159
</Target>
159160
-->
160-
</Project>
161+
</Project>

LibGit2Sharp.Tests/RebaseFixture.cs

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using LibGit2Sharp.Core;
7+
using LibGit2Sharp.Tests.TestHelpers;
8+
using Xunit;
9+
using Xunit.Extensions;
10+
11+
namespace LibGit2Sharp.Tests
12+
{
13+
public class RebaseFixture : BaseFixture
14+
{
15+
const string masterBranch1Name = "M1";
16+
const string masterBranch2Name = "M2";
17+
const string topicBranch1Name = "T1";
18+
const string topicBranch2Name = "T2";
19+
const string conflictBranch1Name = "C1";
20+
21+
[Theory]
22+
[InlineData(topicBranch2Name, topicBranch2Name, topicBranch1Name, masterBranch1Name, 3)]
23+
[InlineData(topicBranch2Name, topicBranch2Name, topicBranch1Name, topicBranch1Name, 3)]
24+
[InlineData(topicBranch2Name, topicBranch1Name, masterBranch2Name, masterBranch2Name, 3)]
25+
[InlineData(topicBranch2Name, topicBranch1Name, masterBranch2Name, null, 3)]
26+
[InlineData(topicBranch1Name, null, masterBranch2Name, null, 3)]
27+
public void CanRebase(string initialBranchName, string branchName, string upstreamName, string ontoName, int stepCount)
28+
{
29+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
30+
var path = Repository.Init(scd.DirectoryPath);
31+
using (Repository repo = new Repository(path))
32+
{
33+
ConstructRebaseTestRepository(repo);
34+
35+
repo.Checkout(initialBranchName);
36+
Assert.False(repo.RetrieveStatus().IsDirty);
37+
38+
Branch branch = (branchName == null) ? null : repo.Branches[branchName];
39+
Branch upstream = (upstreamName == null) ? null : repo.Branches[upstreamName];
40+
Branch onto = (ontoName == null) ? null : repo.Branches[ontoName];
41+
42+
int beforeStepCallCount = 0;
43+
int afterStepCallCount = 0;
44+
RebaseOptions options = new RebaseOptions()
45+
{
46+
RebaseStepStarting = x => beforeStepCallCount++,
47+
RebaseStepCompleted = x => afterStepCallCount++,
48+
};
49+
50+
RebaseResult rebaseResult = repo.Rebase(branch, upstream, onto, Constants.Signature, options);
51+
52+
// Validation:
53+
Assert.Equal(RebaseStatus.Complete, rebaseResult.Status);
54+
Assert.Equal(stepCount, rebaseResult.TotalStepCount);
55+
Assert.Null(rebaseResult.CurrentStepInfo);
56+
57+
// What is the "current step" of a completed operation?
58+
// it looks like it is total steps - 1
59+
Assert.Equal(stepCount, rebaseResult.CompletedStepCount);
60+
Assert.False(repo.RetrieveStatus().IsDirty);
61+
62+
Assert.Equal(stepCount, beforeStepCallCount);
63+
Assert.Equal(stepCount, afterStepCallCount);
64+
65+
// TODO: Validate the expected HEAD commit ID
66+
}
67+
}
68+
69+
[Fact]
70+
public void CanContinueRebase()
71+
{
72+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
73+
var path = Repository.Init(scd.DirectoryPath);
74+
using (Repository repo = new Repository(path))
75+
{
76+
ConstructRebaseTestRepository(repo);
77+
78+
repo.Checkout(topicBranch1Name);
79+
Assert.False(repo.RetrieveStatus().IsDirty);
80+
81+
Branch branch = repo.Branches[topicBranch1Name];
82+
Branch upstream = repo.Branches[conflictBranch1Name];
83+
Branch onto = repo.Branches[conflictBranch1Name];
84+
85+
int beforeStepCallCount = 0;
86+
int afterStepCallCount = 0;
87+
RebaseOptions options = new RebaseOptions()
88+
{
89+
RebaseStepStarting = x => beforeStepCallCount++,
90+
RebaseStepCompleted = x => afterStepCallCount++,
91+
};
92+
93+
RebaseResult rebaseResult = repo.Rebase(branch, upstream, onto, Constants.Signature, options);
94+
95+
// Verify that we have a conflict.
96+
Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
97+
Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
98+
Assert.True(repo.RetrieveStatus().IsDirty);
99+
Assert.False(repo.Index.IsFullyMerged);
100+
Assert.Equal(0, rebaseResult.CompletedStepCount);
101+
Assert.Equal(3, rebaseResult.TotalStepCount);
102+
103+
Assert.Equal(1, beforeStepCallCount);
104+
Assert.Equal(0, afterStepCallCount);
105+
106+
// Resolve the conflict
107+
foreach (Conflict conflict in repo.Index.Conflicts)
108+
{
109+
Touch(repo.Info.WorkingDirectory,
110+
conflict.Theirs.Path,
111+
repo.Lookup<Blob>(conflict.Theirs.Id).GetContentStream(new FilteringOptions(conflict.Theirs.Path)));
112+
repo.Stage(conflict.Theirs.Path);
113+
}
114+
115+
Assert.True(repo.Index.IsFullyMerged);
116+
117+
RebaseResult continuedRebaseResult = repo.CurrentRebaseOperation.Continue(Constants.Signature, options);
118+
119+
Assert.NotNull(continuedRebaseResult);
120+
Assert.Equal(RebaseStatus.Complete, continuedRebaseResult.Status);
121+
Assert.False(repo.RetrieveStatus().IsDirty);
122+
Assert.True(repo.Index.IsFullyMerged);
123+
Assert.Equal(0, rebaseResult.CompletedStepCount);
124+
Assert.Equal(3, rebaseResult.TotalStepCount);
125+
126+
Assert.Equal(3, beforeStepCallCount);
127+
Assert.Equal(3, afterStepCallCount);
128+
129+
// TODO: Validate the expected HEAD commit ID
130+
}
131+
}
132+
133+
[Fact]
134+
public void CanQueryRebaseOperation()
135+
{
136+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
137+
var path = Repository.Init(scd.DirectoryPath);
138+
using (Repository repo = new Repository(path))
139+
{
140+
ConstructRebaseTestRepository(repo);
141+
142+
repo.Checkout(topicBranch1Name);
143+
Assert.False(repo.RetrieveStatus().IsDirty);
144+
145+
Branch branch = repo.Branches[topicBranch1Name];
146+
Branch upstream = repo.Branches[conflictBranch1Name];
147+
Branch onto = repo.Branches[conflictBranch1Name];
148+
149+
RebaseResult rebaseResult = repo.Rebase(branch, upstream, onto, Constants.Signature, null);
150+
151+
// Verify that we have a conflict.
152+
Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
153+
Assert.True(repo.RetrieveStatus().IsDirty);
154+
Assert.False(repo.Index.IsFullyMerged);
155+
Assert.Equal(0, rebaseResult.CompletedStepCount);
156+
Assert.Equal(3, rebaseResult.TotalStepCount);
157+
158+
RebaseStepInfo info = repo.CurrentRebaseOperation.CurrentStepInfo;
159+
Assert.Equal(0, info.StepIndex);
160+
Assert.Equal(3, info.TotalStepCount);
161+
Assert.Equal(RebaseStepOperation.Pick, info.Type);
162+
}
163+
}
164+
165+
[Fact]
166+
public void CanAbortRebase()
167+
{
168+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
169+
var path = Repository.Init(scd.DirectoryPath);
170+
using (Repository repo = new Repository(path))
171+
{
172+
ConstructRebaseTestRepository(repo);
173+
174+
repo.Checkout(topicBranch1Name);
175+
Assert.False(repo.RetrieveStatus().IsDirty);
176+
177+
Branch branch = repo.Branches[topicBranch1Name];
178+
Branch upstream = repo.Branches[conflictBranch1Name];
179+
Branch onto = repo.Branches[conflictBranch1Name];
180+
181+
RebaseResult rebaseResult = repo.Rebase(branch, upstream, onto, Constants.Signature, null);
182+
183+
// Verify that we have a conflict.
184+
Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
185+
Assert.True(repo.RetrieveStatus().IsDirty);
186+
Assert.False(repo.Index.IsFullyMerged);
187+
Assert.Equal(0, rebaseResult.CompletedStepCount);
188+
Assert.Equal(3, rebaseResult.TotalStepCount);
189+
190+
repo.CurrentRebaseOperation.Abort();
191+
Assert.False(repo.RetrieveStatus().IsDirty);
192+
Assert.True(repo.Index.IsFullyMerged);
193+
Assert.Equal(CurrentOperation.None, repo.Info.CurrentOperation);
194+
}
195+
}
196+
197+
[Fact]
198+
public void RebaseWhileAlreadyRebasingThrows()
199+
{
200+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
201+
var path = Repository.Init(scd.DirectoryPath);
202+
using (Repository repo = new Repository(path))
203+
{
204+
ConstructRebaseTestRepository(repo);
205+
206+
repo.Checkout(topicBranch1Name);
207+
Assert.False(repo.RetrieveStatus().IsDirty);
208+
209+
Branch branch = repo.Branches[topicBranch1Name];
210+
Branch upstream = repo.Branches[conflictBranch1Name];
211+
Branch onto = repo.Branches[conflictBranch1Name];
212+
213+
RebaseResult rebaseResult = repo.Rebase(branch, upstream, onto, Constants.Signature, null);
214+
215+
// Verify that we have a conflict.
216+
Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
217+
Assert.True(repo.RetrieveStatus().IsDirty);
218+
Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
219+
220+
Assert.Throws<LibGit2SharpException>(() =>
221+
repo.Rebase(branch, upstream, onto, Constants.Signature, null));
222+
}
223+
}
224+
225+
public void CurrentRebaseOperationIsNullWhenNotRebasing()
226+
{
227+
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
228+
var path = Repository.Init(scd.DirectoryPath);
229+
using (Repository repo = new Repository(path))
230+
{
231+
ConstructRebaseTestRepository(repo);
232+
repo.Checkout(topicBranch1Name);
233+
234+
Assert.Null(repo.CurrentRebaseOperation);
235+
}
236+
}
237+
238+
private void ConstructRebaseTestRepository(Repository repo)
239+
{
240+
// * -- * -- * (modifications to c.txt)
241+
// / |
242+
// / T2
243+
// /
244+
// * -- * -- * (modifications to b.txt)
245+
// / |
246+
// / T1
247+
// /
248+
// *--*--*--*--*--*----
249+
// | | \
250+
// M1 M2 \
251+
// ---*
252+
// |
253+
// C1
254+
const string lineEnding = "\r\n";
255+
256+
string filePathA = "a.txt";
257+
const string fileContentA1 = "A1";
258+
// const string fileContentA2 = "A2";
259+
// const string fileContentA3 = "A3";
260+
261+
string filePathB = "b.txt";
262+
const string fileContentB1 = "B1";
263+
const string fileContentB2 = "B2";
264+
const string fileContentB3 = "B3";
265+
const string fileContentB4 = "B4";
266+
267+
string filePathC = "c.txt";
268+
const string fileContentC1 = "C1";
269+
const string fileContentC2 = "C2";
270+
const string fileContentC3 = "C3";
271+
const string fileContentC4 = "C4";
272+
273+
string filePathD = "d.txt";
274+
const string fileContentD1 = "D1";
275+
const string fileContentD2 = "D2";
276+
const string fileContentD3 = "D3";
277+
278+
string workdir = repo.Info.WorkingDirectory;
279+
Commit commit = null;
280+
281+
Touch(workdir, filePathA, fileContentA1);
282+
repo.Stage(filePathA);
283+
commit = repo.Commit("commit 1", Constants.Signature, Constants.Signature, new CommitOptions());
284+
285+
Touch(workdir, filePathB, fileContentB1);
286+
repo.Stage(filePathB);
287+
commit = repo.Commit("commit 2", Constants.Signature, Constants.Signature, new CommitOptions());
288+
289+
Touch(workdir, filePathC, fileContentC1);
290+
repo.Stage(filePathC);
291+
commit = repo.Commit("commit 3", Constants.Signature, Constants.Signature, new CommitOptions());
292+
293+
repo.CreateBranch(masterBranch1Name, commit, Constants.Signature);
294+
295+
Touch(workdir, filePathB, string.Join(lineEnding, fileContentB1, fileContentB2));
296+
repo.Stage(filePathB);
297+
commit = repo.Commit("commit 4", Constants.Signature, Constants.Signature, new CommitOptions());
298+
299+
Touch(workdir, filePathB, string.Join(lineEnding, fileContentB1, fileContentB2, fileContentB3));
300+
repo.Stage(filePathB);
301+
commit = repo.Commit("commit 5", Constants.Signature, Constants.Signature, new CommitOptions());
302+
303+
Touch(workdir, filePathB, string.Join(lineEnding, fileContentB1, fileContentB2, fileContentB3, fileContentB4));
304+
repo.Stage(filePathB);
305+
commit = repo.Commit("commit 6", Constants.Signature, Constants.Signature, new CommitOptions());
306+
307+
repo.CreateBranch(topicBranch1Name, commit, Constants.Signature);
308+
309+
Touch(workdir, filePathC, string.Join(lineEnding, fileContentC1, fileContentC2));
310+
repo.Stage(filePathC);
311+
commit = repo.Commit("commit 7", Constants.Signature, Constants.Signature, new CommitOptions());
312+
313+
Touch(workdir, filePathC, string.Join(lineEnding, fileContentC1, fileContentC2, fileContentC3));
314+
repo.Stage(filePathC);
315+
commit = repo.Commit("commit 8", Constants.Signature, Constants.Signature, new CommitOptions());
316+
317+
Touch(workdir, filePathC, string.Join(lineEnding, fileContentC1, fileContentC2, fileContentC3, fileContentC4));
318+
repo.Stage(filePathC);
319+
commit = repo.Commit("commit 9", Constants.Signature, Constants.Signature, new CommitOptions());
320+
321+
repo.CreateBranch(topicBranch2Name, commit, Constants.Signature);
322+
323+
repo.Checkout(masterBranch1Name);
324+
Touch(workdir, filePathD, fileContentD1);
325+
repo.Stage(filePathD);
326+
commit = repo.Commit("commit 10", Constants.Signature, Constants.Signature, new CommitOptions());
327+
328+
Touch(workdir, filePathD, string.Join(lineEnding, fileContentD1, fileContentD2));
329+
repo.Stage(filePathD);
330+
commit = repo.Commit("commit 11", Constants.Signature, Constants.Signature, new CommitOptions());
331+
332+
Touch(workdir, filePathD, string.Join(lineEnding, fileContentD1, fileContentD2, fileContentD3));
333+
repo.Stage(filePathD);
334+
commit = repo.Commit("commit 12", Constants.Signature, Constants.Signature, new CommitOptions());
335+
336+
repo.CreateBranch(masterBranch2Name, commit, Constants.Signature);
337+
338+
// Create commit / branch that conflicts with T1 and T2
339+
Touch(workdir, filePathB, string.Join(lineEnding, fileContentB1, fileContentB2 + fileContentB3 + fileContentB4));
340+
repo.Stage(filePathB);
341+
commit = repo.Commit("commit 13", Constants.Signature, Constants.Signature, new CommitOptions());
342+
repo.CreateBranch(conflictBranch1Name, commit, Constants.Signature);
343+
}
344+
}
345+
}

LibGit2Sharp/AfterRebaseStepInfo.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace LibGit2Sharp
7+
{
8+
/// <summary>
9+
/// Information about a rebase step that was just completed.
10+
/// </summary>
11+
public class AfterRebaseStepInfo
12+
{
13+
internal AfterRebaseStepInfo(RebaseStepInfo stepInfo, ObjectId commitId)
14+
{
15+
StepInfo = stepInfo;
16+
CommitId = commitId;
17+
}
18+
19+
/// <summary>
20+
/// The info on the completed step.
21+
/// </summary>
22+
public RebaseStepInfo StepInfo { get; private set; }
23+
24+
/// <summary>
25+
/// The ID of the commit generated by the step, if any.
26+
/// </summary>
27+
public ObjectId CommitId { get; private set; }
28+
}
29+
}

0 commit comments

Comments
 (0)