From ac77036d873c4d808fda21e527d02ee78511c5a4 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Tue, 10 Dec 2024 22:07:32 +1100 Subject: [PATCH] Add support for creating pull requests as drafts (#131) --- .../CreatePullRequestsCommandHandlerTests.cs | 196 ++++++++++++++---- .../OpenPullRequestsCommandHandlerTests.cs | 20 +- .../Stack/StackStatusCommandHandlerTests.cs | 14 +- .../Stack/UpdateStackCommandHandlerTests.cs | 2 +- src/Stack/Commands/Helpers/Questions.cs | 1 + .../PullRequests/CreatePullRequestsCommand.cs | 33 ++- src/Stack/Git/GitHubOperations.cs | 20 +- .../Infrastructure/ConsoleInputProvider.cs | 12 +- 8 files changed, 230 insertions(+), 68 deletions(-) diff --git a/src/Stack.Tests/Commands/PullRequests/CreatePullRequestsCommandHandlerTests.cs b/src/Stack.Tests/Commands/PullRequests/CreatePullRequestsCommandHandlerTests.cs index 08b001a..e7578cd 100644 --- a/src/Stack.Tests/Commands/PullRequests/CreatePullRequestsCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/PullRequests/CreatePullRequestsCommandHandlerTests.cs @@ -53,22 +53,22 @@ public async Task WhenNoPullRequestsExistForAStackWithMultipleBranches_CreatesPu inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty) + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false) .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); // Act await handler.Handle(CreatePullRequestsCommandInputs.Empty); // Assert - gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty); - gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty); + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false); } [Fact] @@ -114,14 +114,14 @@ public async Task WhenCreatingPullRequestsForAStackWithMultipleBranches_EachPull inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); inputProvider.Text(Questions.PullRequestStackDescription, Arg.Any()).Returns("A custom description"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty) + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false) .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); gitHubOperations @@ -189,12 +189,12 @@ public async Task WhenAPullRequestExistForABranch_AndNoneForAnotherBranch_Create inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); inputProvider.Text(Questions.PullRequestStackDescription, Arg.Any()).Returns("A custom description"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations.GetPullRequest("branch-3").Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); gitHubOperations @@ -209,8 +209,8 @@ public async Task WhenAPullRequestExistForABranch_AndNoneForAnotherBranch_Create await handler.Handle(CreatePullRequestsCommandInputs.Empty); // Assert - gitHubOperations.DidNotReceive().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty); - gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty); + gitHubOperations.DidNotReceive().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false); var expectedStackDescription = $@" A custom description @@ -264,22 +264,22 @@ public async Task WhenStackNameIsProvided_PullRequestsAreCreatedForThatStack() inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty) + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false) .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); // Act - await handler.Handle(new CreatePullRequestsCommandInputs("Stack1")); + await handler.Handle(new CreatePullRequestsCommandInputs("Stack1", null)); // Assert - gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty); - gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty); + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false); } [Fact] @@ -319,22 +319,22 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_PullRequestsAreC inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty) + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false) .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); // Act await handler.Handle(CreatePullRequestsCommandInputs.Empty); // Assert - gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty); - gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty); + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, false); } [Fact] @@ -362,7 +362,7 @@ public async Task WhenStackNameIsProvided_ButTheStackDoesNotExist_Throws() // Act and assert var invalidStackName = Some.Name(); - await handler.Invoking(h => h.Handle(new CreatePullRequestsCommandInputs(invalidStackName))) + await handler.Invoking(h => h.Handle(new CreatePullRequestsCommandInputs(invalidStackName, null))) .Should() .ThrowAsync() .WithMessage($"Stack '{invalidStackName}' not found."); @@ -410,12 +410,12 @@ public async Task WhenAPullRequestExistForABranch_AndHasBeenMerged_AndNoneForAno inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-1")).Returns("PR Title for branch-5"); inputProvider.Text(Questions.PullRequestStackDescription, Arg.Any()).Returns("A custom description"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Merged, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Merged, Some.HttpsUri(), false); gitHubOperations.GetPullRequest("branch-3").Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-5", "branch-1", "PR Title for branch-5", string.Empty) + .CreatePullRequest("branch-5", "branch-1", "PR Title for branch-5", string.Empty, false) .Returns(prForBranch5); gitHubOperations @@ -430,8 +430,8 @@ public async Task WhenAPullRequestExistForABranch_AndHasBeenMerged_AndNoneForAno await handler.Handle(CreatePullRequestsCommandInputs.Empty); // Assert - gitHubOperations.DidNotReceive().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty); - gitHubOperations.Received().CreatePullRequest("branch-5", "branch-1", "PR Title for branch-5", string.Empty); + gitHubOperations.DidNotReceive().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, false); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-1", "PR Title for branch-5", string.Empty, false); var expectedStackDescription = $@" A custom description @@ -487,15 +487,137 @@ public async Task WhenAPullRequestTemplateExistsInTheRepo_ItIsUsedAsTheBodyOfANe inputProvider.Confirm(Questions.ConfirmCreatePullRequests).Returns(true); inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", "PR Template", GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", "PR Template", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations - .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", "PR Template") + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", "PR Template", false) .Returns(prForBranch3); // Act await handler.Handle(CreatePullRequestsCommandInputs.Empty); // Assert - gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", "PR Template"); + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", "PR Template", false); + } + + [Fact] + public async Task WhenAskedWhetherToCreateAPullRequestAsADraft_AndTheAnswerIsYes_PullRequestsCreatedAsADraft() + { + // Arrange + var gitOperations = Substitute.For(); + var gitHubOperations = Substitute.For(); + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = Substitute.For(); + var handler = new CreatePullRequestsCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + + var remoteUri = Some.HttpsUri().ToString(); + outputProvider + .WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())) + .Do(ci => ci.ArgAt(1)()); + + gitOperations.GetRemoteUri().Returns(remoteUri); + gitOperations.GetCurrentBranch().Returns("branch-1"); + gitOperations + .GetBranchesThatExistInRemote(Arg.Any()) + .Returns(["branch-1", "branch-3", "branch-5"]); + + gitOperations + .GetBranchesThatExistLocally(Arg.Any()) + .Returns(["branch-1", "branch-3", "branch-5"]); + + var stacks = new List( + [ + new("Stack1", remoteUri, "branch-1", ["branch-3", "branch-5"]), + new("Stack2", remoteUri, "branch-2", ["branch-4"]) + ]); + stackConfig.Load().Returns(stacks); + stackConfig + .WhenForAnyArgs(s => s.Save(Arg.Any>())) + .Do(ci => stacks = ci.ArgAt>(0)); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Confirm(Questions.ConfirmStartCreatePullRequests(2)).Returns(true); + inputProvider.Confirm(Questions.ConfirmCreatePullRequests).Returns(true); + inputProvider.Confirm(Questions.CreatePullRequestsAsDrafts, false).Returns(true); + inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); + inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); + + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), true); + gitHubOperations + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, true) + .Returns(prForBranch3); + + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), true); + gitHubOperations + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, true) + .Returns(prForBranch5); + + // Act + await handler.Handle(CreatePullRequestsCommandInputs.Empty); + + // Assert + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, true); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, true); + } + + [Fact] + public async Task WhenDraftIsProvided_PullRequestsAreCreatedAsDrafts() + { + // Arrange + var gitOperations = Substitute.For(); + var gitHubOperations = Substitute.For(); + var stackConfig = Substitute.For(); + var inputProvider = Substitute.For(); + var outputProvider = Substitute.For(); + var handler = new CreatePullRequestsCommandHandler(inputProvider, outputProvider, gitOperations, gitHubOperations, stackConfig); + + var remoteUri = Some.HttpsUri().ToString(); + outputProvider + .WhenForAnyArgs(o => o.Status(Arg.Any(), Arg.Any())) + .Do(ci => ci.ArgAt(1)()); + + gitOperations.GetRemoteUri().Returns(remoteUri); + gitOperations.GetCurrentBranch().Returns("branch-1"); + gitOperations + .GetBranchesThatExistInRemote(Arg.Any()) + .Returns(["branch-1", "branch-3", "branch-5"]); + + gitOperations + .GetBranchesThatExistLocally(Arg.Any()) + .Returns(["branch-1", "branch-3", "branch-5"]); + + var stacks = new List( + [ + new("Stack1", remoteUri, "branch-1", ["branch-3", "branch-5"]), + new("Stack2", remoteUri, "branch-2", ["branch-4"]) + ]); + stackConfig.Load().Returns(stacks); + stackConfig + .WhenForAnyArgs(s => s.Save(Arg.Any>())) + .Do(ci => stacks = ci.ArgAt>(0)); + + inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); + inputProvider.Confirm(Questions.ConfirmStartCreatePullRequests(2)).Returns(true); + inputProvider.Confirm(Questions.ConfirmCreatePullRequests).Returns(true); + inputProvider.Text(Questions.PullRequestTitle("branch-3", "branch-1")).Returns("PR Title for branch-3"); + inputProvider.Text(Questions.PullRequestTitle("branch-5", "branch-3")).Returns("PR Title for branch-5"); + + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), true); + gitHubOperations + .CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, true) + .Returns(prForBranch3); + + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), true); + gitHubOperations + .CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, true) + .Returns(prForBranch5); + + // Act + await handler.Handle(new CreatePullRequestsCommandInputs(null, true)); + + // Assert + gitHubOperations.Received().CreatePullRequest("branch-3", "branch-1", "PR Title for branch-3", string.Empty, true); + gitHubOperations.Received().CreatePullRequest("branch-5", "branch-3", "PR Title for branch-5", string.Empty, true); + inputProvider.DidNotReceive().Confirm(Questions.CreatePullRequestsAsDrafts, false); } } diff --git a/src/Stack.Tests/Commands/PullRequests/OpenPullRequestsCommandHandlerTests.cs b/src/Stack.Tests/Commands/PullRequests/OpenPullRequestsCommandHandlerTests.cs index 8cd827d..02f2a5b 100644 --- a/src/Stack.Tests/Commands/PullRequests/OpenPullRequestsCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/PullRequests/OpenPullRequestsCommandHandlerTests.cs @@ -39,12 +39,12 @@ public async Task WhenThereAreMultiplePullRequestsInAStack_OpensAllPullRequests( inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") .Returns(prForBranch5); @@ -85,12 +85,12 @@ public async Task WhenThereAreSomePullRequestsInAStack_OpensAllPullRequests() inputProvider.Select(Questions.SelectStack, Arg.Any()).Returns("Stack1"); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Closed, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Closed, Some.HttpsUri(), false); // Act await handler.Handle(OpenPullRequestsCommandInputs.Empty); @@ -123,12 +123,12 @@ public async Task WhenStackNameIsProvided_OpensAllPullRequestsForTheStack() ]); stackConfig.Load().Returns(stacks); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") .Returns(prForBranch5); @@ -163,12 +163,12 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_OpensAllPullRequ ]); stackConfig.Load().Returns(stacks); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") .Returns(prForBranch5); @@ -204,12 +204,12 @@ public async Task WhenStackNameIsProvided_ButItStackDoesNotExist_Throws() ]); stackConfig.Load().Returns(stacks); - var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch3 = new GitHubPullRequest(1, "PR Title for branch-3", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") .Returns(prForBranch3); - var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri()); + var prForBranch5 = new GitHubPullRequest(2, "PR Title for branch-5", string.Empty, GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") .Returns(prForBranch5); diff --git a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs index bb1919a..6e88029 100644 --- a/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/StackStatusCommandHandlerTests.cs @@ -53,7 +53,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneHasAPullRequests_Retur .GetStatusOfRemoteBranch("branch-5", "branch-3") .Returns((1, 0)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") @@ -117,7 +117,7 @@ public async Task WhenStackNameIsProvided_DoesNotAskForStack_ReturnsStatus() .GetStatusOfRemoteBranch("branch-5", "branch-3") .Returns((1, 0)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") @@ -187,7 +187,7 @@ public async Task WhenAllStacksAreRequested_ReturnsStatusOfEachStack() .GetStatusOfRemoteBranch("branch-4", "branch-2") .Returns((3, 1)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") @@ -263,7 +263,7 @@ public async Task WhenAllStacksAreRequested_WithStacksInMultipleRepositories_Ret .GetStatusOfRemoteBranch("branch-4", "branch-2") .Returns((3, 1)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") @@ -360,7 +360,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem .GetStatusOfRemoteBranch("branch-5", "branch-1") .Returns((1, 0)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") @@ -421,7 +421,7 @@ public async Task WhenMultipleBranchesExistInAStack_AndOneNoLongerExistsOnTheRem .GetStatusOfRemoteBranch("branch-5", "branch-1") .Returns((1, 0)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-5") @@ -484,7 +484,7 @@ public async Task WhenOnlyOneStackExists_DoesNotAskForStackName_ReturnsStatus() .GetStatusOfRemoteBranch("branch-5", "branch-3") .Returns((1, 0)); - var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri()); + var pr = new GitHubPullRequest(1, "PR title", "PR body", GitHubPullRequestStates.Open, Some.HttpsUri(), false); gitHubOperations .GetPullRequest("branch-3") diff --git a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs index 80577eb..77144bb 100644 --- a/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs +++ b/src/Stack.Tests/Commands/Stack/UpdateStackCommandHandlerTests.cs @@ -119,7 +119,7 @@ public async Task WhenABranchInTheStackExistsOnTheRemote_ButThePullRequestIsMerg var branchesThatExistInRemote = new List(["branch-1", "branch-2", "branch-3"]); gitOperations.DoesRemoteBranchExist(Arg.Is(b => branchesThatExistInRemote.Contains(b))).Returns(true); - gitHubOperations.GetPullRequest("branch-2").Returns(new GitHubPullRequest(1, Some.Name(), Some.Name(), GitHubPullRequestStates.Merged, Some.HttpsUri())); + gitHubOperations.GetPullRequest("branch-2").Returns(new GitHubPullRequest(1, Some.Name(), Some.Name(), GitHubPullRequestStates.Merged, Some.HttpsUri(), false)); // Act await handler.Handle(new UpdateStackCommandInputs(null, false)); diff --git a/src/Stack/Commands/Helpers/Questions.cs b/src/Stack/Commands/Helpers/Questions.cs index e034a85..6ce18bc 100644 --- a/src/Stack/Commands/Helpers/Questions.cs +++ b/src/Stack/Commands/Helpers/Questions.cs @@ -22,4 +22,5 @@ public static class Questions public static string PullRequestTitle(string sourceBranch, string targetBranch) => $"Title for pull request from {sourceBranch.Branch()} -> {targetBranch.Branch()}:"; public const string PullRequestStackDescription = "Stack description for pull request:"; public const string OpenPullRequests = "Open the pull requests in the browser?"; + public const string CreatePullRequestsAsDrafts = "Create pull requests as drafts?"; } diff --git a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs index 04124f2..d529eac 100644 --- a/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs +++ b/src/Stack/Commands/PullRequests/CreatePullRequestsCommand.cs @@ -15,6 +15,10 @@ public class CreatePullRequestsCommandSettings : DryRunCommandSettingsBase [Description("The name of the stack to create pull requests for.")] [CommandOption("-n|--name")] public string? Name { get; init; } + + [Description("Create pull requests as drafts.")] + [CommandOption("--draft")] + public bool? Draft { get; init; } } public class CreatePullRequestsCommand : AsyncCommand @@ -30,15 +34,15 @@ public override async Task ExecuteAsync(CommandContext context, CreatePullR new GitHubOperations(console, settings.GetGitHubOperationSettings()), new StackConfig()); - await handler.Handle(new CreatePullRequestsCommandInputs(settings.Name)); + await handler.Handle(new CreatePullRequestsCommandInputs(settings.Name, settings.Draft)); return 0; } } -public record CreatePullRequestsCommandInputs(string? StackName) +public record CreatePullRequestsCommandInputs(string? StackName, bool? Draft) { - public static CreatePullRequestsCommandInputs Empty => new((string?)null); + public static CreatePullRequestsCommandInputs Empty => new(null, null); } public record CreatePullRequestsCommandResponse(); @@ -117,7 +121,18 @@ public async Task Handle(CreatePullRequestsCo if (inputProvider.Confirm(Questions.ConfirmCreatePullRequests)) { - CreatePullRequests(outputProvider, gitOperations, gitHubOperations, status, pullRequestCreateActions); + var draft = inputs.Draft ?? false; + + if (inputs.Draft is null) + { + draft = inputProvider.Confirm(Questions.CreatePullRequestsAsDrafts, false); + } + else if (draft) + { + outputProvider.Information("Creating pull requests as drafts."); + } + + CreatePullRequests(outputProvider, gitOperations, gitHubOperations, status, pullRequestCreateActions, draft); var pullRequestsInStack = status.Branches.Values .Where(branch => branch.HasPullRequest) @@ -204,7 +219,13 @@ private static void UpdatePullRequestStackDescriptions(IInputProvider inputProvi } } - private static void CreatePullRequests(IOutputProvider outputProvider, IGitOperations gitOperations, IGitHubOperations gitHubOperations, StackStatus status, List pullRequestCreateActions) + private static void CreatePullRequests( + IOutputProvider outputProvider, + IGitOperations gitOperations, + IGitHubOperations gitHubOperations, + StackStatus status, + List pullRequestCreateActions, + bool draft) { var pullRequestTemplatePath = Path.Join(gitOperations.GetRootOfRepository(), ".github", "pull_request_template.md"); var body = string.Empty; @@ -220,7 +241,7 @@ private static void CreatePullRequests(IOutputProvider outputProvider, IGitOpera { var branchDetail = status.Branches[action.HeadBranch]; outputProvider.Information($"Creating pull request for branch {action.HeadBranch.Branch()} to {action.BaseBranch.Branch()}"); - var pullRequest = gitHubOperations.CreatePullRequest(action.HeadBranch, action.BaseBranch, action.Title!, body); + var pullRequest = gitHubOperations.CreatePullRequest(action.HeadBranch, action.BaseBranch, action.Title!, body, draft); if (pullRequest is not null) { diff --git a/src/Stack/Git/GitHubOperations.cs b/src/Stack/Git/GitHubOperations.cs index 9bf757c..9129c4a 100644 --- a/src/Stack/Git/GitHubOperations.cs +++ b/src/Stack/Git/GitHubOperations.cs @@ -16,12 +16,15 @@ public static class GitHubPullRequestStates public const string Merged = "MERGED"; } -public record GitHubPullRequest(int Number, string Title, string Body, string State, Uri Url); +public record GitHubPullRequest(int Number, string Title, string Body, string State, Uri Url, bool IsDraft); public static class GitHubPullRequestExtensionMethods { public static Color GetPullRequestColor(this GitHubPullRequest pullRequest) { + if (pullRequest.IsDraft) + return Color.Grey; + return pullRequest.State switch { GitHubPullRequestStates.Open => Color.Green, @@ -40,7 +43,7 @@ public static string GetPullRequestDisplay(this GitHubPullRequest pullRequest) public interface IGitHubOperations { GitHubPullRequest? GetPullRequest(string branch); - GitHubPullRequest? CreatePullRequest(string headBranch, string baseBranch, string title, string body); + GitHubPullRequest? CreatePullRequest(string headBranch, string baseBranch, string title, string body, bool draft); void EditPullRequest(int number, string body); void OpenPullRequest(GitHubPullRequest pullRequest); } @@ -49,16 +52,23 @@ public class GitHubOperations(IAnsiConsole console, GitHubOperationSettings sett { public GitHubPullRequest? GetPullRequest(string branch) { - var output = ExecuteGitHubCommandAndReturnOutput($"pr list --json title,number,body,state,url --head {branch} --state all"); + var output = ExecuteGitHubCommandAndReturnOutput($"pr list --json title,number,body,state,url,isDraft --head {branch} --state all"); var pullRequests = System.Text.Json.JsonSerializer.Deserialize>(output, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web))!; return pullRequests.FirstOrDefault(); } - public GitHubPullRequest? CreatePullRequest(string headBranch, string baseBranch, string title, string body) + public GitHubPullRequest? CreatePullRequest(string headBranch, string baseBranch, string title, string body, bool draft) { - ExecuteGitHubCommand($"pr create --title \"{title}\" --body \"{body}\" --base {baseBranch} --head {headBranch}"); + var command = $"pr create --title \"{title}\" --body \"{body}\" --base {baseBranch} --head {headBranch}"; + + if (draft) + { + command += " --draft"; + } + + ExecuteGitHubCommand(command); if (settings.DryRun) { diff --git a/src/Stack/Infrastructure/ConsoleInputProvider.cs b/src/Stack/Infrastructure/ConsoleInputProvider.cs index 8cc1cb6..f769108 100644 --- a/src/Stack/Infrastructure/ConsoleInputProvider.cs +++ b/src/Stack/Infrastructure/ConsoleInputProvider.cs @@ -10,7 +10,7 @@ public interface IInputProvider string Select(string prompt, string[] choices); T Select(string prompt, T[] choices, Func? converter = null) where T : notnull; T SelectGrouped(string prompt, ChoiceGroup[] choices, Func? converter = null) where T : notnull; - bool Confirm(string prompt); + bool Confirm(string prompt, bool defaultValue = true); } public class ConsoleInputProvider(IAnsiConsole console) : IInputProvider @@ -65,7 +65,15 @@ public T SelectGrouped(string prompt, ChoiceGroup[] groups, Func console.Prompt(new ConfirmationPrompt(prompt)); + public bool Confirm(string prompt, bool defaultValue = true) + { + var confirmationPrompt = new ConfirmationPrompt(prompt) + { + DefaultValue = defaultValue + }; + + return console.Prompt(confirmationPrompt); + } }