From 020153f1d7860424ba95396e850bcbd93d877226 Mon Sep 17 00:00:00 2001 From: Richard North Date: Thu, 18 May 2023 17:16:36 +0100 Subject: [PATCH] Add pr-status command --- Makefile | 2 +- cmd/pr_status/pr_status.go | 150 ++++++++++++++++++++++++++++ cmd/pr_status/pr_status_test.go | 171 ++++++++++++++++++++++++++++++++ cmd/root.go | 3 + go.mod | 3 +- go.sum | 13 +-- internal/github/fake_github.go | 29 +++++- internal/github/github.go | 14 +-- internal/logging/logging.go | 4 + 9 files changed, 370 insertions(+), 19 deletions(-) create mode 100644 cmd/pr_status/pr_status.go create mode 100644 cmd/pr_status/pr_status_test.go diff --git a/Makefile b/Makefile index 68b519f..47cd3a1 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/cmd/pr_status/pr_status.go b/cmd/pr_status/pr_status.go new file mode 100644 index 0000000..d92d918 --- /dev/null +++ b/cmd/pr_status/pr_status.go @@ -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, " ")) + } +} diff --git a/cmd/pr_status/pr_status_test.go b/cmd/pr_status/pr_status_test.go new file mode 100644 index 0000000..7026fed --- /dev/null +++ b/cmd/pr_status/pr_status_test.go @@ -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 +} diff --git a/cmd/root.go b/cmd/root.go index dc0795c..0df73cc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 ( @@ -52,6 +54,7 @@ func init() { rootCmd.AddCommand(initCmd.NewInitCmd()) rootCmd.AddCommand(foreachCmd.NewForeachCmd()) rootCmd.AddCommand(updatePrsCmd.NewUpdatePRsCmd()) + rootCmd.AddCommand(prStatusCmd.NewPrStatusCmd()) } func Execute() { diff --git a/go.mod b/go.mod index 99c8e21..8b5a814 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8ae8b0f..720c9ef 100644 --- a/go.sum +++ b/go.sum @@ -23,14 +23,14 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw= -github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/briandowns/spinner v1.15.0 h1:L0jR0MYN7OAeMwpTzDZWIeqyDLXtTeJFxqoq+sL0VQM= github.com/briandowns/spinner v1.15.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -44,7 +44,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -120,16 +119,16 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -160,6 +159,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ= +github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -184,6 +185,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -254,7 +256,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/github/fake_github.go b/internal/github/fake_github.go index 9a7a634..4d95fda 100644 --- a/internal/github/fake_github.go +++ b/internal/github/fake_github.go @@ -24,8 +24,9 @@ import ( ) type FakeGitHub struct { - handler func(output io.Writer, workingDir string, fullRepoName string) (bool, error) - calls [][]string + handler func(output io.Writer, workingDir string, fullRepoName string) (bool, error) + returningHandler func(output io.Writer, workingDir string) (interface{}, error) + calls [][]string } func (f *FakeGitHub) CreatePullRequest(output io.Writer, workingDir string, metadata PullRequest) (didCreate bool, err error) { @@ -53,37 +54,55 @@ func (f *FakeGitHub) ClosePullRequest(output io.Writer, workingDir string, branc return err } +func (f *FakeGitHub) GetPR(output io.Writer, workingDir string, _ string) (*PrStatus, error) { + f.calls = append(f.calls, []string{workingDir}) + result, err := f.returningHandler(output, workingDir) + if result == nil { + return nil, err + } + return result.(*PrStatus), err +} + func (f *FakeGitHub) AssertCalledWith(t *testing.T, expected [][]string) { assert.Equal(t, expected, f.calls) } -func NewFakeGitHub(h func(output io.Writer, workingDir string, fullRepoName string) (bool, error)) *FakeGitHub { +func NewFakeGitHub(h func(output io.Writer, workingDir string, fullRepoName string) (bool, error), r func(output io.Writer, workingDir string) (interface{}, error)) *FakeGitHub { return &FakeGitHub{ - handler: h, - calls: [][]string{}, + handler: h, + returningHandler: r, + calls: [][]string{}, } } func NewAlwaysSucceedsFakeGitHub() *FakeGitHub { return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) { return true, nil + }, func(output io.Writer, workingDir string) (interface{}, error) { + return PrStatus{}, nil }) } func NewAlwaysFailsFakeGitHub() *FakeGitHub { return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) { return false, errors.New("synthetic error") + }, func(output io.Writer, workingDir string) (interface{}, error) { + return nil, errors.New("synthetic error") }) } func NewAlwaysThrowNoPRFound() *FakeGitHub { return NewFakeGitHub(func(output io.Writer, workingDir string, branchName string) (bool, error) { return false, &NoPRFoundError{Path: workingDir, BranchName: branchName} + }, func(output io.Writer, workingDir string) (interface{}, error) { + panic("should not be invoked") }) } func NewAlwaysReturnsFalseFakeGitHub() *FakeGitHub { return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) { return false, nil + }, func(output io.Writer, workingDir string) (interface{}, error) { + return PrStatus{}, nil }) } diff --git a/internal/github/github.go b/internal/github/github.go index 9b179fa..880bc1f 100644 --- a/internal/github/github.go +++ b/internal/github/github.go @@ -27,10 +27,11 @@ import ( var execInstance executor.Executor = executor.NewRealExecutor() type PullRequest struct { - Title string - Body string - UpstreamRepo string - IsDraft bool + Title string + Body string + UpstreamRepo string + IsDraft bool + ReviewDecision string } type GitHub interface { @@ -38,6 +39,7 @@ type GitHub interface { Clone(output io.Writer, workingDir string, fullRepoName string) error CreatePullRequest(output io.Writer, workingDir string, metadata PullRequest) (didCreate bool, err error) ClosePullRequest(output io.Writer, workingDir string, branchName string) error + GetPR(output io.Writer, workingDir string, branchName string) (*PrStatus, error) } type RealGitHub struct{} @@ -138,13 +140,13 @@ func (r *RealGitHub) GetPR(output io.Writer, workingDir string, branchName strin // if the user has write permissions on the repo, // the PR should be under _CurrentBranch_. - if prr.CurrentBranch != nil && !prr.CurrentBranch.Closed { + if prr.CurrentBranch != nil { return prr.CurrentBranch, nil } // If not, then it's a forked PR. The headRefName is as such: `username:branchName` for _, pr := range prr.CreatedBy { - if strings.HasSuffix(pr.HeadRefName, branchName) && !pr.Closed { + if strings.HasSuffix(pr.HeadRefName, branchName) { return pr, nil } } diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 3edcbc8..632126c 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -84,3 +84,7 @@ func (log *Logger) StartActivity(format string, args ...interface{}) *Activity { verbose: log.verbose, } } + +func (log *Logger) Writer() io.Writer { + return log.writer +}