From 3382549797863d9d7eb7e850b3c142f45b48670b Mon Sep 17 00:00:00 2001 From: Jameson Miller Date: Mon, 22 Jun 2015 10:29:47 -0400 Subject: [PATCH] Implement Rebase operation --- LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj | 1 + LibGit2Sharp.Tests/RebaseFixture.cs | 767 ++++++++++++++++++ LibGit2Sharp.Tests/TestHelpers/Constants.cs | 3 + LibGit2Sharp/AfterRebaseStepInfo.cs | 61 ++ LibGit2Sharp/BeforeRebaseStepInfo.cs | 41 + LibGit2Sharp/Core/GitOid.cs | 8 + LibGit2Sharp/Core/GitRebaseOperation.cs | 13 + LibGit2Sharp/Core/GitRebaseOptions.cs | 17 + LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs | 13 + LibGit2Sharp/Core/NativeMethods.cs | 61 ++ LibGit2Sharp/Core/Proxy.cs | 197 ++++- LibGit2Sharp/Handlers.cs | 12 + LibGit2Sharp/IRepository.cs | 5 + LibGit2Sharp/Identity.cs | 25 + LibGit2Sharp/LibGit2Sharp.csproj | 10 + LibGit2Sharp/Rebase.cs | 317 ++++++++ LibGit2Sharp/RebaseOperationImpl.cs | 282 +++++++ LibGit2Sharp/RebaseOptions.cs | 58 ++ LibGit2Sharp/RebaseResult.cs | 76 ++ LibGit2Sharp/RebaseStepInfo.cs | 36 + LibGit2Sharp/Repository.cs | 13 + LibGit2Sharp/Signature.cs | 19 + 22 files changed, 2034 insertions(+), 1 deletion(-) create mode 100644 LibGit2Sharp.Tests/RebaseFixture.cs create mode 100644 LibGit2Sharp/AfterRebaseStepInfo.cs create mode 100644 LibGit2Sharp/BeforeRebaseStepInfo.cs create mode 100644 LibGit2Sharp/Core/GitRebaseOperation.cs create mode 100644 LibGit2Sharp/Core/GitRebaseOptions.cs create mode 100644 LibGit2Sharp/Core/Handles/RebaseSafeHandle.cs create mode 100644 LibGit2Sharp/Rebase.cs create mode 100644 LibGit2Sharp/RebaseOperationImpl.cs create mode 100644 LibGit2Sharp/RebaseOptions.cs create mode 100644 LibGit2Sharp/RebaseResult.cs create mode 100644 LibGit2Sharp/RebaseStepInfo.cs diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index aca844148..296831989 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -66,6 +66,7 @@ + diff --git a/LibGit2Sharp.Tests/RebaseFixture.cs b/LibGit2Sharp.Tests/RebaseFixture.cs new file mode 100644 index 000000000..9823b620c --- /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 PreRebaseCommits = new List(); + List PostRebaseResults = new List(); + 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 + { + bool IEqualityComparer.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.GetHashCode(CompletedRebaseStepInfo obj) + { + int hashCode = obj.WasPatchAlreadyApplied.GetHashCode(); + + if (obj.Commit != null) + { + hashCode += obj.Commit.GetHashCode(); + } + + return hashCode; + } + } + + /// + /// Verify a single rebase, but in more detail. + /// + [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 expectedTreeIds = new List() + { + new ObjectId("447bad85bcc1882037848370620a6f88e8ee264e"), + new ObjectId("3b0fc846952496a64b6149064cde21215daca8f8"), + new ObjectId("a2d114246012daf3ef8e7ccbfbe91889a24e1e60"), + }; + + List 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(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(() => + 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(conflict.Theirs.Id).GetContentText(new FilteringOptions(conflict.Theirs.Path))); + repo.Stage(conflict.Theirs.Path); + } + + Touch(repo.Info.WorkingDirectory, + filePathA, + "Unstaged content"); + + Assert.Throws(() => + 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(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(() => + 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(() => + repo.Rebase.Continue(Constants.Identity, new RebaseOptions())); + + Assert.Throws(() => + 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 rebaseResults = new List(); + + 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(secondCommitCommiterIdentity.Name, rebaseResults[1].Commit.Committer.Name, StringComparer.Ordinal); + Assert.Equal(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(() => repo.Rebase.Start(null, rebaseUpstreamBranch, null, Constants.Identity, new RebaseOptions())); + Assert.Throws(() => repo.Rebase.Continue(Constants.Identity, new RebaseOptions())); + Assert.Throws(() => 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 d3734207e..51d9f697f 100644 --- a/LibGit2Sharp.Tests/TestHelpers/Constants.cs +++ b/LibGit2Sharp.Tests/TestHelpers/Constants.cs @@ -12,7 +12,10 @@ public static class Constants 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 000000000..8e6e78e2d --- /dev/null +++ b/LibGit2Sharp/AfterRebaseStepInfo.cs @@ -0,0 +1,61 @@ +namespace LibGit2Sharp +{ + /// + /// Information about a rebase step that was just completed. + /// + public class AfterRebaseStepInfo + { + /// + /// Needed for mocking. + /// + protected AfterRebaseStepInfo() + { } + + internal AfterRebaseStepInfo(RebaseStepInfo stepInfo, Commit commit, long completedStepIndex, long totalStepCount) + { + StepInfo = stepInfo; + Commit = commit; + WasPatchAlreadyApplied = false; + CompletedStepIndex = completedStepIndex; + TotalStepCount = totalStepCount; + } + + /// + /// Constructor to call when the patch has already been applied for this step. + /// + /// + /// + /// + internal AfterRebaseStepInfo(RebaseStepInfo stepInfo, long completedStepIndex, long totalStepCount) + : this (stepInfo, null, completedStepIndex, totalStepCount) + { + WasPatchAlreadyApplied = true; + } + + /// + /// The info on the completed step. + /// + public virtual RebaseStepInfo StepInfo { get; private set; } + + /// + /// The commit generated by the step, if any. + /// + public virtual Commit Commit { get; private set; } + + /// + /// Was the changes for this step already applied. If so, + /// will be null. + /// + public virtual bool WasPatchAlreadyApplied { get; private set; } + + /// + /// The index of the step that was just completed. + /// + public virtual long CompletedStepIndex { get; private set; } + + /// + /// The total number of steps in the rebase operation. + /// + public virtual long TotalStepCount { get; private set; } + } +} diff --git a/LibGit2Sharp/BeforeRebaseStepInfo.cs b/LibGit2Sharp/BeforeRebaseStepInfo.cs new file mode 100644 index 000000000..e01175c08 --- /dev/null +++ b/LibGit2Sharp/BeforeRebaseStepInfo.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Information about a rebase step that is about to be performed. + /// + public class BeforeRebaseStepInfo + { + /// + /// Needed for mocking. + /// + protected BeforeRebaseStepInfo() + { } + + internal BeforeRebaseStepInfo(RebaseStepInfo stepInfo, long stepIndex, long totalStepCount) + { + StepInfo = stepInfo; + StepIndex = stepIndex; + TotalStepCount = totalStepCount; + } + + /// + /// Information on the step that is about to be performed. + /// + public virtual RebaseStepInfo StepInfo { get; private set; } + + /// + /// The index of the step that is to be run. + /// + public virtual long StepIndex { get; private set; } + + /// + /// The total number of steps in the rebase operation. + /// + public virtual long TotalStepCount { get; private set; } + } +} diff --git a/LibGit2Sharp/Core/GitOid.cs b/LibGit2Sharp/Core/GitOid.cs index 9c5930908..0f1be6743 100644 --- a/LibGit2Sharp/Core/GitOid.cs +++ b/LibGit2Sharp/Core/GitOid.cs @@ -27,5 +27,13 @@ public static implicit operator ObjectId(GitOid? oid) { return oid == null ? null : new ObjectId(oid.Value); } + + /// + /// Static convenience property to return an id (all zeros). + /// + 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 000000000..660676edb --- /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 000000000..2a0a65e42 --- /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 000000000..e5698fcab --- /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 2590669bf..74bc91b52 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -190,6 +190,61 @@ internal static extern int git_branch_remote_name( RepositorySafeHandle repo, [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, @@ -1363,6 +1418,12 @@ internal static extern int git_signature_new( long time, 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); diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index c18c3bfec..a581725e6 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 @@ public static Tuple git_patch_line_stats(PatchSafeHandle patch) #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(); + } + } + + /// + /// The value from the native layer indicating that no rebase operation is in progress. + /// + 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(); + + return operation; + } + + /// + /// Returns null when finished. + /// + /// + /// + 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(); + + 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; + } + } + + /// + /// Struct to report the result of calling git_rebase_commit. + /// + public struct GitRebaseCommitResult + { + /// + /// The ID of the commit that was generated, if any + /// + public GitOid CommitId; + + /// + /// bool to indicate if the patch was already applied. + /// If Patch was already applied, then CommitId will be empty (all zeros). + /// + 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 @@ public static SignatureSafeHandle git_signature_new(string name, string email, D 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 @@ public static int ConvertToInt(this UIntPtr input) return (int)input; } + + + /// + /// Convert a UIntPtr to a long value. Will throw + /// exception if there is an overflow. + /// + /// + /// + 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 a64b21cb5..fd20c7e8e 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -127,6 +127,18 @@ namespace LibGit2Sharp.Handlers /// True to continue checkout operation; false to cancel checkout operation. public delegate bool StashApplyProgressHandler(StashApplyProgress progress); + /// + /// Delegate to report information on a rebase step that is about to be performed. + /// + /// + public delegate void RebaseStepStartingHandler(BeforeRebaseStepInfo beforeRebaseStep); + + /// + /// Delegate to report information on the rebase step that was just completed. + /// + /// + public delegate void RebaseStepCompletedHandler(AfterRebaseStepInfo afterRebaseStepInfo); + /// /// The stages of pack building. /// diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index 391cce1e1..e92af14ae 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -218,6 +218,11 @@ public interface IRepository : IDisposable /// The of the merge. MergeResult Merge(string committish, Signature merger, MergeOptions options); + /// + /// Access to Rebase functionality. + /// + Rebase Rebase { get; } + /// /// Merge the reference that was recently fetched. This will merge /// the branch on the fetched remote that corresponded to the diff --git a/LibGit2Sharp/Identity.cs b/LibGit2Sharp/Identity.cs index 38c1824d7..e3ebd9ff7 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 @@ public string Name { get { return _name; } } + + internal SignatureSafeHandle BuildNowSignatureHandle() + { + return Proxy.git_signature_now(Name, Email); + } + } + + internal static class IdentityHelpers + { + /// + /// Build the handle for the Indentity with the current time, or return a handle + /// to an empty signature. + /// + /// + /// + 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 6969c216f..6e952be2e 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -44,9 +44,11 @@ + + @@ -74,6 +76,9 @@ + + + @@ -129,6 +134,11 @@ + + + + + diff --git a/LibGit2Sharp/Rebase.cs b/LibGit2Sharp/Rebase.cs new file mode 100644 index 000000000..08836d3f8 --- /dev/null +++ b/LibGit2Sharp/Rebase.cs @@ -0,0 +1,317 @@ +using System; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// The type of operation to be performed in a rebase step. + /// + public enum RebaseStepOperation + { + /// + /// Commit is to be cherry-picked. + /// + Pick = 0, + + /// + /// Cherry-pick the commit and edit the commit message. + /// + Reword, + + /// + /// Cherry-pick the commit but allow user to edit changes. + /// + Edit, + + /// + /// Commit is to be squashed into previous commit. The commit + /// message will be merged with the previous message. + /// + Squash, + + /// + /// Commit is to be squashed into previous commit. The commit + /// message will be discarded. + /// + Fixup, + + // + // No commit to cherry-pick. Run the given command and continue + // if successful. + // + // Exec + } + + /// + /// Encapsulates a rebase operation. + /// + public class Rebase + { + internal readonly Repository repository; + + /// + /// Needed for mocking purposes. + /// + protected Rebase() + { } + + internal Rebase(Repository repo) + { + this.repository = repo; + } + + /// + /// Start a rebase operation. + /// + /// The branch to rebase. + /// The starting commit to rebase. + /// The branch to rebase onto. + /// The of who added the change to the repository. + /// The that specify the rebase behavior. + /// true if completed successfully, false if conflicts encountered. + 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 RefHandleFromBranch = (Branch b) => + { + return (b == null) ? + null : + this.repository.Refs.RetrieveReferencePtr(b.CanonicalName); + }; + + Func 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; + } + } + } + + /// + /// Continue the current rebase. + /// + /// The of who added the change to the repository. + /// The that specify the rebase behavior. + 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(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(new ObjectId(rebaseCommitResult.CommitId)), + currentStepIndex, + totalStepCount)); + } + } + + RebaseResult rebaseResult = RebaseOperationImpl.Run(rebase, repository, committer, options); + return rebaseResult; + } + } + } + + /// + /// Abort the rebase operation. + /// + public virtual void Abort() + { + Abort(null); + } + + /// + /// Abort the rebase operation. + /// + /// The that specify the rebase behavior. + 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); + } + } + } + + /// + /// The info on the current step. + /// + 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(new ObjectId(gitRebasestepInfo.id)), + LaxUtf8Marshaler.FromNative(gitRebasestepInfo.exec)); + return stepInfo; + } + } + + /// + /// Get info on the specified step + /// + /// + /// + 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(new ObjectId(gitRebasestepInfo.id)), + LaxUtf8Marshaler.FromNative(gitRebasestepInfo.exec)); + return stepInfo; + } + } + + /// + /// + /// + /// + 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); + } + } + + /// + /// + /// + /// + 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 000000000..6b7a2cfad --- /dev/null +++ b/LibGit2Sharp/RebaseOperationImpl.cs @@ -0,0 +1,282 @@ +using System; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + internal class RebaseOperationImpl + { + /// + /// Run a rebase to completion, a conflict, or a requested stop point. + /// + /// Handle to the rebase operation. + /// Repository in which rebase operation is being run. + /// Committer Identity to use for the rebased commits. + /// Options controlling rebase behavior. + /// RebaseResult that describes the result of the rebase operation. + 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; + } + + /// + /// 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. + /// + /// + /// + /// + /// + /// + /// + /// + 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(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(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; + } + + /// + /// Verify that the information in a GitRebaseOperation and a RebaseStepInfo agree + /// + /// + /// + 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; + } + + /// + /// Returns the next rebase step, or null if there are none, + /// and the rebase operation needs to be finished. + /// + /// + /// + /// + 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; + } + + /// + /// The ID of the commit that was generated, if any + /// + public GitOid CommitId; + + /// + /// bool to indicate if the patch was already applied. + /// If Patch was already applied, then CommitId will be empty (all zeros). + /// + 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 000000000..62cb6cbdb --- /dev/null +++ b/LibGit2Sharp/RebaseOptions.cs @@ -0,0 +1,58 @@ +using LibGit2Sharp.Core; +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp +{ + /// + /// Options controlling rebase behavior. + /// + public sealed class RebaseOptions : IConvertableToGitCheckoutOpts + { + /// + /// Delegate that is called before each rebase step. + /// + public RebaseStepStartingHandler RebaseStepStarting { get; set; } + + /// + /// Delegate that is called after each rebase step is completed. + /// + public RebaseStepCompletedHandler RebaseStepCompleted { get; set; } + + /// + /// The Flags specifying what conditions are + /// reported through the OnCheckoutNotify delegate. + /// + public CheckoutNotifyFlags CheckoutNotifyFlags { get; set; } + + /// + /// Delegate that the checkout will report progress through. + /// + public CheckoutProgressHandler OnCheckoutProgress { get; set; } + + /// + /// Delegate that checkout will notify callers of + /// certain conditions. The conditions that are reported is + /// controlled with the CheckoutNotifyFlags property. + /// + public CheckoutNotifyHandler OnCheckoutNotify { get; set; } + + /// + /// How conflicting index entries should be written out during checkout. + /// + 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 000000000..48f61c1b1 --- /dev/null +++ b/LibGit2Sharp/RebaseResult.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// The status of the rebase. + /// + public enum RebaseStatus + { + /// + /// The rebase operation was run to completion + /// + Complete, + + /// + /// The rebase operation hit a conflict and stopped. + /// + Conflicts, + + /// + /// The rebase operation has hit a user requested stop point + /// (edit, reword, ect.) + /// + Stop, + }; + + /// + /// Information on a rebase operation. + /// + public class RebaseResult + { + /// + /// Needed for mocking. + /// + protected RebaseResult() + { } + + internal RebaseResult(RebaseStatus status, + long stepNumber, + long totalSteps, + RebaseStepInfo currentStepInfo) + { + Status = status; + CompletedStepCount = stepNumber; + TotalStepCount = totalSteps; + CurrentStepInfo = currentStepInfo; + } + + /// + /// Information on the operation to be performed in the current step. + /// If the overall Rebase operation has completed successfully, this will + /// be null. + /// + public virtual RebaseStepInfo CurrentStepInfo { get; private set; } + + /// + /// 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. + /// + public virtual RebaseStatus Status { get; protected set; } + + /// + /// The number of completed steps. + /// + public virtual long CompletedStepCount { get; protected set; } + + /// + /// The total number of steps in the rebase. + /// + public virtual long TotalStepCount { get; protected set; } + } +} diff --git a/LibGit2Sharp/RebaseStepInfo.cs b/LibGit2Sharp/RebaseStepInfo.cs new file mode 100644 index 000000000..4e3557696 --- /dev/null +++ b/LibGit2Sharp/RebaseStepInfo.cs @@ -0,0 +1,36 @@ +namespace LibGit2Sharp +{ + /// + /// Information on a particular step of a rebase operation. + /// + public class RebaseStepInfo + { + /// + /// Needed for mocking purposes. + /// + protected RebaseStepInfo() + { } + + internal RebaseStepInfo(RebaseStepOperation type, Commit commit, string exec) + { + Type = type; + Commit = commit; + Exec = exec; + } + + /// + /// The rebase operation type. + /// + public virtual RebaseStepOperation Type { get; private set; } + + /// + /// The object ID the step is operating on. + /// + public virtual Commit Commit { get; private set; } + + /// + /// Command to execute, if any. + /// + internal virtual string Exec { get; private set; } + } +} diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index d01c36774..0f6de8b61 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -31,6 +31,7 @@ public sealed class Repository : IRepository private readonly NoteCollection notes; private readonly Lazy odb; private readonly Lazy network; + private readonly Lazy rebaseOperation; private readonly Stack toCleanup = new Stack(); private readonly Ignore ignore; private readonly SubmoduleCollection submodules; @@ -132,6 +133,7 @@ public Repository(string path, RepositoryOptions options) notes = new NoteCollection(this); ignore = new Ignore(this); network = new Lazy(() => new Network(this)); + rebaseOperation = new Lazy(() => new Rebase(this)); pathCase = new Lazy(() => new PathCase(this)); submodules = new SubmoduleCollection(this); @@ -272,6 +274,17 @@ public Network Network } } + /// + /// Provides access to rebase functionality for a repository. + /// + public Rebase Rebase + { + get + { + return rebaseOperation.Value; + } + } + /// /// Gets the database. /// diff --git a/LibGit2Sharp/Signature.cs b/LibGit2Sharp/Signature.cs index bc3f8143f..149ce0aa2 100644 --- a/LibGit2Sharp/Signature.cs +++ b/LibGit2Sharp/Signature.cs @@ -147,4 +147,23 @@ public override string ToString() return string.Format(CultureInfo.InvariantCulture, "{0} <{1}>", Name, Email); } } + + internal static class SignatureHelpers + { + /// + /// Build the handle for the Signature, or return a handle + /// to an empty signature. + /// + /// + /// + public static SignatureSafeHandle SafeBuildHandle(this Signature signature) + { + if (signature == null) + { + return new SignatureSafeHandle(); + } + + return signature.BuildHandle(); + } + } }