Skip to content

Commit

Permalink
Add pr-status command (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
rnorth authored May 18, 2023
1 parent d427d49 commit 0e1678b
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ BIN_DIR := $(CURDIR)/bin
$(BIN_DIR)/golangci-lint: $(BIN_DIR)/golangci-lint-${GOLANGCI_VERSION}
@ln -sf golangci-lint-${GOLANGCI_VERSION} $(BIN_DIR)/golangci-lint
$(BIN_DIR)/golangci-lint-${GOLANGCI_VERSION}:
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
@mv $(BIN_DIR)/golangci-lint $@

mod:
Expand Down
150 changes: 150 additions & 0 deletions cmd/pr_status/pr_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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 pr_status

import (
"fmt"
"github.com/fatih/color"
"github.com/rodaine/table"
"github.com/skyscanner/turbolift/internal/campaign"
"github.com/skyscanner/turbolift/internal/github"
"github.com/skyscanner/turbolift/internal/logging"
"github.com/spf13/cobra"
"os"
"path"
"strings"
)

var reactionsOrder = []string{
"THUMBS_UP",
"THUMBS_DOWN",
"LAUGH",
"HOORAY",
"CONFUSED",
"HEART",
"ROCKET",
"EYES",
}

var reactionsMapping = map[string]string{
"THUMBS_UP": "👍",
"THUMBS_DOWN": "👎",
"LAUGH": "😆",
"HOORAY": "🎉",
"CONFUSED": "😕",
"HEART": "❤️",
"ROCKET": "🚀",
"EYES": "👀",
}

var gh github.GitHub = github.NewRealGitHub()

var list bool

func NewPrStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "pr-status",
Short: "Displays the status of PRs",
Run: run,
}
cmd.Flags().BoolVar(&list, "list", false, "Displays a listing by PR")

return cmd
}

func run(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()

statuses := make(map[string]int)
reactions := make(map[string]int)

detailsTable := table.New("Repository", "State", "Reviews", "URL")
detailsTable.WithHeaderFormatter(color.New(color.Underline).SprintfFunc())
detailsTable.WithFirstColumnFormatter(color.New(color.FgCyan).SprintfFunc())
detailsTable.WithWriter(logger.Writer())

for _, repo := range dir.Repos {
repoDirPath := path.Join("work", repo.OrgName, repo.RepoName) // i.e. work/org/repo

checkStatusActivity := logger.StartActivity("Checking PR status for %s", repo.FullRepoName)

// skip if the working copy does not exist
if _, err = os.Stat(repoDirPath); os.IsNotExist(err) {
checkStatusActivity.EndWithWarningf("Directory %s does not exist - has it been cloned?", repoDirPath)
statuses["SKIPPED"]++
continue
}

prStatus, err := gh.GetPR(checkStatusActivity.Writer(), repoDirPath, dir.Name)
if err != nil {
checkStatusActivity.EndWithFailuref("No PR found: %v", err)
statuses["NO_PR"]++
continue
}

statuses[prStatus.State]++

for _, reaction := range prStatus.ReactionGroups {
reactions[reaction.Content] += reaction.Users.TotalCount
}

detailsTable.AddRow(repo.FullRepoName, prStatus.State, prStatus.ReviewDecision, prStatus.Url)

checkStatusActivity.EndWithSuccess()
}

logger.Successf("turbolift pr-status completed\n")

logger.Println()

if list {
detailsTable.Print()
logger.Println()
}

summaryTable := table.New("State", "Count")
summaryTable.WithHeaderFormatter(color.New(color.Underline).SprintfFunc())
summaryTable.WithFirstColumnFormatter(color.New(color.FgCyan).SprintfFunc())
summaryTable.WithWriter(logger.Writer())

summaryTable.AddRow("Merged", statuses["MERGED"])
summaryTable.AddRow("Open", statuses["OPEN"])
summaryTable.AddRow("Closed", statuses["CLOSED"])
summaryTable.AddRow("Skipped", statuses["SKIPPED"])
summaryTable.AddRow("No PR Found", statuses["NO_PR"])

summaryTable.Print()

logger.Println()

var reactionsOutput []string
for _, key := range reactionsOrder {
if reactions[key] > 0 {
reactionsOutput = append(reactionsOutput, fmt.Sprintf("%s %d", reactionsMapping[key], reactions[key]))
}
}
if len(reactionsOutput) > 0 {
logger.Println("Reactions:", strings.Join(reactionsOutput, " "))
}
}
171 changes: 171 additions & 0 deletions cmd/pr_status/pr_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* 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 pr_status

import (
"bytes"
"errors"
"io"
"os"
"testing"

"github.com/skyscanner/turbolift/internal/github"
"github.com/skyscanner/turbolift/internal/testsupport"
"github.com/stretchr/testify/assert"
)

func init() {
// disable output colouring so that strings we want to do 'Contains' checks on do not have ANSI escape sequences in IDEs
_ = os.Setenv("NO_COLOR", "1")
}

