Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(update-prs): push new commits #156

Merged
merged 5 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,23 +260,22 @@ redacted/redacted OPEN REVIEW_REQUIRE

Use the `update-prs` command to update PRs after creating them. Current options for updating PRs are:

##### Update PR titles and descriptions with `--amend-description`
- `--push` to push new commits
- `--amend-description` to update PR titles and descriptions
- `--close` to close PRs

```turbolift update-prs --amend-description [--yes]```
If the flag `--yes` is not passed with an `update-prs` command, a confirmation prompt will be presented.
As always, use the `--repos` flag to specify an alternative repo file to the default `repos.txt`.

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.

As with creating PRs, if you need Turbolift to read these values from an alternative file, use the flag `--description PATH_TO_FILE`.

```turblift update-prs --amend-description --description prDescriptionFile1.md```

##### Close PRs with the `--close` flag
##### Examples

```turbolift update-prs --close [--yes]```
```turbolift update-prs --push [--yes]```
```turbolift update-prs --amend-description [--description prDescriptionFile1.md] [--yes]```

If the flag `--yes` is not passed with an `update-prs` command, a confirmation prompt will be presented to the user.

As always, use the `--repos` flag to specify an alternative repo file to repos.txt.
Note that when updating PR descriptions, as when creating PRs, the `--description` flag can be used to specify an
alternative description file to the default `README.md`.
The updated title is taken from the first line of the file, and the updated description is the remainder of the file contents.

## Status: Preview

