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

Adds update-prs with close feature #72

Merged
merged 4 commits into from
Jun 27, 2022
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
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ It's dumb but it works. It doesn't scale well, though. Manually cloning and rais

Turbolift essentially automates the boring parts and stays out of the way when it comes to actually making the changes. It automates cloning, committing, and raising PRs en-masse, so that you can focus on the substance of the change.

> Historical note: Turbolift supersedes an internal system at Skyscanner named Codelift. Codelift was a centralised batch system, requiring changes to be scripted upfront and run overnight. While Codelift was useful, we have found that a decentralised, interactive tool is far easier and quicker for people to use in practice.
> Historical note: Turbolift supersedes an internal system at Skyscanner named Codelift. Codelift was a centralised batch system, requiring changes to be scripted upfront and run overnight. While Codelift was useful, we have found that a decentralised, interactive tool is far easier and quicker for people to use in practice.

## Demo

Expand All @@ -24,7 +24,7 @@ This demo shows Turbolift in action, creating a simple PR in two repositories:

Pre-built binary archives can be downloaded from the [Releases](https://github.com/Skyscanner/turbolift/releases) page.

* Download, extract the archive, and move it onto your `PATH`.
* Download, extract the archive, and move it onto your `PATH`.
* Note that the binaries are not currently notarized for MacOS Gatekeeper. If errors are displayed, use `xattr -c PATH_TO_TURBOLIFT_BINARY` to un-quarantine the binary, or right-click on the binary in Finder and choose 'Open' once to allow future execution. Distribution will be improved under https://github.com/Skyscanner/turbolift/issues/43.

You must also have the GitHub CLI, `gh`, installed:
Expand Down Expand Up @@ -99,8 +99,8 @@ You may wish to skip the fork and work on the upstream repository branch directl

### Making changes

Now, make changes to the checked-out repos under the `work` directory.
You can do this manually using an editor, using `sed` and similar commands, or using [`codemod`](https://github.com/facebook/codemod)/[`comby`](https://comby.dev/), etc.
Now, make changes to the checked-out repos under the `work` directory.
You can do this manually using an editor, using `sed` and similar commands, or using [`codemod`](https://github.com/facebook/codemod)/[`comby`](https://comby.dev/), etc.

**You are free to use any tools that help get the job done.**

Expand All @@ -123,8 +123,8 @@ When ready to commit changes across all repos, run:

```turbolift commit --message "Your commit message"```

This command is a no-op on any repos that do not have any changes.
Note that the commit will be run with the `--all` flag set, meaning that it is not necessary to stage changes using `git add/rm` for changed files.
This command is a no-op on any repos that do not have any changes.
Note that the commit will be run with the `--all` flag set, meaning that it is not necessary to stage changes using `git add/rm` for changed files.
Newly created files _will_ still need to be staged using `git add`.

Repeat if you want to make multiple commits.
Expand Down Expand Up @@ -152,6 +152,16 @@ For example:
turbolift foreach gh pr close --delete-branch YOUR_USERNAME:CAMPAIGN_NAME
```

### Updating PRs

#### Closing all PRs

To close all PRs currently opened under the campaign, there is a `--close` flag:

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

If the flag `--yes` is not present, a confirmation prompt will be presented to the user.

## Status: Preview

This tool is fully functional, but we have improvements that we'd like to make, and would appreciate feedback.
Expand Down
4 changes: 1 addition & 3 deletions cmd/clone/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestItAbortsIfReposFileNotFound(t *testing.T) {
fakeGit := git.NewAlwaysFailsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(false)
_ = testsupport.PrepareTempCampaign(false)
err := os.Remove("repos.txt")
if err != nil {
panic(err)
Expand Down Expand Up @@ -113,7 +113,6 @@ func TestItLogsCheckoutErrorsButContinuesToTryAll(t *testing.T) {
{"checkout", "work/org/repo1", testsupport.Pwd()},
{"checkout", "work/org/repo2", testsupport.Pwd()},
})

}

func TestItClonesReposFoundInReposFile(t *testing.T) {
Expand Down Expand Up @@ -220,7 +219,6 @@ func runCloneCommandWithFork() (string, error) {
cmd.SetOut(outBuffer)
nofork = false
err := cmd.Execute()

if err != nil {
return outBuffer.String(), err
}
Expand Down
19 changes: 12 additions & 7 deletions cmd/create_prs/create_prs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,27 @@
package create_prs

import (
"os"
sledigabel marked this conversation as resolved.
Show resolved Hide resolved
"path"
"time"

"github.com/skyscanner/turbolift/internal/campaign"
"github.com/skyscanner/turbolift/internal/colors"
"github.com/skyscanner/turbolift/internal/git"
"github.com/skyscanner/turbolift/internal/github"
"github.com/skyscanner/turbolift/internal/logging"
"github.com/spf13/cobra"
"os"
"path"
"time"
)

var gh github.GitHub = github.NewRealGitHub()
var g git.Git = git.NewRealGit()
var (
gh github.GitHub = github.NewRealGitHub()
g git.Git = git.NewRealGit()
)

var sleep time.Duration
var isDraft bool
var (
sleep time.Duration
isDraft bool
)

func NewCreatePRsCmd() *cobra.Command {
cmd := &cobra.Command{
Expand Down
13 changes: 9 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@ package cmd

import (
"fmt"
"log"

cloneCmd "github.com/skyscanner/turbolift/cmd/clone"
commitCmd "github.com/skyscanner/turbolift/cmd/commit"
createPrsCmd "github.com/skyscanner/turbolift/cmd/create_prs"
"github.com/skyscanner/turbolift/cmd/flags"
foreachCmd "github.com/skyscanner/turbolift/cmd/foreach"
initCmd "github.com/skyscanner/turbolift/cmd/init"
updatePrsCmd "github.com/skyscanner/turbolift/cmd/update_prs"
"github.com/spf13/cobra"
"log"
)

var version = "version-dev"
var commit = "commit-dev"
var date = "date-dev"
var (
version = "version-dev"
commit = "commit-dev"
date = "date-dev"
)

var rootCmd = &cobra.Command{
Use: "turbolift",
Expand All @@ -47,6 +51,7 @@ func init() {
rootCmd.AddCommand(createPrsCmd.NewCreatePRsCmd())
rootCmd.AddCommand(initCmd.NewInitCmd())
rootCmd.AddCommand(foreachCmd.NewForeachCmd())
rootCmd.AddCommand(updatePrsCmd.NewUpdatePRsCmd())
}

func Execute() {
Expand Down
141 changes: 141 additions & 0 deletions cmd/update_prs/update_prs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2021 Skyscanner Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package update_prs

import (
"errors"
"fmt"
"os"

"github.com/skyscanner/turbolift/internal/campaign"
"github.com/skyscanner/turbolift/internal/colors"
"github.com/skyscanner/turbolift/internal/github"
"github.com/skyscanner/turbolift/internal/logging"
"github.com/skyscanner/turbolift/internal/prompt"
"github.com/spf13/cobra"
)

var (
gh github.GitHub = github.NewRealGitHub()
p prompt.Prompt = prompt.NewRealPrompt()
)

var (
closeFlag bool
yesFlag bool
)

func NewUpdatePRsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update-prs",
Short: "update all PRs that have been generated by the campaign",
Run: run,
}

cmd.Flags().BoolVar(&closeFlag, "close", false, "Close all generated PRs")
cmd.Flags().BoolVar(&yesFlag, "yes", false, "Skips the confirmation prompt")

return cmd
}

// makes sure there is only one action activated
func onlyOne(args ...bool) bool {
// simple counter
b := map[bool]int{
false: 0,
true: 0,
}
for _, v := range args {
b[v] += 1
}
return b[true] == 1
}

func validateFlags(closeFlag bool) error {
// only option at the moment is `close`
if !onlyOne(closeFlag) {
return errors.New("update-prs needs one and only one action flag")
}
return nil
}

// 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); err != nil {
logger.Errorf("Error while parsing the flags: %v", err)
return
}

if closeFlag {
runClose(c, args)
}
}

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

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

// Prompting for confirmation
if !yesFlag {
// TODO: add the number of PRs that it will actually close
if !p.AskConfirm(fmt.Sprintf("Close all PRs from the %s campaign?", dir.Name)) {
return
}
}

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

for _, repo := range dir.Repos {

closeActivity := logger.StartActivity("Closing PR in %s", repo.FullRepoName)
// skip if the working copy does not exist
if _, err = os.Stat(repo.FullRepoPath()); os.IsNotExist(err) {
closeActivity.EndWithWarningf("Directory %s does not exist - has it been cloned?", repo.FullRepoPath())
skippedCount++
continue
}

err = gh.ClosePullRequest(closeActivity.Writer(), repo.FullRepoPath(), dir.Name)
if err != nil {
if _, ok := err.(*github.NoPRFoundError); ok {
closeActivity.EndWithWarning(err)
skippedCount++
} else {
closeActivity.EndWithFailure(err)
errorCount++
}
} else {
closeActivity.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"))
}
}
Loading