func TestItLogsSummaryInformation(t *testing.T) {
prepareFakeResponses()

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

out, err := runCommand(false)
assert.NoError(t, err)
assert.Contains(t, out, "Checking PR status for org/repo1")
assert.Contains(t, out, "Checking PR status for org/repo2")
assert.Contains(t, out, "turbolift pr-status completed")
assert.Regexp(t, "Open\\s+1", out)
assert.Regexp(t, "Merged\\s+1", out)
assert.Regexp(t, "Closed\\s+1", out)

assert.Regexp(t, "Reactions: 👍 4 👎 3 🚀 1", out)

// Shouldn't show 'list' detailed info
assert.NotRegexp(t, "org/repo1\\s+OPEN", out)
assert.NotRegexp(t, "org/repo2\\s+MERGED", out)
}

func TestItLogsDetailedInformation(t *testing.T) {
prepareFakeResponses()

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

out, err := runCommand(true)
assert.NoError(t, err)
// Should still show summary info
assert.Regexp(t, "Open\\s+1", out)
assert.Regexp(t, "👍\\s+4", out)

assert.Regexp(t, "org/repo1\\s+OPEN\\s+REVIEW_REQUIRED", out)
assert.Regexp(t, "org/repo2\\s+MERGED\\s+APPROVED", out)
assert.Regexp(t, "org/repo3\\s+CLOSED", out)
}

func TestItSkipsUnclonedRepos(t *testing.T) {
prepareFakeResponses()

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

out, err := runCommand(true)
assert.NoError(t, err)
// Should still show summary info
assert.Regexp(t, "Open\\s+1", out)
assert.Regexp(t, "Merged\\s+0", out)
assert.Regexp(t, "Skipped\\s+1", out)
assert.Regexp(t, "No PR Found\\s+0", out)

assert.Regexp(t, "org/repo1\\s+OPEN", out)
assert.NotRegexp(t, "org/repo2\\s+MERGED", out)
}

func TestItNotesReposWhereNoPrCanBeFound(t *testing.T) {
prepareFakeResponses()

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

out, err := runCommand(true)
assert.NoError(t, err)
// Should still show summary info
assert.Regexp(t, "Open\\s+1", out)
assert.Regexp(t, "Merged\\s+1", out)
assert.Regexp(t, "Skipped\\s+0", out)
assert.Regexp(t, "No PR Found\\s+1", out)

assert.Regexp(t, "org/repo1\\s+OPEN", out)
}

func runCommand(showList bool) (string, error) {
cmd := NewPrStatusCmd()
list = showList
outBuffer := bytes.NewBufferString("")
cmd.SetOut(outBuffer)
err := cmd.Execute()

if err != nil {
return outBuffer.String(), err
}
return outBuffer.String(), nil
}

func prepareFakeResponses() {
dummyData := map[string]*github.PrStatus{
"work/org/repo1": {
State: "OPEN",
ReactionGroups: []github.ReactionGroup{
{
Content: "THUMBS_UP",
Users: github.ReactionGroupUsers{
TotalCount: 3,
},
},
{
Content: "ROCKET",
Users: github.ReactionGroupUsers{
TotalCount: 1,
},
},
},
ReviewDecision: "REVIEW_REQUIRED",
},
"work/org/repo2": {
State: "MERGED",
ReactionGroups: []github.ReactionGroup{
{
Content: "THUMBS_UP",
Users: github.ReactionGroupUsers{
TotalCount: 1,
},
},
},
ReviewDecision: "APPROVED",
},
"work/org/repo3": {
State: "CLOSED",
ReactionGroups: []github.ReactionGroup{
{
Content: "THUMBS_DOWN",
Users: github.ReactionGroupUsers{
TotalCount: 3,
},
},
},
},
}
fakeGitHub := github.NewFakeGitHub(nil, func(output io.Writer, workingDir string) (interface{}, error) {
if workingDir == "work/org/repoWithError" {
return nil, errors.New("Synthetic error")
} else {
return dummyData[workingDir], nil
}
})
gh = fakeGitHub
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
initCmd "github.com/skyscanner/turbolift/cmd/init"
updatePrsCmd "github.com/skyscanner/turbolift/cmd/update_prs"
"github.com/spf13/cobra"

prStatusCmd "github.com/skyscanner/turbolift/cmd/pr_status"
)

var (
Expand All @@ -52,6 +54,7 @@ func init() {
rootCmd.AddCommand(initCmd.NewInitCmd())
rootCmd.AddCommand(foreachCmd.NewForeachCmd())
rootCmd.AddCommand(updatePrsCmd.NewUpdatePRsCmd())
rootCmd.AddCommand(prStatusCmd.NewPrStatusCmd())
}

func Execute() {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ go 1.16
require (
github.com/briandowns/spinner v1.15.0
github.com/fatih/color v1.12.0
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/rodaine/table v1.0.1
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20210603125802-9665404d3644 // indirect
Expand Down
Loading

0 comments on commit 0e1678b

Please sign in to comment.