Expand Down
66 changes: 61 additions & 5 deletions cmd/updateprs/updateprs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package updateprs
import (
"errors"
"fmt"
"github.com/skyscanner/turbolift/internal/git"
"os"

"github.com/spf13/cobra"
Expand All @@ -31,12 +32,14 @@ import (

var (
gh github.GitHub = github.NewRealGitHub()
g git.Git = git.NewRealGit()
p prompt.Prompt = prompt.NewRealPrompt()
)

var (
closeFlag bool
updateDescriptionFlag bool
pushFlag bool
yesFlag bool
repoFile string
prDescriptionFile string
Expand All @@ -51,6 +54,7 @@ func NewUpdatePRsCmd() *cobra.Command {

cmd.Flags().BoolVar(&closeFlag, "close", false, "Close all generated PRs")
cmd.Flags().BoolVar(&updateDescriptionFlag, "amend-description", false, "Update PR titles and descriptions")
cmd.Flags().BoolVar(&pushFlag, "push", false, "Push new commits")
cmd.Flags().BoolVar(&yesFlag, "yes", false, "Skips the confirmation prompt")
cmd.Flags().StringVar(&repoFile, "repos", "repos.txt", "A file containing a list of repositories to clone.")
cmd.Flags().StringVar(&prDescriptionFile, "description", "README.md", "A file containing the title and description for the PRs.")
Expand All @@ -71,8 +75,8 @@ func onlyOne(args ...bool) bool {
return b[true] == 1
}

func validateFlags(closeFlag bool, updateDescriptionFlag bool) error {
if !onlyOne(closeFlag, updateDescriptionFlag) {
func validateFlags(closeFlag bool, updateDescriptionFlag bool, pushFlag bool) error {
if !onlyOne(closeFlag, updateDescriptionFlag, pushFlag) {
return errors.New("update-prs needs one and only one action flag")
}
return nil
Expand All @@ -81,7 +85,7 @@ func validateFlags(closeFlag bool, updateDescriptionFlag bool) error {
// we keep the args as one of the subfunctions might need it one day.
func run(c *cobra.Command, args []string) {
logger := logging.NewLogger(c)
if err := validateFlags(closeFlag, updateDescriptionFlag); err != nil {
if err := validateFlags(closeFlag, updateDescriptionFlag, pushFlag); err != nil {
logger.Errorf("Error while parsing the flags: %v", err)
return
}
Expand All @@ -90,6 +94,8 @@ func run(c *cobra.Command, args []string) {
runClose(c, args)
} else if updateDescriptionFlag {
runUpdatePrDescription(c, args)
} else if pushFlag {
runPush(c, args)
}
}

Expand All @@ -109,7 +115,7 @@ func runClose(c *cobra.Command, _ []string) {
// Prompting for confirmation
if !yesFlag {
// TODO: add the number of PRs that it will actually close
if !p.AskConfirm(fmt.Sprintf("Close %s campaign PRs for all repos in %s?", dir.Name, repoFile)) {
if !p.AskConfirm(fmt.Sprintf("Close %s campaign PRs for all repos in %s", dir.Name, repoFile)) {
rnorth marked this conversation as resolved.
Show resolved Hide resolved
return
}
}
Expand Down Expand Up @@ -166,7 +172,7 @@ func runUpdatePrDescription(c *cobra.Command, _ []string) {

// Prompting for confirmation
if !yesFlag {
if !p.AskConfirm(fmt.Sprintf("Update %s campaign PR titles and descriptions for all repos listed in %s?", dir.Name, repoFile)) {
if !p.AskConfirm(fmt.Sprintf("Update %s campaign PR titles and descriptions for all repos listed in %s", dir.Name, repoFile)) {
return
}
}
Expand Down Expand Up @@ -206,3 +212,53 @@ func runUpdatePrDescription(c *cobra.Command, _ []string) {
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"))
}
}

func runPush(c *cobra.Command, _ []string) {
logger := logging.NewLogger(c)

readCampaignActivity := logger.StartActivity("Reading campaign data (%s)", repoFile)
options := campaign.NewCampaignOptions()
options.RepoFilename = repoFile
dir, err := campaign.OpenCampaign(options)
if err != nil {
readCampaignActivity.EndWithFailure(err)
return
}
readCampaignActivity.EndWithSuccess()

// Prompting for confirmation
if !yesFlag {
if !p.AskConfirm(fmt.Sprintf("Push new commits to %s campaign PRs for all repos in %s", dir.Name, repoFile)) {
return
}
}

doneCount := 0
skippedCount := 0
errorCount := 0

for _, repo := range dir.Repos {
pushActivity := logger.StartActivity("Pushing changes in %s to origin", repo.FullRepoName)
// skip if the working copy does not exist
if _, err = os.Stat(repo.FullRepoPath()); os.IsNotExist(err) {
pushActivity.EndWithWarningf("Directory %s does not exist - has it been cloned?", repo.FullRepoPath())
skippedCount++
continue
}

err := g.Push(pushActivity.Writer(), repo.FullRepoPath(), "origin", dir.Name)
if err != nil {
pushActivity.EndWithFailure(err)
errorCount++
continue
}
pushActivity.EndWithSuccess()
doneCount++
}

if errorCount == 0 {
logger.Successf("turbolift update-prs completed %s(%s, %s)\n", colors.Normal(), colors.Green(doneCount, " OK"), colors.Yellow(skippedCount, " skipped"))
} else {
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"))
}
}
87 changes: 87 additions & 0 deletions cmd/updateprs/updateprs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package updateprs

import (
"bytes"
"github.com/skyscanner/turbolift/internal/git"
"path/filepath"
"testing"

Expand Down Expand Up @@ -165,6 +166,66 @@ func TestItDoesNotUpdateDescriptionsIfNotConfirmed(t *testing.T) {
fakeGitHub.AssertCalledWith(t, [][]string{})
}

func TestItPushesNewCommits(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

tempDir := testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runPushCommandAuto()
assert.NoError(t, err)
assert.Contains(t, out, "Pushing changes in org/repo1 to origin")
assert.Contains(t, out, "Pushing changes in org/repo2 to origin")
assert.Contains(t, out, "turbolift update-prs completed")
assert.Contains(t, out, "2 OK, 0 skipped")

fakeGit.AssertCalledWith(t, [][]string{
{"push", "work/org/repo1", filepath.Base(tempDir)},
{"push", "work/org/repo2", filepath.Base(tempDir)},
})
}

func TestItLogsPushErrorsButContinuesToTryAll(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysFailsFakeGit()
g = fakeGit

tempDir := testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runPushCommandAuto()
assert.NoError(t, err)
assert.Contains(t, out, "Pushing changes in org/repo1 to origin")
assert.Contains(t, out, "Pushing changes in org/repo2 to origin")
assert.Contains(t, out, "turbolift update-prs completed with errors")
assert.Contains(t, out, "2 errored")

fakeGit.AssertCalledWith(t, [][]string{
{"push", "work/org/repo1", filepath.Base(tempDir)},
{"push", "work/org/repo2", filepath.Base(tempDir)},
})
}

func TestItDoesNotPushIfNotConfirmed(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakePrompt := prompt.NewFakePromptNo()
p = fakePrompt

_ = testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runPushCommandConfirm()
assert.NoError(t, err)
assert.NotContains(t, out, "Pushing changes in org/repo1 to origin")
assert.NotContains(t, out, "Pushing changes in org/repo2 to origin")
assert.NotContains(t, out, "turbolift update-prs completed")
assert.NotContains(t, out, "2 OK")

fakeGitHub.AssertCalledWith(t, [][]string{})
}

func runCloseCommandAuto() (string, error) {
cmd := NewUpdatePRsCmd()
closeFlag = true
Expand Down Expand Up @@ -217,3 +278,29 @@ func runUpdateDescriptionCommandConfirm() (string, error) {
}
return outBuffer.String(), nil
}

func runPushCommandAuto() (string, error) {
cmd := NewUpdatePRsCmd()
pushFlag = true
yesFlag = true
outBuffer := bytes.NewBufferString("")
cmd.SetOut(outBuffer)
err := cmd.Execute()
if err != nil {
return outBuffer.String(), err
}
return outBuffer.String(), nil
}

func runPushCommandConfirm() (string, error) {
cmd := NewUpdatePRsCmd()
pushFlag = true
yesFlag = false
outBuffer := bytes.NewBufferString("")
cmd.SetOut(outBuffer)
err := cmd.Execute()
if err != nil {
return outBuffer.String(), err
}
return outBuffer.String(), nil
}
Loading