Skip to content

Commit 667704d

Browse files
committed
Add pr list command
Old `pr list` is now `pr status`
1 parent a66aaaf commit 667704d

File tree

7 files changed

+368
-73
lines changed

7 files changed

+368
-73
lines changed

api/queries.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,95 @@ func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRe
171171

172172
return prs, nil
173173
}
174+
175+
func PullRequestList(client *Client, vars map[string]interface{}, limit int) ([]PullRequest, error) {
176+
type response struct {
177+
Repository struct {
178+
PullRequests struct {
179+
Edges []struct {
180+
Node PullRequest
181+
}
182+
PageInfo struct {
183+
HasNextPage bool
184+
EndCursor string
185+
}
186+
}
187+
}
188+
}
189+
190+
query := `
191+
query(
192+
$owner: String!,
193+
$repo: String!,
194+
$limit: Int!,
195+
$endCursor: String,
196+
$baseBranch: String,
197+
$labels: [String!],
198+
$state: [PullRequestState!] = OPEN
199+
) {
200+
repository(owner: $owner, name: $repo) {
201+
pullRequests(
202+
states: $state,
203+
baseRefName: $baseBranch,
204+
labels: $labels,
205+
first: $limit,
206+
after: $endCursor,
207+
orderBy: {field: CREATED_AT, direction: DESC}
208+
) {
209+
edges {
210+
node {
211+
number
212+
title
213+
url
214+
headRefName
215+
}
216+
}
217+
pageInfo {
218+
hasNextPage
219+
endCursor
220+
}
221+
}
222+
}
223+
}`
224+
225+
prs := []PullRequest{}
226+
pageLimit := min(limit, 100)
227+
variables := map[string]interface{}{}
228+
for name, val := range vars {
229+
variables[name] = val
230+
}
231+
232+
for {
233+
variables["limit"] = pageLimit
234+
var data response
235+
err := client.GraphQL(query, variables, &data)
236+
if err != nil {
237+
return nil, err
238+
}
239+
prData := data.Repository.PullRequests
240+
241+
for _, edge := range prData.Edges {
242+
prs = append(prs, edge.Node)
243+
if len(prs) == limit {
244+
goto done
245+
}
246+
}
247+
248+
if prData.PageInfo.HasNextPage {
249+
variables["endCursor"] = prData.PageInfo.EndCursor
250+
pageLimit = min(pageLimit, limit-len(prs))
251+
continue
252+
}
253+
done:
254+
break
255+
}
256+
257+
return prs, nil
258+
}
259+
260+
func min(a, b int) int {
261+
if a < b {
262+
return a
263+
}
264+
return b
265+
}

command/pr.go

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,49 @@ package command
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67

78
"github.com/github/gh-cli/api"
89
"github.com/github/gh-cli/utils"
910
"github.com/spf13/cobra"
11+
"golang.org/x/crypto/ssh/terminal"
1012
)
1113

1214
func init() {
1315
RootCmd.AddCommand(prCmd)
14-
prCmd.AddCommand(
15-
&cobra.Command{
16-
Use: "list",
17-
Short: "List pull requests",
18-
RunE: prList,
19-
},
20-
&cobra.Command{
21-
Use: "view [pr-number]",
22-
Short: "Open a pull request in the browser",
23-
RunE: prView,
24-
},
25-
)
16+
prCmd.AddCommand(prListCmd)
17+
prCmd.AddCommand(prStatusCmd)
18+
prCmd.AddCommand(prViewCmd)
19+
20+
prListCmd.Flags().IntP("limit", "L", 30, "maximum number of items to fetch")
21+
prListCmd.Flags().StringP("state", "s", "open", "filter by state")
22+
prListCmd.Flags().StringP("base", "b", "", "filter by base branch")
23+
prListCmd.Flags().StringArrayP("label", "l", nil, "filter by label")
2624
}
2725

