Skip to content

Commit 9c33426

Browse files
Dan7-7-7rnorth
andauthored
Update PR descriptions (#117)
* add update PR description * add github tests * tests for updating via default readme * custom description file, instructions * Update README.md Co-authored-by: Richard North <[email protected]> * Update README.md Co-authored-by: Richard North <[email protected]> * test for default description file --------- Co-authored-by: Danny Ranson <[email protected]> Co-authored-by: Richard North <[email protected]>
1 parent 36daf83 commit 9c33426

File tree

8 files changed

+236
-13
lines changed

8 files changed

+236
-13
lines changed

README.md

+17-3
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,27 @@ redacted/redacted OPEN REVIEW_REQUIRE
217217
...
218218
```
219219

220-
#### Closing all PRs
220+
#### Updating PRs
221221

222-
To close all PRs currently opened under the campaign, there is a `--close` flag:
222+
Use the `update-prs` command to update PRs after creating them. Current options for updating PRs are:
223+
224+
##### Update PR titles and descriptions with `--amend-description`
225+
226+
```turbolift update-prs --amend-description [--yes]```
227+
228+
By default, turbolift will read a revised PR Title and Description from `README.md`. The title is taken from the first heading line, and the description is the remainder of the file contents.
229+
230+
As with creating PRs, if you need Turbolift to read these values from an alternative file, use the flag `--description PATH_TO_FILE`.
231+
232+
```turblift update-prs --amend-description --description prDescriptionFile1.md```
233+
234+
##### Close PRs with the `--close` flag
223235

224236
```turbolift update-prs --close [--yes]```
225237

226-
If the flag `--yes` is not present, a confirmation prompt will be presented to the user.
238+
If the flag `--yes` is not passed with an `update-prs` command, a confirmation prompt will be presented to the user.
239+
240+
As always, use the `--repos` flag to specify an alternative repo file to repos.txt.
227241

228242
## Status: Preview
229243

cmd/updateprs/updateprs.go

+70-8
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ var (
3535
)
3636

3737
var (
38-
closeFlag bool
39-
yesFlag bool
40-
repoFile string
38+
closeFlag bool
39+
updateDescriptionFlag bool
40+
yesFlag bool
41+
repoFile string
42+
prDescriptionFile string
4143
)
4244

4345
func NewUpdatePRsCmd() *cobra.Command {
@@ -48,8 +50,10 @@ func NewUpdatePRsCmd() *cobra.Command {
4850
}
4951

5052
cmd.Flags().BoolVar(&closeFlag, "close", false, "Close all generated PRs")
53+
cmd.Flags().BoolVar(&updateDescriptionFlag, "amend-description", false, "Update PR titles and descriptions")
5154
cmd.Flags().BoolVar(&yesFlag, "yes", false, "Skips the confirmation prompt")
5255
cmd.Flags().StringVar(&repoFile, "repos", "repos.txt", "A file containing a list of repositories to clone.")
56+
cmd.Flags().StringVar(&prDescriptionFile, "description", "README.md", "A file containing the title and description for the PRs.")
5357

5458
return cmd
5559
}
@@ -67,9 +71,8 @@ func onlyOne(args ...bool) bool {
6771
return b[true] == 1
6872
}
6973

70-
func validateFlags(closeFlag bool) error {
71-
// only option at the moment is `close`
72-
if !onlyOne(closeFlag) {
74+
func validateFlags(closeFlag bool, updateDescriptionFlag bool) error {
75+
if !onlyOne(closeFlag, updateDescriptionFlag) {
7376
return errors.New("update-prs needs one and only one action flag")
7477
}
7578
return nil
@@ -78,13 +81,15 @@ func validateFlags(closeFlag bool) error {
7881
// we keep the args as one of the subfunctions might need it one day.
7982
func run(c *cobra.Command, args []string) {
8083
logger := logging.NewLogger(c)
81-
if err := validateFlags(closeFlag); err != nil {
84+
if err := validateFlags(closeFlag, updateDescriptionFlag); err != nil {
8285
logger.Errorf("Error while parsing the flags: %v", err)
8386
return
8487
}
8588

8689
if closeFlag {
8790
runClose(c, args)
91+
} else if updateDescriptionFlag {
92+
runUpdatePrDescription(c, args)
8893
}
8994
}
9095

@@ -104,7 +109,7 @@ func runClose(c *cobra.Command, _ []string) {
104109
// Prompting for confirmation
105110
if !yesFlag {
106111
// TODO: add the number of PRs that it will actually close
107-
if !p.AskConfirm(fmt.Sprintf("Close all PRs from the %s campaign?", dir.Name)) {
112+
if !p.AskConfirm(fmt.Sprintf("Close %s campaign PRs for all repos in %s?", dir.Name, repoFile)) {
108113
return
109114
}
110115
}
@@ -144,3 +149,60 @@ func runClose(c *cobra.Command, _ []string) {
144149
logger.Warnf("turbolift update-prs completed with %s %s(%s, %s, %s)\n", colors.Red("errors"), colors.Normal(), colors.Green(doneCount, " OK"), colors.Yellow(skippedCount, " skipped"), colors.Red(errorCount, " errored"))
145150
}
146151
}
152+
153+
func runUpdatePrDescription(c *cobra.Command, _ []string) {
154+
logger := logging.NewLogger(c)
155+
156+
readCampaignActivity := logger.StartActivity("Reading campaign data (%s)", repoFile)
157+
options := campaign.NewCampaignOptions()
158+
options.RepoFilename = repoFile
159+
options.PrDescriptionFilename = prDescriptionFile
160+
dir, err := campaign.OpenCampaign(options)
161+
if err != nil {
162+
readCampaignActivity.EndWithFailure(err)
163+
return
164+
}
165+
readCampaignActivity.EndWithSuccess()
166+
167+
// Prompting for confirmation
168+
if !yesFlag {
169+
if !p.AskConfirm(fmt.Sprintf("Update %s campaign PR titles and descriptions for all repos listed in %s?", dir.Name, repoFile)) {
170+
return
171+
}
172+
}
173+
174+
doneCount := 0
175+
skippedCount := 0
176+
errorCount := 0
177+
178+
for _, repo := range dir.Repos {
179+
updatePrActivity := logger.StartActivity("Updating PR description in %s", repo.FullRepoName)
180+
181+
// skip if the working copy does not exist
182+
if _, err = os.Stat(repo.FullRepoPath()); os.IsNotExist(err) {
183+
updatePrActivity.EndWithWarningf("Directory %s does not exist - has it been cloned?", repo.FullRepoPath())
184+
skippedCount++
185+
continue
186+
}
187+
188+
err = gh.UpdatePRDescription(updatePrActivity.Writer(), repo.FullRepoPath(), dir.PrTitle, dir.PrBody)
189+
if err != nil {
190+
if _, ok := err.(*github.NoPRFoundError); ok {
191+
updatePrActivity.EndWithWarning(err)
192+
skippedCount++
193+
} else {
194+
updatePrActivity.EndWithFailure(err)
195+
errorCount++
196+
}
197+
} else {
198+
updatePrActivity.EndWithSuccess()
199+
doneCount++
200+
}
201+
}
202+
203+
if errorCount == 0 {
204+
logger.Successf("turbolift update-prs completed %s(%s, %s)\n", colors.Normal(), colors.Green(doneCount, " OK"), colors.Yellow(skippedCount, " skipped"))
205+
} else {
206+
logger.Warnf("turbolift update-prs completed with %s %s(%s, %s, %s)\n", colors.Red("errors"), colors.Normal(), colors.Green(doneCount, " OK"), colors.Yellow(skippedCount, " skipped"), colors.Red(errorCount, " errored"))
207+
}
208+
}

cmd/updateprs/updateprs_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,83 @@ func TestItDoesNotClosePRsIfNotConfirmed(t *testing.T) {
8888
fakeGitHub.AssertCalledWith(t, [][]string{})
8989
}
9090

91+
func TestItLogsUpdateDescriptionErrorsButContinuesToTryAll(t *testing.T) {
92+
fakeGitHub := github.NewAlwaysFailsFakeGitHub()
93+
gh = fakeGitHub
94+
95+
testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
96+
97+
out, err := runUpdateDescriptionCommandAuto("README.md")
98+
assert.NoError(t, err)
99+
assert.Contains(t, out, "Updating PR description in org/repo1")
100+
assert.Contains(t, out, "Updating PR description in org/repo2")
101+
assert.Contains(t, out, "turbolift update-prs completed with errors")
102+
assert.Contains(t, out, "2 errored")
103+
104+
fakeGitHub.AssertCalledWith(t, [][]string{
105+
{"work/org/repo1", "PR title", "PR body"},
106+
{"work/org/repo2", "PR title", "PR body"},
107+
})
108+
}
109+
110+
func TestItUpdatesDescriptionsSuccessfully(t *testing.T) {
111+
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
112+
gh = fakeGitHub
113+
114+
testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
115+
testsupport.CreateOrUpdatePrDescriptionFile("README.md", "Updated PR title", "Updated PR body")
116+
117+
out, err := runUpdateDescriptionCommandAuto("README.md")
118+
assert.NoError(t, err)
119+
assert.Contains(t, out, "Updating PR description in org/repo1")
120+
assert.Contains(t, out, "Updating PR description in org/repo2")
121+
assert.Contains(t, out, "turbolift update-prs completed")
122+
assert.Contains(t, out, "2 OK, 0 skipped")
123+
124+
fakeGitHub.AssertCalledWith(t, [][]string{
125+
{"work/org/repo1", "Updated PR title", "Updated PR body"},
126+
{"work/org/repo2", "Updated PR title", "Updated PR body"},
127+
})
128+
}
129+
130+
func TestItUpdatesDescriptionsFromAlternativeFile(t *testing.T) {
131+
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
132+
gh = fakeGitHub
133+
134+
testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
135+
testsupport.CreateOrUpdatePrDescriptionFile("custom.md", "custom PR title", "custom PR body")
136+
137+
out, err := runUpdateDescriptionCommandAuto("custom.md")
138+
assert.NoError(t, err)
139+
assert.Contains(t, out, "Updating PR description in org/repo1")
140+
assert.Contains(t, out, "Updating PR description in org/repo2")
141+
assert.Contains(t, out, "turbolift update-prs completed")
142+
assert.Contains(t, out, "2 OK, 0 skipped")
143+
144+
fakeGitHub.AssertCalledWith(t, [][]string{
145+
{"work/org/repo1", "custom PR title", "custom PR body"},
146+
{"work/org/repo2", "custom PR title", "custom PR body"},
147+
})
148+
}
149+
150+
func TestItDoesNotUpdateDescriptionsIfNotConfirmed(t *testing.T) {
151+
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
152+
gh = fakeGitHub
153+
fakePrompt := prompt.NewFakePromptNo()
154+
p = fakePrompt
155+
156+
testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
157+
158+
out, err := runUpdateDescriptionCommandConfirm()
159+
assert.NoError(t, err)
160+
assert.NotContains(t, out, "Updating PR description in org/repo1")
161+
assert.NotContains(t, out, "Updating PR description in org/repo2")
162+
assert.NotContains(t, out, "turbolift update-prs completed")
163+
assert.NotContains(t, out, "2 OK")
164+
165+
fakeGitHub.AssertCalledWith(t, [][]string{})
166+
}
167+
91168
func runCloseCommandAuto() (string, error) {
92169
cmd := NewUpdatePRsCmd()
93170
closeFlag = true
@@ -113,3 +190,30 @@ func runCloseCommandConfirm() (string, error) {
113190
}
114191
return outBuffer.String(), nil
115192
}
193+
194+
func runUpdateDescriptionCommandAuto(descriptionFile string) (string, error) {
195+
cmd := NewUpdatePRsCmd()
196+
updateDescriptionFlag = true
197+
yesFlag = true
198+
prDescriptionFile = descriptionFile
199+
outBuffer := bytes.NewBufferString("")
200+
cmd.SetOut(outBuffer)
201+
err := cmd.Execute()
202+
if err != nil {
203+
return outBuffer.String(), err
204+
}
205+
return outBuffer.String(), nil
206+
}
207+
208+
func runUpdateDescriptionCommandConfirm() (string, error) {
209+
cmd := NewUpdatePRsCmd()
210+
updateDescriptionFlag = true
211+
yesFlag = false
212+
outBuffer := bytes.NewBufferString("")
213+
cmd.SetOut(outBuffer)
214+
err := cmd.Execute()
215+
if err != nil {
216+
return outBuffer.String(), err
217+
}
218+
return outBuffer.String(), nil
219+
}

internal/campaign/campaign_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func TestItShouldErrorWhenRepoFileIsEmpty(t *testing.T) {
227227
func TestItShouldAcceptADifferentPrDescriptionFile(t *testing.T) {
228228
testsupport.PrepareTempCampaign(false)
229229

230-
testsupport.CreateAnotherPrDescriptionFile("newprdescription.txt", "new PR title", "new PR body")
230+
testsupport.CreateOrUpdatePrDescriptionFile("newprdescription.txt", "new PR title", "new PR body")
231231
options := NewCampaignOptions()
232232
options.PrDescriptionFilename = "newprdescription.txt"
233233
campaign, err := OpenCampaign(options)

internal/github/fake_github.go

+8
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ func (f *FakeGitHub) GetDefaultBranchName(_ io.Writer, workingDir string, fullRe
7272
return "main", err
7373
}
7474

75+
func (f *FakeGitHub) UpdatePRDescription(_ io.Writer, workingDir string, title string, body string) error {
76+
args := []string{workingDir, title, body}
77+
f.calls = append(f.calls, args)
78+
_, err := f.handler(UpdatePRDescription, args)
79+
return err
80+
}
81+
7582
func (f *FakeGitHub) AssertCalledWith(t *testing.T, expected [][]string) {
7683
assert.Equal(t, expected, f.calls)
7784
}
@@ -136,4 +143,5 @@ const (
136143
CreatePullRequest
137144
ClosePullRequest
138145
GetDefaultBranchName
146+
UpdatePRDescription
139147
)

internal/github/github.go

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type GitHub interface {
3939
Clone(output io.Writer, workingDir string, fullRepoName string) error
4040
CreatePullRequest(output io.Writer, workingDir string, metadata PullRequest) (didCreate bool, err error)
4141
ClosePullRequest(output io.Writer, workingDir string, branchName string) error
42+
UpdatePRDescription(output io.Writer, workingDir string, title string, body string) error
4243
GetPR(output io.Writer, workingDir string, branchName string) (*PrStatus, error)
4344
GetDefaultBranchName(output io.Writer, workingDir string, fullRepoName string) (string, error)
4445
}
@@ -88,6 +89,10 @@ func (r *RealGitHub) ClosePullRequest(output io.Writer, workingDir string, branc
8889
return execInstance.Execute(output, workingDir, "gh", "pr", "close", fmt.Sprint(pr.Number))
8990
}
9091

92+
func (r *RealGitHub) UpdatePRDescription(output io.Writer, workingDir string, title string, body string) error {
93+
return execInstance.Execute(output, workingDir, "gh", "pr", "edit", "--title", title, "--body", body)
94+
}
95+
9196
func (r *RealGitHub) GetDefaultBranchName(output io.Writer, workingDir string, fullRepoName string) (string, error) {
9297
defaultBranch, err := execInstance.ExecuteAndCapture(output, workingDir, "gh", "repo", "view", fullRepoName, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name")
9398
return strings.Trim(defaultBranch, "\n"), err

internal/github/github_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,30 @@ func TestItReturnsNilErrorOnSuccessfulGetDefaultBranchName(t *testing.T) {
151151
})
152152
}
153153

154+
func TestItReturnsErrorOnFailedUpdatePrDescription(t *testing.T) {
155+
fakeExecutor := executor.NewAlwaysFailsFakeExecutor()
156+
execInstance = fakeExecutor
157+
158+
_, err := runUpdatePrDescriptionAndCaptureOutput()
159+
assert.Error(t, err)
160+
161+
fakeExecutor.AssertCalledWith(t, [][]string{
162+
{"work/org/repo1", "gh", "pr", "edit", "--title", "new title", "--body", "new body"},
163+
})
164+
}
165+
166+
func TestItReturnsNilErrorOnSuccessfulUpdatePrDescription(t *testing.T) {
167+
fakeExecutor := executor.NewAlwaysSucceedsFakeExecutor()
168+
execInstance = fakeExecutor
169+
170+
_, err := runUpdatePrDescriptionAndCaptureOutput()
171+
assert.NoError(t, err)
172+
173+
fakeExecutor.AssertCalledWith(t, [][]string{
174+
{"work/org/repo1", "gh", "pr", "edit", "--title", "new title", "--body", "new body"},
175+
})
176+
}
177+
154178
func runForkAndCloneAndCaptureOutput() (string, error) {
155179
sb := strings.Builder{}
156180
err := NewRealGitHub().ForkAndClone(&sb, "work/org", "org/repo1")
@@ -193,3 +217,9 @@ func runGetDefaultBranchNameAndCaptureOutput() (string, string, error) {
193217
defaultBranchName, err := NewRealGitHub().GetDefaultBranchName(&sb, "work/org1/repo1", "org1/repo1")
194218
return defaultBranchName, sb.String(), err
195219
}
220+
221+
func runUpdatePrDescriptionAndCaptureOutput() (string, error) {
222+
sb := strings.Builder{}
223+
err := NewRealGitHub().UpdatePRDescription(&sb, "work/org/repo1", "new title", "new body")
224+
return sb.String(), err
225+
}

internal/testsupport/testsupport.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func CreateAnotherRepoFile(filename string, repos ...string) {
7474
}
7575
}
7676

77-
func CreateAnotherPrDescriptionFile(filename string, prTitle string, prBody string) {
77+
func CreateOrUpdatePrDescriptionFile(filename string, prTitle string, prBody string) {
7878
prDescription := fmt.Sprintf("# %s\n%s", prTitle, prBody)
7979
err := os.WriteFile(filename, []byte(prDescription), os.ModePerm|0o644)
8080
if err != nil {

0 commit comments

Comments
 (0)