Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/libgit2sharp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornulltoken <emeric.fermas@gmail.com>2015-06-23 19:07:28 +0300
committernulltoken <emeric.fermas@gmail.com>2015-06-23 19:07:28 +0300
commit87dcccec77ebeee50465eb12d93c55c9a2186fbc (patch)
tree1d0599c6eaca70f3ef1afd1d39fd16b9c39f2e4c
parentc25c6e4310fc706eb4cb53285b52844377c90f61 (diff)
parent3382549797863d9d7eb7e850b3c142f45b48670b (diff)
Merge pull request #964 from libgit2/jamill/rebase
Initial Rebase implementation
-rw-r--r--LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj1
-rw-r--r--LibGit2Sharp.Tests/RebaseFixture.cs767
-rw-r--r--LibGit2Sharp.Tests/TestHelpers/Constants.cs3
-rw-r--r--LibGit2Sharp/AfterRebaseStepInfo.cs61
-rw-r--r--LibGit2Sharp/BeforeRebaseStepInfo.cs41
-rw-r--r--LibGit2Sharp/Core/GitOid.cs8
-rw-r--r--LibGit2Sharp/Core/GitRebaseOperation.cs13
-rw-r--r--LibGit2Sharp/Core/GitRebaseOptions.cs17
-rw-r--r--LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs13
-rw-r--r--LibGit2Sharp/Core/NativeMethods.cs61
-rw-r--r--LibGit2Sharp/Core/Proxy.cs197
-rw-r--r--LibGit2Sharp/Handlers.cs12
-rw-r--r--LibGit2Sharp/IRepository.cs5
-rw-r--r--LibGit2Sharp/Identity.cs25
-rw-r--r--LibGit2Sharp/LibGit2Sharp.csproj10
-rw-r--r--LibGit2Sharp/Rebase.cs317
-rw-r--r--LibGit2Sharp/RebaseOperationImpl.cs282
-rw-r--r--LibGit2Sharp/RebaseOptions.cs58
-rw-r--r--LibGit2Sharp/RebaseResult.cs76
-rw-r--r--LibGit2Sharp/RebaseStepInfo.cs36
-rw-r--r--LibGit2Sharp/Repository.cs13
-rw-r--r--LibGit2Sharp/Signature.cs19
22 files changed, 2034 insertions, 1 deletions
diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
index aca84414..29683198 100644
--- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
+++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj
@@ -66,6 +66,7 @@
<Compile Include="FilterFixture.cs" />
<Compile Include="GlobalSettingsFixture.cs" />
<Compile Include="PatchStatsFixture.cs" />
+ <Compile Include="RebaseFixture.cs" />
<Compile Include="RefSpecFixture.cs" />
<Compile Include="EqualityFixture.cs" />
<Compile Include="RevertFixture.cs" />
diff --git a/LibGit2Sharp.Tests/RebaseFixture.cs b/LibGit2Sharp.Tests/RebaseFixture.cs
new file mode 100644
index 00000000..9823b620
--- /dev/null
+++ b/LibGit2Sharp.Tests/RebaseFixture.cs
@@ -0,0 +1,767 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using LibGit2Sharp.Tests.TestHelpers;
+using Xunit;
+using Xunit.Extensions;
+
+namespace LibGit2Sharp.Tests
+{
+ public class RebaseFixture : BaseFixture
+ {
+ const string masterBranch1Name = "M1";
+ const string masterBranch2Name = "M2";
+ const string topicBranch1Name = "T1";
+ const string topicBranch2Name = "T2";
+ const string conflictBranch1Name = "C1";
+ const string topicBranch1PrimeName = "T1Prime";
+
+ string filePathA = "a.txt";
+ string filePathB = "b.txt";
+ string filePathC = "c.txt";
+ string filePathD = "d.txt";
+
+ [Theory]
+ [InlineData(topicBranch2Name, topicBranch2Name, topicBranch1Name, masterBranch1Name, 3)]
+ [InlineData(topicBranch2Name, topicBranch2Name, topicBranch1Name, topicBranch1Name, 3)]
+ [InlineData(topicBranch2Name, topicBranch1Name, masterBranch2Name, masterBranch2Name, 3)]
+ [InlineData(topicBranch2Name, topicBranch1Name, masterBranch2Name, null, 3)]
+ [InlineData(topicBranch1Name, null, masterBranch2Name, null, 3)]
+ public void CanRebase(string initialBranchName,
+ string branchName,
+ string upstreamName,
+ string ontoName,
+ int stepCount)
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(initialBranchName);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = (branchName == null) ? null : repo.Branches[branchName];
+ Branch upstream = repo.Branches[upstreamName];
+ Branch onto = (ontoName == null) ? null : repo.Branches[ontoName];
+ Commit expectedSinceCommit = (branch == null) ? repo.Head.Tip : branch.Tip;
+ Commit expectedUntilCommit = upstream.Tip;
+ Commit expectedOntoCommit = (onto == null) ? upstream.Tip : onto.Tip;
+
+ int beforeStepCallCount = 0;
+ int afterStepCallCount = 0;
+ bool beforeRebaseStepCountCorrect = true;
+ bool afterRebaseStepCountCorrect = true;
+ bool totalStepCountCorrect = true;
+
+ List<Commit> PreRebaseCommits = new List<Commit>();
+ List<CompletedRebaseStepInfo> PostRebaseResults = new List<CompletedRebaseStepInfo>();
+ ObjectId expectedParentId = upstream.Tip.Id;
+
+ RebaseOptions options = new RebaseOptions()
+ {
+ RebaseStepStarting = x =>
+ {
+ beforeRebaseStepCountCorrect &= beforeStepCallCount == x.StepIndex;
+ totalStepCountCorrect &= (x.TotalStepCount == stepCount);
+ beforeStepCallCount++;
+ PreRebaseCommits.Add(x.StepInfo.Commit);
+ },
+ RebaseStepCompleted = x =>
+ {
+ afterRebaseStepCountCorrect &= (afterStepCallCount == x.CompletedStepIndex);
+ totalStepCountCorrect &= (x.TotalStepCount == stepCount);
+ afterStepCallCount++;
+ PostRebaseResults.Add(new CompletedRebaseStepInfo(x.Commit, x.WasPatchAlreadyApplied));
+ },
+ };
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, options);
+
+ // Validation:
+ Assert.True(afterRebaseStepCountCorrect, "Unexpected CompletedStepIndex value in RebaseStepCompleted");
+ Assert.True(beforeRebaseStepCountCorrect, "Unexpected StepIndex value in RebaseStepStarting");
+ Assert.True(totalStepCountCorrect, "Unexpected TotalStepcount value in Rebase step callback");
+ Assert.Equal(RebaseStatus.Complete, rebaseResult.Status);
+ Assert.Equal(stepCount, rebaseResult.TotalStepCount);
+ Assert.Null(rebaseResult.CurrentStepInfo);
+
+ Assert.Equal(stepCount, rebaseResult.CompletedStepCount);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Assert.Equal(stepCount, beforeStepCallCount);
+ Assert.Equal(stepCount, afterStepCallCount);
+
+ // Verify the chain of source commits that were rebased.
+ CommitFilter sourceCommitFilter = new CommitFilter()
+ {
+ Since = expectedSinceCommit,
+ Until = expectedUntilCommit,
+ SortBy = CommitSortStrategies.Reverse | CommitSortStrategies.Topological,
+ };
+ Assert.Equal(repo.Commits.QueryBy(sourceCommitFilter), PreRebaseCommits);
+
+ // Verify the chain of commits that resulted from the rebase.
+ Commit expectedParent = expectedOntoCommit;
+ foreach (CompletedRebaseStepInfo stepInfo in PostRebaseResults)
+ {
+ Commit rebasedCommit = stepInfo.Commit;
+ Assert.Equal(expectedParent.Id, rebasedCommit.Parents.First().Id);
+ Assert.False(stepInfo.WasPatchAlreadyApplied);
+ expectedParent = rebasedCommit;
+ }
+
+ Assert.Equal(repo.Head.Tip, PostRebaseResults.Last().Commit);
+ }
+ }
+
+ [Fact]
+ public void CanRebaseBranchOntoItself()
+ {
+ // Maybe we should have an "up-to-date" return type for scenarios such as these,
+ // but for now this test is to make sure we do something reasonable
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+ repo.Checkout(topicBranch2Name);
+ Branch b = repo.Branches[topicBranch2Name];
+
+ RebaseResult result = repo.Rebase.Start(b, b, null, Constants.Identity, new RebaseOptions());
+ Assert.Equal(0, result.TotalStepCount);
+ Assert.Equal(RebaseStatus.Complete, result.Status);
+ Assert.Equal(0, result.CompletedStepCount);
+ }
+ }
+
+ private class CompletedRebaseStepInfo
+ {
+ public CompletedRebaseStepInfo(Commit commit, bool wasPatchAlreadyApplied)
+ {
+ Commit = commit;
+ WasPatchAlreadyApplied = wasPatchAlreadyApplied;
+ }
+
+ public Commit Commit { get; set; }
+
+ public bool WasPatchAlreadyApplied { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("CompletedRebaseStepInfo: {0}", Commit);
+ }
+ }
+
+ private class CompletedRebaseStepInfoEqualityComparer : IEqualityComparer<CompletedRebaseStepInfo>
+ {
+ bool IEqualityComparer<CompletedRebaseStepInfo>.Equals(CompletedRebaseStepInfo x, CompletedRebaseStepInfo y)
+ {
+ if (x == null && y == null)
+ {
+ return true;
+ }
+
+ if ((x == null && y != null) ||
+ (x != null && y == null))
+ {
+ return false;
+ }
+
+ return x.WasPatchAlreadyApplied == y.WasPatchAlreadyApplied &&
+ ObjectId.Equals(x.Commit, y.Commit);
+ }
+
+ int IEqualityComparer<CompletedRebaseStepInfo>.GetHashCode(CompletedRebaseStepInfo obj)
+ {
+ int hashCode = obj.WasPatchAlreadyApplied.GetHashCode();
+
+ if (obj.Commit != null)
+ {
+ hashCode += obj.Commit.GetHashCode();
+ }
+
+ return hashCode;
+ }
+ }
+
+ /// <summary>
+ /// Verify a single rebase, but in more detail.
+ /// </summary>
+ [Fact]
+ public void VerifyRebaseDetailed()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ Branch initialBranch = repo.Branches[topicBranch1Name];
+ Branch upstreamBranch = repo.Branches[masterBranch2Name];
+
+ repo.Checkout(initialBranch);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ bool wasCheckoutProgressCalled = false;
+ bool wasCheckoutProgressCalledForResetingHead = false;
+ bool wasCheckoutNotifyCalled = false;
+ bool wasCheckoutNotifyCalledForResetingHead = false;
+
+ bool startedApplyingSteps = false;
+
+ RebaseOptions options = new RebaseOptions()
+ {
+ OnCheckoutProgress = (x, y, z) =>
+ {
+ if (startedApplyingSteps)
+ {
+ wasCheckoutProgressCalled = true;
+ }
+ else
+ {
+ wasCheckoutProgressCalledForResetingHead = true;
+ }
+ },
+ OnCheckoutNotify = (x, y) =>
+ {
+ if (startedApplyingSteps)
+ {
+ wasCheckoutNotifyCalled = true;
+ }
+ else
+ {
+ wasCheckoutNotifyCalledForResetingHead = true;
+ }
+
+ return true;
+ },
+ CheckoutNotifyFlags = CheckoutNotifyFlags.Updated,
+
+ RebaseStepStarting = x => startedApplyingSteps = true,
+
+ };
+
+ repo.Rebase.Start(null, upstreamBranch, null, Constants.Identity2, options);
+
+ Assert.Equal(true, wasCheckoutNotifyCalledForResetingHead);
+ Assert.Equal(true, wasCheckoutProgressCalledForResetingHead);
+ Assert.Equal(true, wasCheckoutNotifyCalled);
+ Assert.Equal(true, wasCheckoutProgressCalled);
+
+ // Verify the chain of resultant rebased commits.
+ CommitFilter commitFilter = new CommitFilter()
+ {
+ Since = repo.Head.Tip,
+ Until = upstreamBranch.Tip,
+ SortBy = CommitSortStrategies.Reverse | CommitSortStrategies.Topological,
+ };
+
+ List<ObjectId> expectedTreeIds = new List<ObjectId>()
+ {
+ new ObjectId("447bad85bcc1882037848370620a6f88e8ee264e"),
+ new ObjectId("3b0fc846952496a64b6149064cde21215daca8f8"),
+ new ObjectId("a2d114246012daf3ef8e7ccbfbe91889a24e1e60"),
+ };
+
+ List<Commit> rebasedCommits = repo.Commits.QueryBy(commitFilter).ToList();
+
+ Assert.Equal(3, rebasedCommits.Count);
+ for(int i = 0; i < 3; i++)
+ {
+ Assert.Equal(expectedTreeIds[i], rebasedCommits[i].Tree.Id);
+ Assert.Equal(Constants.Signature.Name, rebasedCommits[i].Author.Name);
+ Assert.Equal(Constants.Signature.Email, rebasedCommits[i].Author.Email);
+ Assert.Equal(Constants.Signature2.Name, rebasedCommits[i].Committer.Name);
+ Assert.Equal(Constants.Signature2.Email, rebasedCommits[i].Committer.Email);
+ }
+ }
+ }
+
+ [Fact]
+ public void CanContinueRebase()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ int beforeStepCallCount = 0;
+ int afterStepCallCount = 0;
+ bool wasCheckoutProgressCalled = false;
+ bool wasCheckoutNotifyCalled = false;
+
+ RebaseOptions options = new RebaseOptions()
+ {
+ RebaseStepStarting = x => beforeStepCallCount++,
+ RebaseStepCompleted = x => afterStepCallCount++,
+ OnCheckoutProgress = (x, y, z) => wasCheckoutProgressCalled = true,
+ OnCheckoutNotify = (x, y) => { wasCheckoutNotifyCalled = true; return true; },
+ CheckoutNotifyFlags = CheckoutNotifyFlags.Updated,
+ };
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, options);
+
+ // Verify that we have a conflict.
+ Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.False(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ // Verify that expected callbacks were called
+ Assert.Equal(1, beforeStepCallCount);
+ Assert.Equal(0, afterStepCallCount);
+ Assert.True(wasCheckoutProgressCalled, "CheckoutProgress callback was not called.");
+
+ // Resolve the conflict
+ foreach (Conflict conflict in repo.Index.Conflicts)
+ {
+ Touch(repo.Info.WorkingDirectory,
+ conflict.Theirs.Path,
+ repo.Lookup<Blob>(conflict.Theirs.Id).GetContentText(new FilteringOptions(conflict.Theirs.Path)));
+ repo.Stage(conflict.Theirs.Path);
+ }
+
+ Assert.True(repo.Index.IsFullyMerged);
+
+ // Clear the flags:
+ wasCheckoutProgressCalled = false; wasCheckoutNotifyCalled = false;
+ RebaseResult continuedRebaseResult = repo.Rebase.Continue(Constants.Identity, options);
+
+ Assert.NotNull(continuedRebaseResult);
+ Assert.Equal(RebaseStatus.Complete, continuedRebaseResult.Status);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+ Assert.True(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ Assert.Equal(3, beforeStepCallCount);
+ Assert.Equal(3, afterStepCallCount);
+ Assert.True(wasCheckoutProgressCalled, "CheckoutProgress callback was not called.");
+ Assert.True(wasCheckoutNotifyCalled, "CheckoutNotify callback was not called.");
+ }
+ }
+
+ [Fact]
+ public void ContinuingRebaseWithUnstagedChangesThrows()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, null);
+
+ // Verify that we have a conflict.
+ Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.False(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ Assert.Throws<UnmergedIndexEntriesException>(() =>
+ repo.Rebase.Continue(Constants.Identity, null));
+
+ // Resolve the conflict
+ foreach (Conflict conflict in repo.Index.Conflicts)
+ {
+ Touch(repo.Info.WorkingDirectory,
+ conflict.Theirs.Path,
+ repo.Lookup<Blob>(conflict.Theirs.Id).GetContentText(new FilteringOptions(conflict.Theirs.Path)));
+ repo.Stage(conflict.Theirs.Path);
+ }
+
+ Touch(repo.Info.WorkingDirectory,
+ filePathA,
+ "Unstaged content");
+
+ Assert.Throws<UnmergedIndexEntriesException>(() =>
+ repo.Rebase.Continue(Constants.Identity, null));
+
+ Assert.True(repo.Index.IsFullyMerged);
+ }
+ }
+
+ [Fact]
+ public void CanSpecifyFileConflictStrategy()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ RebaseOptions options = new RebaseOptions()
+ {
+ FileConflictStrategy = CheckoutFileConflictStrategy.Ours,
+ };
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, options);
+
+ // Verify that we have a conflict.
+ Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.False(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ string conflictFile = filePathB;
+ // Get the information on the conflict.
+ Conflict conflict = repo.Index.Conflicts[conflictFile];
+
+ Assert.NotNull(conflict);
+ Assert.NotNull(conflict.Theirs);
+ Assert.NotNull(conflict.Ours);
+
+ Blob expectedBlob = repo.Lookup<Blob>(conflict.Ours.Id);
+
+ // Check the content of the file on disk matches what is expected.
+ string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictFile));
+ Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictFile)));
+ }
+ }
+
+ [Fact]
+ public void CanQueryRebaseOperation()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, null);
+
+ // Verify that we have a conflict.
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.False(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ RebaseStepInfo info = repo.Rebase.GetCurrentStepInfo();
+
+ Assert.Equal(0, repo.Rebase.GetCurrentStepIndex());
+ Assert.Equal(3, repo.Rebase.GetTotalStepCount());
+ Assert.Equal(RebaseStepOperation.Pick, info.Type);
+ }
+ }
+
+ [Fact]
+ public void CanAbortRebase()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, null);
+
+ // Verify that we have a conflict.
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.False(repo.Index.IsFullyMerged);
+ Assert.Equal(0, rebaseResult.CompletedStepCount);
+ Assert.Equal(3, rebaseResult.TotalStepCount);
+
+ // Set up the callbacks to verify that checkout progress / notify
+ // callbacks are called.
+ bool wasCheckoutProgressCalled = false;
+ bool wasCheckoutNotifyCalled = false;
+ RebaseOptions options = new RebaseOptions()
+ {
+ OnCheckoutProgress = (x, y, z) => wasCheckoutProgressCalled = true,
+ OnCheckoutNotify = (x, y) => { wasCheckoutNotifyCalled = true; return true; },
+ CheckoutNotifyFlags = CheckoutNotifyFlags.Updated,
+ };
+
+ repo.Rebase.Abort(options);
+ Assert.False(repo.RetrieveStatus().IsDirty, "Repository workdir is dirty after Rebase.Abort.");
+ Assert.True(repo.Index.IsFullyMerged, "Repository index is not fully merged after Rebase.Abort.");
+ Assert.Equal(CurrentOperation.None, repo.Info.CurrentOperation);
+
+ Assert.True(wasCheckoutProgressCalled, "Checkout progress callback was not called during Rebase.Abort.");
+ Assert.True(wasCheckoutNotifyCalled, "Checkout notify callback was not called during Rebase.Abort.");
+ }
+ }
+
+ [Fact]
+ public void RebaseWhileAlreadyRebasingThrows()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+ Assert.False(repo.RetrieveStatus().IsDirty);
+
+ Branch branch = repo.Branches[topicBranch1Name];
+ Branch upstream = repo.Branches[conflictBranch1Name];
+ Branch onto = repo.Branches[conflictBranch1Name];
+
+ RebaseResult rebaseResult = repo.Rebase.Start(branch, upstream, onto, Constants.Identity, null);
+
+ // Verify that we have a conflict.
+ Assert.Equal(RebaseStatus.Conflicts, rebaseResult.Status);
+ Assert.True(repo.RetrieveStatus().IsDirty);
+ Assert.Equal(CurrentOperation.RebaseMerge, repo.Info.CurrentOperation);
+
+ Assert.Throws<LibGit2SharpException>(() =>
+ repo.Rebase.Start(branch, upstream, onto, Constants.Identity, null));
+ }
+ }
+
+ [Fact]
+ public void RebaseOperationsWithoutRebasingThrow()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+
+ Assert.Throws<NotFoundException>(() =>
+ repo.Rebase.Continue(Constants.Identity, new RebaseOptions()));
+
+ Assert.Throws<NotFoundException>(() =>
+ repo.Rebase.Abort());
+ }
+ }
+
+ [Fact]
+ public void CurrentStepInfoIsNullWhenNotRebasing()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+ repo.Checkout(topicBranch1Name);
+
+ Assert.Null(repo.Rebase.GetCurrentStepInfo());
+ }
+ }
+
+ [Fact]
+ public void CanRebaseHandlePatchAlreadyApplied()
+ {
+ SelfCleaningDirectory scd = BuildSelfCleaningDirectory();
+ var path = Repository.Init(scd.DirectoryPath);
+ using (Repository repo = new Repository(path))
+ {
+ ConstructRebaseTestRepository(repo);
+
+ repo.Checkout(topicBranch1Name);
+
+ Branch topicBranch1Prime = repo.CreateBranch(topicBranch1PrimeName, masterBranch1Name);
+
+ string newFileRelativePath = "new_file.txt";
+ Touch(repo.Info.WorkingDirectory, newFileRelativePath, "New Content");
+ repo.Stage(newFileRelativePath);
+ Commit commit = repo.Commit("new commit 1", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ repo.Checkout(topicBranch1Prime);
+ var cherryPickResult = repo.CherryPick(commit, Constants.Signature2);
+ Assert.Equal(CherryPickStatus.CherryPicked, cherryPickResult.Status);
+
+ string newFileRelativePath2 = "new_file_2.txt";
+ Touch(repo.Info.WorkingDirectory, newFileRelativePath2, "New Content for path 2");
+ repo.Stage(newFileRelativePath2);
+ repo.Commit("new commit 2", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Branch upstreamBranch = repo.Branches[topicBranch1Name];
+
+ List<CompletedRebaseStepInfo> rebaseResults = new List<CompletedRebaseStepInfo>();
+
+ RebaseOptions options = new RebaseOptions()
+ {
+ RebaseStepCompleted = x =>
+ {
+ rebaseResults.Add(new CompletedRebaseStepInfo(x.Commit, x.WasPatchAlreadyApplied));
+ }
+ };
+
+ repo.Rebase.Start(null, upstreamBranch, null, Constants.Identity2, options);
+ ObjectId secondCommitExpectedTreeId = new ObjectId("ac04bf04980c9be72f64ba77fd0d9088a40ed681");
+ Signature secondCommitAuthorSignature = Constants.Signature;
+ Identity secondCommitCommiterIdentity = Constants.Identity2;
+
+ Assert.Equal(2, rebaseResults.Count);
+ Assert.True(rebaseResults[0].WasPatchAlreadyApplied);
+
+ Assert.False(rebaseResults[1].WasPatchAlreadyApplied);
+ Assert.NotNull(rebaseResults[1].Commit);
+
+ // This is the expected tree ID of the new commit.
+ Assert.True(ObjectId.Equals(secondCommitExpectedTreeId, rebaseResults[1].Commit.Tree.Id));
+ Assert.True(Signature.Equals(secondCommitAuthorSignature, rebaseResults[1].Commit.Author));
+ Assert.Equal<string>(secondCommitCommiterIdentity.Name, rebaseResults[1].Commit.Committer.Name, StringComparer.Ordinal);
+ Assert.Equal<string>(secondCommitCommiterIdentity.Email, rebaseResults[1].Commit.Committer.Email, StringComparer.Ordinal);
+ }
+ }
+
+ [Fact]
+ public void RebasingInBareRepositoryThrows()
+ {
+ string path = SandboxBareTestRepo();
+ using (var repo = new Repository(path))
+ {
+ Branch rebaseUpstreamBranch = repo.Branches["refs/heads/test"];
+
+ Assert.NotNull(rebaseUpstreamBranch);
+ Assert.Throws<BareRepositoryException>(() => repo.Rebase.Start(null, rebaseUpstreamBranch, null, Constants.Identity, new RebaseOptions()));
+ Assert.Throws<BareRepositoryException>(() => repo.Rebase.Continue(Constants.Identity, new RebaseOptions()));
+ Assert.Throws<BareRepositoryException>(() => repo.Rebase.Abort());
+ }
+ }
+
+ private void ConstructRebaseTestRepository(Repository repo)
+ {
+ // Constructs a graph that looks like:
+ // * -- * -- * (modifications to c.txt)
+ // / |
+ // / T2
+ // /
+ // * -- * -- * (modifications to b.txt)
+ // / |
+ // / T1
+ // /
+ // *--*--*--*--*--*----
+ // | | \
+ // M1 M2 \
+ // ---*
+ // |
+ // C1
+ const string fileContentA1 = "A1";
+
+ const string fileContentB1 = "B1";
+ const string fileContentB2 = "B2";
+ const string fileContentB3 = "B3";
+ const string fileContentB4 = "B4";
+
+ const string fileContentC1 = "C1";
+ const string fileContentC2 = "C2";
+ const string fileContentC3 = "C3";
+ const string fileContentC4 = "C4";
+
+ const string fileContentD1 = "D1";
+ const string fileContentD2 = "D2";
+ const string fileContentD3 = "D3";
+
+ string workdir = repo.Info.WorkingDirectory;
+ Commit commit = null;
+
+ Touch(workdir, filePathA, fileContentA1);
+ repo.Stage(filePathA);
+ commit = repo.Commit("commit 1", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathB, fileContentB1);
+ repo.Stage(filePathB);
+ commit = repo.Commit("commit 2", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathC, fileContentC1);
+ repo.Stage(filePathC);
+ commit = repo.Commit("commit 3", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Branch masterBranch1 = repo.CreateBranch(masterBranch1Name, commit);
+
+ Touch(workdir, filePathB, string.Join(Environment.NewLine, fileContentB1, fileContentB2));
+ repo.Stage(filePathB);
+ commit = repo.Commit("commit 4", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathB, string.Join(Environment.NewLine, fileContentB1, fileContentB2, fileContentB3));
+ repo.Stage(filePathB);
+ commit = repo.Commit("commit 5", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathB, string.Join(Environment.NewLine, fileContentB1, fileContentB2, fileContentB3, fileContentB4));
+ repo.Stage(filePathB);
+ commit = repo.Commit("commit 6", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ repo.CreateBranch(topicBranch1Name, commit);
+
+ Touch(workdir, filePathC, string.Join(Environment.NewLine, fileContentC1, fileContentC2));
+ repo.Stage(filePathC);
+ commit = repo.Commit("commit 7", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathC, string.Join(Environment.NewLine, fileContentC1, fileContentC2, fileContentC3));
+ repo.Stage(filePathC);
+ commit = repo.Commit("commit 8", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathC, string.Join(Environment.NewLine, fileContentC1, fileContentC2, fileContentC3, fileContentC4));
+ repo.Stage(filePathC);
+ commit = repo.Commit("commit 9", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ repo.CreateBranch(topicBranch2Name, commit);
+
+ repo.Checkout(masterBranch1.Tip);
+ Touch(workdir, filePathD, fileContentD1);
+ repo.Stage(filePathD);
+ commit = repo.Commit("commit 10", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathD, string.Join(Environment.NewLine, fileContentD1, fileContentD2));
+ repo.Stage(filePathD);
+ commit = repo.Commit("commit 11", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ Touch(workdir, filePathD, string.Join(Environment.NewLine, fileContentD1, fileContentD2, fileContentD3));
+ repo.Stage(filePathD);
+ commit = repo.Commit("commit 12", Constants.Signature, Constants.Signature, new CommitOptions());
+
+ repo.CreateBranch(masterBranch2Name, commit);
+
+ // Create commit / branch that conflicts with T1 and T2
+ Touch(workdir, filePathB, string.Join(Environment.NewLine, fileContentB1, fileContentB2 + fileContentB3 + fileContentB4));
+ repo.Stage(filePathB);
+ commit = repo.Commit("commit 13", Constants.Signature, Constants.Signature, new CommitOptions());
+ repo.CreateBranch(conflictBranch1Name, commit);
+ }
+ }
+}
diff --git a/LibGit2Sharp.Tests/TestHelpers/Constants.cs b/LibGit2Sharp.Tests/TestHelpers/Constants.cs
index d3734207..51d9f697 100644
--- a/LibGit2Sharp.Tests/TestHelpers/Constants.cs
+++ b/LibGit2Sharp.Tests/TestHelpers/Constants.cs
@@ -12,7 +12,10 @@ namespace LibGit2Sharp.Tests.TestHelpers
public static readonly string TemporaryReposPath = BuildPath();
public const string UnknownSha = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
public static readonly Identity Identity = new Identity("A. U. Thor", "thor@valhalla.asgard.com");
+ public static readonly Identity Identity2 = new Identity("nulltoken", "emeric.fermas@gmail.com");
+
public static readonly Signature Signature = new Signature(Identity, new DateTimeOffset(2011, 06, 16, 10, 58, 27, TimeSpan.FromHours(2)));
+ public static readonly Signature Signature2 = new Signature(Identity2, DateTimeOffset.Parse("Wed, Dec 14 2011 08:29:03 +0100"));
// Populate these to turn on live credential tests: set the
// PrivateRepoUrl to the URL of a repository that requires
diff --git a/LibGit2Sharp/AfterRebaseStepInfo.cs b/LibGit2Sharp/AfterRebaseStepInfo.cs
new file mode 100644
index 00000000..8e6e78e2
--- /dev/null
+++ b/LibGit2Sharp/AfterRebaseStepInfo.cs
@@ -0,0 +1,61 @@
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// Information about a rebase step that was just completed.
+ /// </summary>
+ public class AfterRebaseStepInfo
+ {
+ /// <summary>
+ /// Needed for mocking.
+ /// </summary>
+ protected AfterRebaseStepInfo()
+ { }
+
+ internal AfterRebaseStepInfo(RebaseStepInfo stepInfo, Commit commit, long completedStepIndex, long totalStepCount)
+ {
+ StepInfo = stepInfo;
+ Commit = commit;
+ WasPatchAlreadyApplied = false;
+ CompletedStepIndex = completedStepIndex;
+ TotalStepCount = totalStepCount;
+ }
+
+ /// <summary>
+ /// Constructor to call when the patch has already been applied for this step.
+ /// </summary>
+ /// <param name="stepInfo"></param>
+ /// <param name="completedStepIndex"/>
+ /// <param name="totalStepCount"></param>
+ internal AfterRebaseStepInfo(RebaseStepInfo stepInfo, long completedStepIndex, long totalStepCount)
+ : this (stepInfo, null, completedStepIndex, totalStepCount)
+ {
+ WasPatchAlreadyApplied = true;
+ }
+
+ /// <summary>
+ /// The info on the completed step.
+ /// </summary>
+ public virtual RebaseStepInfo StepInfo { get; private set; }
+
+ /// <summary>
+ /// The commit generated by the step, if any.
+ /// </summary>
+ public virtual Commit Commit { get; private set; }
+
+ /// <summary>
+ /// Was the changes for this step already applied. If so,
+ /// <see cref="AfterRebaseStepInfo.Commit"/> will be null.
+ /// </summary>
+ public virtual bool WasPatchAlreadyApplied { get; private set; }
+
+ /// <summary>
+ /// The index of the step that was just completed.
+ /// </summary>
+ public virtual long CompletedStepIndex { get; private set; }
+
+ /// <summary>
+ /// The total number of steps in the rebase operation.
+ /// </summary>
+ public virtual long TotalStepCount { get; private set; }
+ }
+}
diff --git a/LibGit2Sharp/BeforeRebaseStepInfo.cs b/LibGit2Sharp/BeforeRebaseStepInfo.cs
new file mode 100644
index 00000000..e01175c0
--- /dev/null
+++ b/LibGit2Sharp/BeforeRebaseStepInfo.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// Information about a rebase step that is about to be performed.
+ /// </summary>
+ public class BeforeRebaseStepInfo
+ {
+ /// <summary>
+ /// Needed for mocking.
+ /// </summary>
+ protected BeforeRebaseStepInfo()
+ { }
+
+ internal BeforeRebaseStepInfo(RebaseStepInfo stepInfo, long stepIndex, long totalStepCount)
+ {
+ StepInfo = stepInfo;
+ StepIndex = stepIndex;
+ TotalStepCount = totalStepCount;
+ }
+
+ /// <summary>
+ /// Information on the step that is about to be performed.
+ /// </summary>
+ public virtual RebaseStepInfo StepInfo { get; private set; }
+
+ /// <summary>
+ /// The index of the step that is to be run.
+ /// </summary>
+ public virtual long StepIndex { get; private set; }
+
+ /// <summary>
+ /// The total number of steps in the rebase operation.
+ /// </summary>
+ public virtual long TotalStepCount { get; private set; }
+ }
+}
diff --git a/LibGit2Sharp/Core/GitOid.cs b/LibGit2Sharp/Core/GitOid.cs
index 9c593090..0f1be674 100644
--- a/LibGit2Sharp/Core/GitOid.cs
+++ b/LibGit2Sharp/Core/GitOid.cs
@@ -27,5 +27,13 @@ namespace LibGit2Sharp.Core
{
return oid == null ? null : new ObjectId(oid.Value);
}
+
+ /// <summary>
+ /// Static convenience property to return an id (all zeros).
+ /// </summary>
+ public static GitOid Empty
+ {
+ get { return new GitOid(); }
+ }
}
}
diff --git a/LibGit2Sharp/Core/GitRebaseOperation.cs b/LibGit2Sharp/Core/GitRebaseOperation.cs
new file mode 100644
index 00000000..660676ed
--- /dev/null
+++ b/LibGit2Sharp/Core/GitRebaseOperation.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace LibGit2Sharp.Core
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal class GitRebaseOperation
+ {
+ internal RebaseStepOperation type;
+ internal GitOid id;
+ internal IntPtr exec;
+ }
+}
diff --git a/LibGit2Sharp/Core/GitRebaseOptions.cs b/LibGit2Sharp/Core/GitRebaseOptions.cs
new file mode 100644
index 00000000..2a0a65e4
--- /dev/null
+++ b/LibGit2Sharp/Core/GitRebaseOptions.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace LibGit2Sharp.Core
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal class GitRebaseOptions
+ {
+ public uint version = 1;
+
+ public int quiet;
+
+ public IntPtr rewrite_notes_ref;
+
+ public GitCheckoutOpts checkout_options = new GitCheckoutOpts { version = 1 };
+ }
+}
diff --git a/LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs b/LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs
new file mode 100644
index 00000000..e5698fca
--- /dev/null
+++ b/LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs
@@ -0,0 +1,13 @@
+using LibGit2Sharp.Core.Handles;
+
+namespace LibGit2Sharp.Core
+{
+ internal class RebaseSafeHandle : SafeHandleBase
+ {
+ protected override bool ReleaseHandleImpl()
+ {
+ Proxy.git_rebase_free(handle);
+ return true;
+ }
+ }
+}
diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs
index 2590669b..74bc91b5 100644
--- a/LibGit2Sharp/Core/NativeMethods.cs
+++ b/LibGit2Sharp/Core/NativeMethods.cs
@@ -191,6 +191,61 @@ namespace LibGit2Sharp.Core
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string canonical_branch_name);
[DllImport(libgit2)]
+ internal static extern int git_rebase_init(
+ out RebaseSafeHandle rebase,
+ RepositorySafeHandle repo,
+ GitAnnotatedCommitHandle branch,
+ GitAnnotatedCommitHandle upstream,
+ GitAnnotatedCommitHandle onto,
+ GitRebaseOptions options);
+
+ [DllImport(libgit2)]
+ internal static extern int git_rebase_open(
+ out RebaseSafeHandle rebase,
+ RepositorySafeHandle repo,
+ GitRebaseOptions options);
+
+ [DllImport(libgit2)]
+ internal static extern UIntPtr git_rebase_operation_entrycount(
+ RebaseSafeHandle rebase);
+
+ [DllImport(libgit2)]
+ internal static extern UIntPtr git_rebase_operation_current(
+ RebaseSafeHandle rebase);
+
+ [DllImport(libgit2)]
+ internal static extern IntPtr git_rebase_operation_byindex(
+ RebaseSafeHandle rebase,
+ UIntPtr index);
+
+ [DllImport(libgit2)]
+ internal static extern int git_rebase_next(
+ out IntPtr operation,
+ RebaseSafeHandle rebase);
+
+ [DllImport(libgit2)]
+ internal static extern int git_rebase_commit(
+ ref GitOid id,
+ RebaseSafeHandle rebase,
+ SignatureSafeHandle author,
+ SignatureSafeHandle committer,
+ IntPtr message_encoding,
+ IntPtr message);
+
+ [DllImport(libgit2)]
+ internal static extern int git_rebase_abort(
+ RebaseSafeHandle rebase);
+
+ [DllImport(libgit2)]
+ internal static extern int git_rebase_finish(
+ RebaseSafeHandle repo,
+ SignatureSafeHandle signature);
+
+ [DllImport(libgit2)]
+ internal static extern void git_rebase_free(
+ IntPtr rebase);
+
+ [DllImport(libgit2)]
internal static extern int git_remote_rename(
ref GitStrArray problems,
RepositorySafeHandle repo,
@@ -1364,6 +1419,12 @@ namespace LibGit2Sharp.Core
int offset);
[DllImport(libgit2)]
+ internal static extern int git_signature_now(
+ out SignatureSafeHandle signature,
+ [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name,
+ [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string email);
+
+ [DllImport(libgit2)]
internal static extern int git_signature_dup(out IntPtr dest, IntPtr sig);
[DllImport(libgit2)]
diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs
index c18c3bfe..a581725e 100644
--- a/LibGit2Sharp/Core/Proxy.cs
+++ b/LibGit2Sharp/Core/Proxy.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -1539,6 +1540,174 @@ namespace LibGit2Sharp.Core
#endregion
+ #region git_rebase
+
+ public static RebaseSafeHandle git_rebase_init(
+ RepositorySafeHandle repo,
+ GitAnnotatedCommitHandle branch,
+ GitAnnotatedCommitHandle upstream,
+ GitAnnotatedCommitHandle onto,
+ GitRebaseOptions options)
+ {
+ RebaseSafeHandle rebase = null;
+
+ int result = NativeMethods.git_rebase_init(out rebase, repo, branch, upstream, onto, options);
+ Ensure.ZeroResult(result);
+
+ return rebase;
+ }
+
+ public static RebaseSafeHandle git_rebase_open(RepositorySafeHandle repo, GitRebaseOptions options)
+ {
+ RebaseSafeHandle rebase = null;
+
+ int result = NativeMethods.git_rebase_open(out rebase, repo, options);
+ Ensure.ZeroResult(result);
+
+ return rebase;
+ }
+
+ public static long git_rebase_operation_entrycount(RebaseSafeHandle rebase)
+ {
+ return NativeMethods.git_rebase_operation_entrycount(rebase).ConvertToLong();
+ }
+
+ public static long git_rebase_operation_current(RebaseSafeHandle rebase)
+ {
+ UIntPtr result = NativeMethods.git_rebase_operation_current(rebase);
+
+ if (result == GIT_REBASE_NO_OPERATION)
+ {
+ return RebaseNoOperation;
+ }
+ else
+ {
+ return result.ConvertToLong();
+ }
+ }
+
+ /// <summary>
+ /// The value from the native layer indicating that no rebase operation is in progress.
+ /// </summary>
+ private static UIntPtr GIT_REBASE_NO_OPERATION
+ {
+ get
+ {
+ return UIntPtr.Size == 4 ? new UIntPtr(uint.MaxValue) : new UIntPtr(ulong.MaxValue);
+ }
+ }
+
+ public const long RebaseNoOperation = -1;
+
+ public static GitRebaseOperation git_rebase_operation_byindex(
+ RebaseSafeHandle rebase,
+ long index)
+ {
+ Debug.Assert(index >= 0);
+ IntPtr ptr = NativeMethods.git_rebase_operation_byindex(rebase, ((UIntPtr)index));
+ GitRebaseOperation operation = ptr.MarshalAs<GitRebaseOperation>();
+
+ return operation;
+ }
+
+ /// <summary>
+ /// Returns null when finished.
+ /// </summary>
+ /// <param name="rebase"></param>
+ /// <returns></returns>
+ public static GitRebaseOperation git_rebase_next(RebaseSafeHandle rebase)
+ {
+ GitRebaseOperation operation = null;
+ IntPtr ptr;
+ int result = NativeMethods.git_rebase_next(out ptr, rebase);
+ if (result == (int)GitErrorCode.IterOver)
+ {
+ return null;
+ }
+ Ensure.ZeroResult(result);
+
+ // If successsful, then marshal native struct to managed struct.
+ operation = ptr.MarshalAs<GitRebaseOperation>();
+
+ return operation;
+ }
+
+ public static GitRebaseCommitResult git_rebase_commit(
+ RebaseSafeHandle rebase,
+ Identity author,
+ Identity committer)
+ {
+ Ensure.ArgumentNotNull(rebase, "rebase");
+ Ensure.ArgumentNotNull(committer, "committer");
+
+ using (SignatureSafeHandle committerHandle = committer.BuildNowSignatureHandle())
+ using (SignatureSafeHandle authorHandle = author.SafeBuildNowSignatureHandle())
+ {
+ GitRebaseCommitResult commitResult = new GitRebaseCommitResult();
+
+ int result = NativeMethods.git_rebase_commit(ref commitResult.CommitId, rebase, authorHandle, committerHandle, IntPtr.Zero, IntPtr.Zero);
+
+ if (result == (int)GitErrorCode.Applied)
+ {
+ commitResult.CommitId = GitOid.Empty;
+ commitResult.WasPatchAlreadyApplied = true;
+ }
+ else
+ {
+ Ensure.ZeroResult(result);
+ }
+
+ return commitResult;
+ }
+ }
+
+ /// <summary>
+ /// Struct to report the result of calling git_rebase_commit.
+ /// </summary>
+ public struct GitRebaseCommitResult
+ {
+ /// <summary>
+ /// The ID of the commit that was generated, if any
+ /// </summary>
+ public GitOid CommitId;
+
+ /// <summary>
+ /// bool to indicate if the patch was already applied.
+ /// If Patch was already applied, then CommitId will be empty (all zeros).
+ /// </summary>
+ public bool WasPatchAlreadyApplied;
+ }
+
+ public static void git_rebase_abort(
+ RebaseSafeHandle rebase)
+ {
+ Ensure.ArgumentNotNull(rebase, "rebase");
+
+ int result = NativeMethods.git_rebase_abort(rebase);
+ Ensure.ZeroResult(result);
+ }
+
+ public static void git_rebase_finish(
+ RebaseSafeHandle rebase,
+ Identity committer)
+ {
+ Ensure.ArgumentNotNull(rebase, "rebase");
+ Ensure.ArgumentNotNull(committer, "committer");
+
+ using (var signatureHandle = committer.BuildNowSignatureHandle())
+ {
+ int result = NativeMethods.git_rebase_finish(rebase, signatureHandle);
+ Ensure.ZeroResult(result);
+ }
+ }
+
+ public static void git_rebase_free(IntPtr rebase)
+ {
+ NativeMethods.git_rebase_free(rebase);
+ }
+
+ #endregion
+
#region git_reference_
public static ReferenceSafeHandle git_reference_create(RepositorySafeHandle repo, string name, ObjectId targetId, bool allowOverwrite,
@@ -2436,8 +2605,16 @@ namespace LibGit2Sharp.Core
SignatureSafeHandle handle;
int res = NativeMethods.git_signature_new(out handle, name, email, when.ToSecondsSinceEpoch(),
- (int)when.Offset.TotalMinutes);
+ (int)when.Offset.TotalMinutes);
+ Ensure.ZeroResult(res);
+ return handle;
+ }
+
+ public static SignatureSafeHandle git_signature_now(string name, string email)
+ {
+ SignatureSafeHandle handle;
+ int res = NativeMethods.git_signature_now(out handle, name, email);
Ensure.ZeroResult(res);
return handle;
@@ -3184,6 +3361,24 @@ namespace LibGit2Sharp.Core
return (int)input;
}
+
+
+ /// <summary>
+ /// Convert a UIntPtr to a long value. Will throw
+ /// exception if there is an overflow.
+ /// </summary>
+ /// <param name="input"></param>
+ /// <returns></returns>
+ public static long ConvertToLong(this UIntPtr input)
+ {
+ ulong ulongValue = (ulong)input;
+ if (ulongValue > long.MaxValue)
+ {
+ throw new LibGit2SharpException("value exceeds size of long");
+ }
+
+ return (long)input;
+ }
}
}
// ReSharper restore InconsistentNaming
diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs
index a64b21cb..fd20c7e8 100644
--- a/LibGit2Sharp/Handlers.cs
+++ b/LibGit2Sharp/Handlers.cs
@@ -128,6 +128,18 @@ namespace LibGit2Sharp.Handlers
public delegate bool StashApplyProgressHandler(StashApplyProgress progress);
/// <summary>
+ /// Delegate to report information on a rebase step that is about to be performed.
+ /// </summary>
+ /// <param name="beforeRebaseStep"></param>
+ public delegate void RebaseStepStartingHandler(BeforeRebaseStepInfo beforeRebaseStep);
+
+ /// <summary>
+ /// Delegate to report information on the rebase step that was just completed.
+ /// </summary>
+ /// <param name="afterRebaseStepInfo"></param>
+ public delegate void RebaseStepCompletedHandler(AfterRebaseStepInfo afterRebaseStepInfo);
+
+ /// <summary>
/// The stages of pack building.
/// </summary>
public enum PackBuilderStage
diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs
index 391cce1e..e92af14a 100644
--- a/LibGit2Sharp/IRepository.cs
+++ b/LibGit2Sharp/IRepository.cs
@@ -219,6 +219,11 @@ namespace LibGit2Sharp
MergeResult Merge(string committish, Signature merger, MergeOptions options);
/// <summary>
+ /// Access to Rebase functionality.
+ /// </summary>
+ Rebase Rebase { get; }
+
+ /// <summary>
/// Merge the reference that was recently fetched. This will merge
/// the branch on the fetched remote that corresponded to the
/// current local branch when we did the fetch. This is the
diff --git a/LibGit2Sharp/Identity.cs b/LibGit2Sharp/Identity.cs
index 38c1824d..e3ebd9ff 100644
--- a/LibGit2Sharp/Identity.cs
+++ b/LibGit2Sharp/Identity.cs
@@ -1,4 +1,5 @@
using LibGit2Sharp.Core;
+using LibGit2Sharp.Core.Handles;
namespace LibGit2Sharp
{
@@ -41,5 +42,29 @@ namespace LibGit2Sharp
{
get { return _name; }
}
+
+ internal SignatureSafeHandle BuildNowSignatureHandle()
+ {
+ return Proxy.git_signature_now(Name, Email);
+ }
+ }
+
+ internal static class IdentityHelpers
+ {
+ /// <summary>
+ /// Build the handle for the Indentity with the current time, or return a handle
+ /// to an empty signature.
+ /// </summary>
+ /// <param name="identity"></param>
+ /// <returns></returns>
+ public static SignatureSafeHandle SafeBuildNowSignatureHandle(this Identity identity)
+ {
+ if (identity == null)
+ {
+ return new SignatureSafeHandle();
+ }
+
+ return identity.BuildNowSignatureHandle();
+ }
}
}
diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj
index 6969c216..6e952be2 100644
--- a/LibGit2Sharp/LibGit2Sharp.csproj
+++ b/LibGit2Sharp/LibGit2Sharp.csproj
@@ -44,9 +44,11 @@
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="AfterRebaseStepInfo.cs" />
<Compile Include="AmbiguousSpecificationException.cs" />
<Compile Include="ArchiverBase.cs" />
<Compile Include="BareRepositoryException.cs" />
+ <Compile Include="BeforeRebaseStepInfo.cs" />
<Compile Include="BlameHunkCollection.cs" />
<Compile Include="BlameHunk.cs" />
<Compile Include="BlameOptions.cs" />
@@ -74,6 +76,9 @@
<Compile Include="Core\Handles\ConflictIteratorSafeHandle.cs" />
<Compile Include="Core\GitWriteStream.cs" />
<Compile Include="Core\WriteStream.cs" />
+ <Compile Include="Core\GitRebaseOperation.cs" />
+ <Compile Include="Core\GitRebaseOptions.cs" />
+ <Compile Include="Core\Handles\RebaseSafeHandle.cs" />
<Compile Include="DescribeOptions.cs" />
<Compile Include="DescribeStrategy.cs" />
<Compile Include="Core\GitDescribeFormatOptions.cs" />
@@ -129,6 +134,11 @@
<Compile Include="PullOptions.cs" />
<Compile Include="PushUpdate.cs" />
<Compile Include="RecurseSubmodulesException.cs" />
+ <Compile Include="RebaseOperationImpl.cs" />
+ <Compile Include="RebaseOptions.cs" />
+ <Compile Include="RebaseResult.cs" />
+ <Compile Include="Rebase.cs" />
+ <Compile Include="RebaseStepInfo.cs" />
<Compile Include="RefSpec.cs" />
<Compile Include="RefSpecCollection.cs" />
<Compile Include="Core\EncodingMarshaler.cs" />
diff --git a/LibGit2Sharp/Rebase.cs b/LibGit2Sharp/Rebase.cs
new file mode 100644
index 00000000..08836d3f
--- /dev/null
+++ b/LibGit2Sharp/Rebase.cs
@@ -0,0 +1,317 @@
+using System;
+using LibGit2Sharp.Core;
+using LibGit2Sharp.Core.Handles;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// The type of operation to be performed in a rebase step.
+ /// </summary>
+ public enum RebaseStepOperation
+ {
+ /// <summary>
+ /// Commit is to be cherry-picked.
+ /// </summary>
+ Pick = 0,
+
+ /// <summary>
+ /// Cherry-pick the commit and edit the commit message.
+ /// </summary>
+ Reword,
+
+ /// <summary>
+ /// Cherry-pick the commit but allow user to edit changes.
+ /// </summary>
+ Edit,
+
+ /// <summary>
+ /// Commit is to be squashed into previous commit. The commit
+ /// message will be merged with the previous message.
+ /// </summary>
+ Squash,
+
+ /// <summary>
+ /// Commit is to be squashed into previous commit. The commit
+ /// message will be discarded.
+ /// </summary>
+ Fixup,
+
+ // <summary>
+ // No commit to cherry-pick. Run the given command and continue
+ // if successful.
+ // </summary>
+ // Exec
+ }
+
+ /// <summary>
+ /// Encapsulates a rebase operation.
+ /// </summary>
+ public class Rebase
+ {
+ internal readonly Repository repository;
+
+ /// <summary>
+ /// Needed for mocking purposes.
+ /// </summary>
+ protected Rebase()
+ { }
+
+ internal Rebase(Repository repo)
+ {
+ this.repository = repo;
+ }
+
+ /// <summary>
+ /// Start a rebase operation.
+ /// </summary>
+ /// <param name="branch">The branch to rebase.</param>
+ /// <param name="upstream">The starting commit to rebase.</param>
+ /// <param name="onto">The branch to rebase onto.</param>
+ /// <param name="committer">The <see cref="Identity"/> of who added the change to the repository.</param>
+ /// <param name="options">The <see cref="RebaseOptions"/> that specify the rebase behavior.</param>
+ /// <returns>true if completed successfully, false if conflicts encountered.</returns>
+ public virtual RebaseResult Start(Branch branch, Branch upstream, Branch onto, Identity committer, RebaseOptions options)
+ {
+ Ensure.ArgumentNotNull(upstream, "upstream");
+
+ options = options ?? new RebaseOptions();
+
+ EnsureNonBareRepo();
+
+ if (this.repository.Info.CurrentOperation != CurrentOperation.None)
+ {
+ throw new LibGit2SharpException(string.Format(
+ "A {0} operation is already in progress.", this.repository.Info.CurrentOperation));
+ }
+
+ Func<Branch, ReferenceSafeHandle> RefHandleFromBranch = (Branch b) =>
+ {
+ return (b == null) ?
+ null :
+ this.repository.Refs.RetrieveReferencePtr(b.CanonicalName);
+ };
+
+ Func<ReferenceSafeHandle, GitAnnotatedCommitHandle> AnnotatedCommitHandleFromRefHandle =
+ (ReferenceSafeHandle refHandle) =>
+ {
+ return (refHandle == null) ?
+ new GitAnnotatedCommitHandle() :
+ Proxy.git_annotated_commit_from_ref(this.repository.Handle, refHandle);
+ };
+
+ using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
+ {
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ checkout_options = checkoutOptionsWrapper.Options,
+ };
+
+ using (ReferenceSafeHandle branchRefPtr = RefHandleFromBranch(branch))
+ using (ReferenceSafeHandle upstreamRefPtr = RefHandleFromBranch(upstream))
+ using (ReferenceSafeHandle ontoRefPtr = RefHandleFromBranch(onto))
+ using (GitAnnotatedCommitHandle annotatedBranchCommitHandle = AnnotatedCommitHandleFromRefHandle(branchRefPtr))
+ using (GitAnnotatedCommitHandle upstreamRefAnnotatedCommitHandle = AnnotatedCommitHandleFromRefHandle(upstreamRefPtr))
+ using (GitAnnotatedCommitHandle ontoRefAnnotatedCommitHandle = AnnotatedCommitHandleFromRefHandle(ontoRefPtr))
+ using (RebaseSafeHandle rebaseOperationHandle = Proxy.git_rebase_init(this.repository.Handle,
+ annotatedBranchCommitHandle,
+ upstreamRefAnnotatedCommitHandle,
+ ontoRefAnnotatedCommitHandle,
+ gitRebaseOptions))
+ {
+ RebaseResult rebaseResult = RebaseOperationImpl.Run(rebaseOperationHandle,
+ this.repository,
+ committer,
+ options);
+ return rebaseResult;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Continue the current rebase.
+ /// </summary>
+ /// <param name="committer">The <see cref="Identity"/> of who added the change to the repository.</param>
+ /// <param name="options">The <see cref="RebaseOptions"/> that specify the rebase behavior.</param>
+ public virtual RebaseResult Continue(Identity committer, RebaseOptions options)
+ {
+ Ensure.ArgumentNotNull(committer, "committer");
+
+ options = options ?? new RebaseOptions();
+
+ EnsureNonBareRepo();
+
+ using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
+ {
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ checkout_options = checkoutOptionsWrapper.Options,
+ };
+
+ using (RebaseSafeHandle rebase = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ // TODO: Should we check the pre-conditions for committing here
+ // for instance - what if we had failed on the git_rebase_finish call,
+ // do we want continue to be able to restart afterwords...
+ var rebaseCommitResult = Proxy.git_rebase_commit(rebase, null, committer);
+
+ // Report that we just completed the step
+ if (options.RebaseStepCompleted != null)
+ {
+ // Get information on the current step
+ long currentStepIndex = Proxy.git_rebase_operation_current(rebase);
+ long totalStepCount = Proxy.git_rebase_operation_entrycount(rebase);
+ GitRebaseOperation gitRebasestepInfo = Proxy.git_rebase_operation_byindex(rebase, currentStepIndex);
+
+ var stepInfo = new RebaseStepInfo(gitRebasestepInfo.type,
+ repository.Lookup<Commit>(new ObjectId(gitRebasestepInfo.id)),
+ LaxUtf8NoCleanupMarshaler.FromNative(gitRebasestepInfo.exec));
+
+ if (rebaseCommitResult.WasPatchAlreadyApplied)
+ {
+ options.RebaseStepCompleted(new AfterRebaseStepInfo(stepInfo, currentStepIndex, totalStepCount));
+ }
+ else
+ {
+ options.RebaseStepCompleted(new AfterRebaseStepInfo(stepInfo,
+ repository.Lookup<Commit>(new ObjectId(rebaseCommitResult.CommitId)),
+ currentStepIndex,
+ totalStepCount));
+ }
+ }
+
+ RebaseResult rebaseResult = RebaseOperationImpl.Run(rebase, repository, committer, options);
+ return rebaseResult;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Abort the rebase operation.
+ /// </summary>
+ public virtual void Abort()
+ {
+ Abort(null);
+ }
+
+ /// <summary>
+ /// Abort the rebase operation.
+ /// </summary>
+ /// <param name="options">The <see cref="RebaseOptions"/> that specify the rebase behavior.</param>
+ public virtual void Abort(RebaseOptions options)
+ {
+ options = options ?? new RebaseOptions();
+
+ EnsureNonBareRepo();
+
+ using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
+ {
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ checkout_options = checkoutOptionsWrapper.Options,
+ };
+
+ using (RebaseSafeHandle rebase = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ Proxy.git_rebase_abort(rebase);
+ }
+ }
+ }
+
+ /// <summary>
+ /// The info on the current step.
+ /// </summary>
+ public virtual RebaseStepInfo GetCurrentStepInfo()
+ {
+ if (repository.Info.CurrentOperation != LibGit2Sharp.CurrentOperation.RebaseMerge)
+ {
+ return null;
+ }
+
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ };
+
+ using (RebaseSafeHandle rebaseHandle = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ long currentStepIndex = Proxy.git_rebase_operation_current(rebaseHandle);
+ GitRebaseOperation gitRebasestepInfo = Proxy.git_rebase_operation_byindex(rebaseHandle, currentStepIndex);
+ var stepInfo = new RebaseStepInfo(gitRebasestepInfo.type,
+ repository.Lookup<Commit>(new ObjectId(gitRebasestepInfo.id)),
+ LaxUtf8Marshaler.FromNative(gitRebasestepInfo.exec));
+ return stepInfo;
+ }
+ }
+
+ /// <summary>
+ /// Get info on the specified step
+ /// </summary>
+ /// <param name="stepIndex"></param>
+ /// <returns></returns>
+ public virtual RebaseStepInfo GetStepInfo(long stepIndex)
+ {
+ if (repository.Info.CurrentOperation != LibGit2Sharp.CurrentOperation.RebaseMerge)
+ {
+ return null;
+ }
+
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ };
+
+ using (RebaseSafeHandle rebaseHandle = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ GitRebaseOperation gitRebasestepInfo = Proxy.git_rebase_operation_byindex(rebaseHandle, stepIndex);
+ var stepInfo = new RebaseStepInfo(gitRebasestepInfo.type,
+ repository.Lookup<Commit>(new ObjectId(gitRebasestepInfo.id)),
+ LaxUtf8Marshaler.FromNative(gitRebasestepInfo.exec));
+ return stepInfo;
+ }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <returns></returns>
+ public virtual long GetCurrentStepIndex()
+ {
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ };
+
+ using (RebaseSafeHandle rebaseHandle = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ return Proxy.git_rebase_operation_current(rebaseHandle);
+ }
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <returns></returns>
+ public virtual long GetTotalStepCount()
+ {
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ };
+
+ using (RebaseSafeHandle rebaseHandle = Proxy.git_rebase_open(repository.Handle, gitRebaseOptions))
+ {
+ return Proxy.git_rebase_operation_entrycount(rebaseHandle);
+ }
+ }
+
+ private void EnsureNonBareRepo()
+ {
+ if (this.repository.Info.IsBare)
+ {
+ throw new BareRepositoryException("Rebase operations in a bare repository are not supported.");
+ }
+ }
+ }
+}
diff --git a/LibGit2Sharp/RebaseOperationImpl.cs b/LibGit2Sharp/RebaseOperationImpl.cs
new file mode 100644
index 00000000..6b7a2cfa
--- /dev/null
+++ b/LibGit2Sharp/RebaseOperationImpl.cs
@@ -0,0 +1,282 @@
+using System;
+using LibGit2Sharp.Core;
+
+namespace LibGit2Sharp
+{
+ internal class RebaseOperationImpl
+ {
+ /// <summary>
+ /// Run a rebase to completion, a conflict, or a requested stop point.
+ /// </summary>
+ /// <param name="rebaseOperationHandle">Handle to the rebase operation.</param>
+ /// <param name="repository">Repository in which rebase operation is being run.</param>
+ /// <param name="committer">Committer Identity to use for the rebased commits.</param>
+ /// <param name="options">Options controlling rebase behavior.</param>
+ /// <returns>RebaseResult that describes the result of the rebase operation.</returns>
+ public static RebaseResult Run(RebaseSafeHandle rebaseOperationHandle,
+ Repository repository,
+ Identity committer,
+ RebaseOptions options)
+ {
+ Ensure.ArgumentNotNull(rebaseOperationHandle, "rebaseOperationHandle");
+ Ensure.ArgumentNotNull(repository, "repository");
+ Ensure.ArgumentNotNull(committer, "committer");
+ Ensure.ArgumentNotNull(options, "options");
+
+ RebaseResult rebaseResult = null;
+
+ // This loop will run until a rebase result has been set.
+ while (rebaseResult == null)
+ {
+ RebaseProgress rebaseStepContext = NextRebaseStep(repository, rebaseOperationHandle);
+
+ if (rebaseStepContext.current != -1)
+ {
+ rebaseResult = RunRebaseStep(rebaseOperationHandle,
+ repository,
+ committer,
+ options,
+ rebaseStepContext.current,
+ rebaseStepContext.total);
+ }
+ else
+ {
+ // No step to apply - need to complete the rebase.
+ rebaseResult = CompleteRebase(rebaseOperationHandle, committer, rebaseResult);
+ }
+ }
+
+ return rebaseResult;
+ }
+
+ private static RebaseResult CompleteRebase(RebaseSafeHandle rebaseOperationHandle, Identity committer, RebaseResult rebaseResult)
+ {
+ long totalStepCount = Proxy.git_rebase_operation_entrycount(rebaseOperationHandle);
+ GitRebaseOptions gitRebaseOptions = new GitRebaseOptions()
+ {
+ version = 1,
+ };
+
+ // Rebase is completed!
+ Proxy.git_rebase_finish(rebaseOperationHandle, committer);
+ rebaseResult = new RebaseResult(RebaseStatus.Complete,
+ totalStepCount,
+ totalStepCount,
+ null);
+ return rebaseResult;
+ }
+
+ /// <summary>
+ /// Run the current rebase step. This will handle reporting that we are about to run a rebase step,
+ /// identifying and running the operation for the current step, and reporting the current step is completed.
+ /// </summary>
+ /// <param name="rebaseOperationHandle"></param>
+ /// <param name="repository"></param>
+ /// <param name="committer"></param>
+ /// <param name="options"></param>
+ /// <param name="stepToApplyIndex"></param>
+ /// <param name="totalStepCount"/>
+ /// <returns></returns>
+ private static RebaseResult RunRebaseStep(RebaseSafeHandle rebaseOperationHandle,
+ Repository repository,
+ Identity committer,
+ RebaseOptions options,
+ long stepToApplyIndex,
+ long totalStepCount)
+ {
+ RebaseStepResult rebaseStepResult = null;
+ RebaseResult rebaseSequenceResult = null;
+
+ GitRebaseOperation rebaseOp = Proxy.git_rebase_operation_byindex(rebaseOperationHandle, stepToApplyIndex);
+ ObjectId idOfCommitBeingRebased = new ObjectId(rebaseOp.id);
+
+ RebaseStepInfo stepToApplyInfo = new RebaseStepInfo(rebaseOp.type,
+ repository.Lookup<Commit>(idOfCommitBeingRebased),
+ LaxUtf8NoCleanupMarshaler.FromNative(rebaseOp.exec));
+
+ // Report the rebase step we are about to perform.
+ if (options.RebaseStepStarting != null)
+ {
+ options.RebaseStepStarting(new BeforeRebaseStepInfo(stepToApplyInfo, stepToApplyIndex, totalStepCount));
+ }
+
+ // Perform the rebase step
+ GitRebaseOperation rebaseOpReport = Proxy.git_rebase_next(rebaseOperationHandle);
+
+ // Verify that the information from the native library is consistent.
+ VerifyRebaseOp(rebaseOpReport, stepToApplyInfo);
+
+ // Handle the result
+ switch (stepToApplyInfo.Type)
+ {
+ case RebaseStepOperation.Pick:
+ rebaseStepResult = ApplyPickStep(rebaseOperationHandle, repository, committer, options, stepToApplyInfo);
+ break;
+ case RebaseStepOperation.Squash:
+ case RebaseStepOperation.Edit:
+ // case RebaseStepOperation.Exec:
+ case RebaseStepOperation.Fixup:
+ case RebaseStepOperation.Reword:
+ // These operations are not yet supported by lg2.
+ throw new LibGit2SharpException(string.Format(
+ "Rebase Operation Type ({0}) is not currently supported in LibGit2Sharp.",
+ stepToApplyInfo.Type));
+ default:
+ throw new ArgumentException(string.Format(
+ "Unexpected Rebase Operation Type: {0}", stepToApplyInfo.Type));
+ }
+
+ // Report that we just completed the step
+ if (options.RebaseStepCompleted != null &&
+ (rebaseStepResult.Status == RebaseStepStatus.Committed ||
+ rebaseStepResult.Status == RebaseStepStatus.ChangesAlreadyApplied))
+ {
+ if (rebaseStepResult.ChangesAlreadyApplied)
+ {
+ options.RebaseStepCompleted(new AfterRebaseStepInfo(stepToApplyInfo, stepToApplyIndex, totalStepCount));
+ }
+ else
+ {
+ options.RebaseStepCompleted(new AfterRebaseStepInfo(stepToApplyInfo,
+ repository.Lookup<Commit>(new ObjectId(rebaseStepResult.CommitId)),
+ stepToApplyIndex,
+ totalStepCount));
+ }
+ }
+
+ // If the result of the rebase step is something that requires us to stop
+ // running the rebase sequence operations, then report the result.
+ if (rebaseStepResult.Status == RebaseStepStatus.Conflicts)
+ {
+ rebaseSequenceResult = new RebaseResult(RebaseStatus.Conflicts,
+ stepToApplyIndex,
+ totalStepCount,
+ null);
+ }
+
+ return rebaseSequenceResult;
+ }
+
+ private static RebaseStepResult ApplyPickStep(RebaseSafeHandle rebaseOperationHandle, Repository repository, Identity committer, RebaseOptions options, RebaseStepInfo stepToApplyInfo)
+ {
+ RebaseStepResult rebaseStepResult;
+
+ // commit and continue.
+ if (repository.Index.IsFullyMerged)
+ {
+ Proxy.GitRebaseCommitResult rebase_commit_result = Proxy.git_rebase_commit(rebaseOperationHandle, null, committer);
+
+ if (rebase_commit_result.WasPatchAlreadyApplied)
+ {
+ rebaseStepResult = new RebaseStepResult(RebaseStepStatus.ChangesAlreadyApplied);
+ }
+ else
+ {
+ rebaseStepResult = new RebaseStepResult(RebaseStepStatus.Committed, rebase_commit_result.CommitId);
+ }
+ }
+ else
+ {
+ rebaseStepResult = new RebaseStepResult(RebaseStepStatus.Conflicts);
+ }
+
+ return rebaseStepResult;
+ }
+
+ /// <summary>
+ /// Verify that the information in a GitRebaseOperation and a RebaseStepInfo agree
+ /// </summary>
+ /// <param name="rebaseOpReport"></param>
+ /// <param name="stepInfo"></param>
+ private static void VerifyRebaseOp(GitRebaseOperation rebaseOpReport, RebaseStepInfo stepInfo)
+ {
+ // The step reported via querying by index and the step returned from git_rebase_next
+ // should be the same
+ if (rebaseOpReport == null ||
+ new ObjectId(rebaseOpReport.id) != stepInfo.Commit.Id ||
+ rebaseOpReport.type != stepInfo.Type)
+ {
+ // This is indicative of a program error - should never happen.
+ throw new LibGit2SharpException("Unexpected step info reported by running rebase step.");
+ }
+ }
+
+ private struct RebaseProgress
+ {
+ public long current;
+ public long total;
+ }
+
+ /// <summary>
+ /// Returns the next rebase step, or null if there are none,
+ /// and the rebase operation needs to be finished.
+ /// </summary>
+ /// <param name="repository"></param>
+ /// <param name="rebaseOperationHandle"></param>
+ /// <returns></returns>
+ private static RebaseProgress NextRebaseStep(
+ Repository repository,
+ RebaseSafeHandle rebaseOperationHandle)
+ {
+ // stepBeingApplied indicates the step that will be applied by by git_rebase_next.
+ // The current step does not get incremented until git_rebase_next (except on
+ // the initial step), but we want to report the step that will be applied.
+ long stepToApplyIndex = Proxy.git_rebase_operation_current(rebaseOperationHandle);
+
+ stepToApplyIndex++;
+
+ long totalStepCount = Proxy.git_rebase_operation_entrycount(rebaseOperationHandle);
+
+ if (stepToApplyIndex == totalStepCount)
+ {
+ stepToApplyIndex = -1;
+ }
+
+ RebaseProgress progress = new RebaseProgress()
+ {
+ current = stepToApplyIndex,
+ total = totalStepCount,
+ };
+
+ return progress;
+ }
+
+ private enum RebaseStepStatus
+ {
+ Committed,
+ Conflicts,
+ ChangesAlreadyApplied,
+ }
+
+ private class RebaseStepResult
+ {
+ public RebaseStepResult(RebaseStepStatus status)
+ {
+ Status = status;
+ CommitId = GitOid.Empty;
+ }
+
+ public RebaseStepResult(RebaseStepStatus status, GitOid commitId)
+ {
+ Status = status;
+ CommitId = commitId;
+ }
+
+ /// <summary>
+ /// The ID of the commit that was generated, if any
+ /// </summary>
+ public GitOid CommitId;
+
+ /// <summary>
+ /// bool to indicate if the patch was already applied.
+ /// If Patch was already applied, then CommitId will be empty (all zeros).
+ /// </summary>
+ public bool ChangesAlreadyApplied
+ {
+ get { return Status == RebaseStepStatus.ChangesAlreadyApplied; }
+ }
+
+ public RebaseStepStatus Status;
+ }
+ }
+}
diff --git a/LibGit2Sharp/RebaseOptions.cs b/LibGit2Sharp/RebaseOptions.cs
new file mode 100644
index 00000000..62cb6cbd
--- /dev/null
+++ b/LibGit2Sharp/RebaseOptions.cs
@@ -0,0 +1,58 @@
+using LibGit2Sharp.Core;
+using LibGit2Sharp.Handlers;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// Options controlling rebase behavior.
+ /// </summary>
+ public sealed class RebaseOptions : IConvertableToGitCheckoutOpts
+ {
+ /// <summary>
+ /// Delegate that is called before each rebase step.
+ /// </summary>
+ public RebaseStepStartingHandler RebaseStepStarting { get; set; }
+
+ /// <summary>
+ /// Delegate that is called after each rebase step is completed.
+ /// </summary>
+ public RebaseStepCompletedHandler RebaseStepCompleted { get; set; }
+
+ /// <summary>
+ /// The Flags specifying what conditions are
+ /// reported through the OnCheckoutNotify delegate.
+ /// </summary>
+ public CheckoutNotifyFlags CheckoutNotifyFlags { get; set; }
+
+ /// <summary>
+ /// Delegate that the checkout will report progress through.
+ /// </summary>
+ public CheckoutProgressHandler OnCheckoutProgress { get; set; }
+
+ /// <summary>
+ /// Delegate that checkout will notify callers of
+ /// certain conditions. The conditions that are reported is
+ /// controlled with the CheckoutNotifyFlags property.
+ /// </summary>
+ public CheckoutNotifyHandler OnCheckoutNotify { get; set; }
+
+ /// <summary>
+ /// How conflicting index entries should be written out during checkout.
+ /// </summary>
+ public CheckoutFileConflictStrategy FileConflictStrategy { get; set; }
+
+ CheckoutCallbacks IConvertableToGitCheckoutOpts.GenerateCallbacks()
+ {
+ return CheckoutCallbacks.From(OnCheckoutProgress, OnCheckoutNotify);
+ }
+
+ CheckoutStrategy IConvertableToGitCheckoutOpts.CheckoutStrategy
+ {
+ get
+ {
+ return CheckoutStrategy.GIT_CHECKOUT_SAFE |
+ GitCheckoutOptsWrapper.CheckoutStrategyFromFileConflictStrategy(FileConflictStrategy);
+ }
+ }
+ }
+}
diff --git a/LibGit2Sharp/RebaseResult.cs b/LibGit2Sharp/RebaseResult.cs
new file mode 100644
index 00000000..48f61c1b
--- /dev/null
+++ b/LibGit2Sharp/RebaseResult.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// The status of the rebase.
+ /// </summary>
+ public enum RebaseStatus
+ {
+ /// <summary>
+ /// The rebase operation was run to completion
+ /// </summary>
+ Complete,
+
+ /// <summary>
+ /// The rebase operation hit a conflict and stopped.
+ /// </summary>
+ Conflicts,
+
+ /// <summary>
+ /// The rebase operation has hit a user requested stop point
+ /// (edit, reword, ect.)
+ /// </summary>
+ Stop,
+ };
+
+ /// <summary>
+ /// Information on a rebase operation.
+ /// </summary>
+ public class RebaseResult
+ {
+ /// <summary>
+ /// Needed for mocking.
+ /// </summary>
+ protected RebaseResult()
+ { }
+
+ internal RebaseResult(RebaseStatus status,
+ long stepNumber,
+ long totalSteps,
+ RebaseStepInfo currentStepInfo)
+ {
+ Status = status;
+ CompletedStepCount = stepNumber;
+ TotalStepCount = totalSteps;
+ CurrentStepInfo = currentStepInfo;
+ }
+
+ /// <summary>
+ /// Information on the operation to be performed in the current step.
+ /// If the overall Rebase operation has completed successfully, this will
+ /// be null.
+ /// </summary>
+ public virtual RebaseStepInfo CurrentStepInfo { get; private set; }
+
+ /// <summary>
+ /// Did the rebase operation run until it should stop
+ /// (completed the rebase, or the operation for the current step
+ /// is one that sequencing should stop.
+ /// </summary>
+ public virtual RebaseStatus Status { get; protected set; }
+
+ /// <summary>
+ /// The number of completed steps.
+ /// </summary>
+ public virtual long CompletedStepCount { get; protected set; }
+
+ /// <summary>
+ /// The total number of steps in the rebase.
+ /// </summary>
+ public virtual long TotalStepCount { get; protected set; }
+ }
+}
diff --git a/LibGit2Sharp/RebaseStepInfo.cs b/LibGit2Sharp/RebaseStepInfo.cs
new file mode 100644
index 00000000..4e355769
--- /dev/null
+++ b/LibGit2Sharp/RebaseStepInfo.cs
@@ -0,0 +1,36 @@
+namespace LibGit2Sharp
+{
+ /// <summary>
+ /// Information on a particular step of a rebase operation.
+ /// </summary>
+ public class RebaseStepInfo
+ {
+ /// <summary>
+ /// Needed for mocking purposes.
+ /// </summary>
+ protected RebaseStepInfo()
+ { }
+
+ internal RebaseStepInfo(RebaseStepOperation type, Commit commit, string exec)
+ {
+ Type = type;
+ Commit = commit;
+ Exec = exec;
+ }
+
+ /// <summary>
+ /// The rebase operation type.
+ /// </summary>
+ public virtual RebaseStepOperation Type { get; private set; }
+
+ /// <summary>
+ /// The object ID the step is operating on.
+ /// </summary>
+ public virtual Commit Commit { get; private set; }
+
+ /// <summary>
+ /// Command to execute, if any.
+ /// </summary>
+ internal virtual string Exec { get; private set; }
+ }
+}
diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs
index d01c3677..0f6de8b6 100644
--- a/LibGit2Sharp/Repository.cs
+++ b/LibGit2Sharp/Repository.cs
@@ -31,6 +31,7 @@ namespace LibGit2Sharp
private readonly NoteCollection notes;
private readonly Lazy<ObjectDatabase> odb;
private readonly Lazy<Network> network;
+ private readonly Lazy<Rebase> rebaseOperation;
private readonly Stack<IDisposable> toCleanup = new Stack<IDisposable>();
private readonly Ignore ignore;
private readonly SubmoduleCollection submodules;
@@ -132,6 +133,7 @@ namespace LibGit2Sharp
notes = new NoteCollection(this);
ignore = new Ignore(this);
network = new Lazy<Network>(() => new Network(this));
+ rebaseOperation = new Lazy<Rebase>(() => new Rebase(this));
pathCase = new Lazy<PathCase>(() => new PathCase(this));
submodules = new SubmoduleCollection(this);
@@ -273,6 +275,17 @@ namespace LibGit2Sharp
}
/// <summary>
+ /// Provides access to rebase functionality for a repository.
+ /// </summary>
+ public Rebase Rebase
+ {
+ get
+ {
+ return rebaseOperation.Value;
+ }
+ }
+
+ /// <summary>
/// Gets the database.
/// </summary>
public ObjectDatabase ObjectDatabase
diff --git a/LibGit2Sharp/Signature.cs b/LibGit2Sharp/Signature.cs
index bc3f8143..149ce0aa 100644
--- a/LibGit2Sharp/Signature.cs
+++ b/LibGit2Sharp/Signature.cs
@@ -147,4 +147,23 @@ namespace LibGit2Sharp
return string.Format(CultureInfo.InvariantCulture, "{0} <{1}>", Name, Email);
}
}
+
+ internal static class SignatureHelpers
+ {
+ /// <summary>
+ /// Build the handle for the Signature, or return a handle
+ /// to an empty signature.
+ /// </summary>
+ /// <param name="signature"></param>
+ /// <returns></returns>
+ public static SignatureSafeHandle SafeBuildHandle(this Signature signature)
+ {
+ if (signature == null)
+ {
+ return new SignatureSafeHandle();
+ }
+
+ return signature.BuildHandle();
+ }
+ }
}