2826
var prCmd = &cobra.Command{
2927
Use: "pr",
3028
Short: "Work with pull requests",
31-
Long: `This command allows you to
32-
work with pull requests.`,
33-
Args: cobra.MinimumNArgs(1),
34-
RunE: func(cmd *cobra.Command, args []string) error {
35-
return fmt.Errorf("%+v is not a valid PR command", args)
36-
},
29+
Long: `Helps you work with pull requests.`,
30+
}
31+
var prListCmd = &cobra.Command{
32+
Use: "list",
33+
Short: "List pull requests",
34+
RunE: prList,
35+
}
36+
var prStatusCmd = &cobra.Command{
37+
Use: "status",
38+
Short: "Show status of relevant pull requests",
39+
RunE: prStatus,
40+
}
41+
var prViewCmd = &cobra.Command{
42+
Use: "view [pr-number]",
43+
Short: "Open a pull request in the browser",
44+
RunE: prView,
3745
}
3846

39-
func prList(cmd *cobra.Command, args []string) error {
47+
func prStatus(cmd *cobra.Command, args []string) error {
4048
ctx := contextForCommand(cmd)
4149
apiClient, err := apiClientForContext(ctx)
4250
if err != nil {
@@ -89,6 +97,101 @@ func prList(cmd *cobra.Command, args []string) error {
8997
return nil
9098
}
9199

100+
func prList(cmd *cobra.Command, args []string) error {
101+
ctx := contextForCommand(cmd)
102+
apiClient, err := apiClientForContext(ctx)
103+
if err != nil {
104+
return err
105+
}
106+
107+
baseRepo, err := ctx.BaseRepo()
108+
if err != nil {
109+
return err
110+
}
111+
112+
limit, err := cmd.Flags().GetInt("limit")
113+
if err != nil {
114+
return err
115+
}
116+
state, err := cmd.Flags().GetString("state")
117+
if err != nil {
118+
return err
119+
}
120+
baseBranch, err := cmd.Flags().GetString("base")
121+
if err != nil {
122+
return err
123+
}
124+
labels, err := cmd.Flags().GetStringArray("label")
125+
if err != nil {
126+
return err
127+
}
128+
129+
var graphqlState string
130+
switch state {
131+
case "open":
132+
graphqlState = "OPEN"
133+
case "closed":
134+
graphqlState = "CLOSED"
135+
case "all":
136+
graphqlState = "ALL"
137+
default:
138+
return fmt.Errorf("invalid state: %s", state)
139+
}
140+
141+
params := map[string]interface{}{
142+
"owner": baseRepo.RepoOwner(),
143+
"repo": baseRepo.RepoName(),
144+
"state": graphqlState,
145+
}
146+
if len(labels) > 0 {
147+
params["labels"] = labels
148+
}
149+
if baseBranch != "" {
150+
params["baseBranch"] = baseBranch
151+
}
152+
153+
prs, err := api.PullRequestList(apiClient, params, limit)
154+
if err != nil {
155+
return err
156+
}
157+
158+
tty := false
159+
ttyWidth := 80
160+
out := cmd.OutOrStdout()
161+
if outFile, isFile := out.(*os.File); isFile {
162+
fd := int(outFile.Fd())
163+
tty = terminal.IsTerminal(fd)
164+
if w, _, err := terminal.GetSize(fd); err == nil {
165+
ttyWidth = w
166+
}
167+
}
168+
169+
numWidth := 8
170+
branchWidth := 40
171+
titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2
172+
maxTitleWidth := 0
173+
for _, pr := range prs {
174+
if len(pr.Title) > maxTitleWidth {
175+
maxTitleWidth = len(pr.Title)
176+
}
177+
}
178+
if maxTitleWidth < titleWidth {
179+
branchWidth += titleWidth - maxTitleWidth
180+
titleWidth = maxTitleWidth
181+
}
182+
183+
for _, pr := range prs {
184+
if tty {
185+
prNum := utils.Yellow(fmt.Sprintf("% *s", numWidth, fmt.Sprintf("#%d", pr.Number)))
186+
prBranch := utils.Cyan(truncate(branchWidth, pr.HeadRefName))
187+
fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch)
188+
} else {
189+
fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadRefName)
190+
}
191+
}
192+
return nil
193+
}
194+
92195
func prView(cmd *cobra.Command, args []string) error {
93196
ctx := contextForCommand(cmd)
94197
baseRepo, err := ctx.BaseRepo()
@@ -129,7 +232,7 @@ func prView(cmd *cobra.Command, args []string) error {
129232

130233
func printPrs(prs ...api.PullRequest) {
131234
for _, pr := range prs {
132-
fmt.Printf(" #%d %s %s\n", pr.Number, truncateTitle(pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
235+
fmt.Printf(" #%d %s %s\n", pr.Number, truncate(50, pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
133236
}
134237
}
135238

@@ -141,9 +244,7 @@ func printMessage(s string) {
141244
fmt.Println(utils.Gray(s))
142245
}
143246

144-
func truncateTitle(title string) string {
145-
const maxLength = 50
146-
247+
func truncate(maxLength int, title string) string {
147248
if len(title) > maxLength {
148249
return title[0:maxLength-3] + "..."
149250
}

command/pr_test.go

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package command
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"io/ioutil"
47
"os"
8+
"reflect"
59
"regexp"
610
"testing"
711

@@ -11,6 +15,13 @@ import (
1115
"github.com/github/gh-cli/utils"
1216
)
1317

18+
func eq(t *testing.T, got interface{}, expected interface{}) {
19+
t.Helper()
20+
if !reflect.DeepEqual(got, expected) {
21+
t.Errorf("expected: %v, got: %v", expected, got)
22+
}
23+
}
24+
1425
func initBlankContext(repo, branch string) {
1526
initContext = func() context.Context {
1627
ctx := context.NewBlank()
@@ -28,17 +39,17 @@ func initFakeHTTP() *api.FakeHTTP {
2839
return http
2940
}
3041

31-
func TestPRList(t *testing.T) {
42+
func TestPRStatus(t *testing.T) {
3243
initBlankContext("OWNER/REPO", "master")
3344
http := initFakeHTTP()
3445

35-
jsonFile, _ := os.Open("../test/fixtures/prList.json")
46+
jsonFile, _ := os.Open("../test/fixtures/prStatus.json")
3647
defer jsonFile.Close()
3748
http.StubResponse(200, jsonFile)
3849

39-
output, err := test.RunCommand(RootCmd, "pr list")
50+
output, err := test.RunCommand(RootCmd, "pr status")
4051
if err != nil {
41-
t.Errorf("error running command `pr list`: %v", err)
52+
t.Errorf("error running command `pr status`: %v", err)
4253
}
4354

4455
expectedPrs := []*regexp.Regexp{
@@ -55,6 +66,57 @@ func TestPRList(t *testing.T) {
5566
}
5667
}
5768

69+
func TestPRList(t *testing.T) {
70+
initBlankContext("OWNER/REPO", "master")
71+
http := initFakeHTTP()
72+
73+
jsonFile, _ := os.Open("../test/fixtures/prList.json")
74+
defer jsonFile.Close()
75+
http.StubResponse(200, jsonFile)
76+
77+
out := bytes.Buffer{}
78+
prListCmd.SetOut(&out)
79+
80+
RootCmd.SetArgs([]string{"pr", "list"})
81+
_, err := RootCmd.ExecuteC()
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
eq(t, out.String(), `32 New feature feature
87+
29 Fixed bad bug bug-fix
88+
28 Improve documentation docs
89+
`)
90+
}
91+
92+
func TestPRList_filtering(t *testing.T) {
93+
initBlankContext("OWNER/REPO", "master")
94+
http := initFakeHTTP()
95+
96+
respBody := bytes.NewBufferString(`{ "data": {} }`)
97+
http.StubResponse(200, respBody)
98+
99+
prListCmd.SetOut(ioutil.Discard)
100+
101+
RootCmd.SetArgs([]string{"pr", "list", "-s", "all", "-l", "one", "-l", "two"})
102+
_, err := RootCmd.ExecuteC()
103+
if err != nil {
104+
t.Fatal(err)
105+
}
106+
107+
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
108+
reqBody := struct {
109+
Variables struct {
110+
State string
111+
Labels []string
112+
}
113+
}{}
114+
json.Unmarshal(bodyBytes, &reqBody)
115+
116+
eq(t, reqBody.Variables.State, "ALL")
117+
eq(t, reqBody.Variables.Labels, []string{"one", "two"})
118+
}
119+
58120
func TestPRView(t *testing.T) {
59121
initBlankContext("OWNER/REPO", "master")
60122
http := initFakeHTTP()

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ require (
99
github.com/mattn/go-isatty v0.0.9
1010
github.com/mitchellh/go-homedir v1.1.0
1111
github.com/spf13/cobra v0.0.5
12+
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
1213
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652
1314
)

0 commit comments

Comments
 (0)