Skip to content

Commit a8a96f1

Browse files
committed
Add bulk command
1 parent 4b3ec15 commit a8a96f1

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

api/queries_bulk.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/cli/cli/internal/ghrepo"
8+
)
9+
10+
type BulkInput struct {
11+
Number int
12+
Repository ghrepo.Interface
13+
14+
Label string
15+
RepositoryID string
16+
}
17+
18+
type ResolveResult struct {
19+
ID string
20+
Issue struct {
21+
ID string
22+
} `json:"issueOrPullRequest"`
23+
Label struct {
24+
ID string
25+
}
26+
}
27+
28+
func ResolveNodeIDs(client *Client, inputs []BulkInput) ([]ResolveResult, error) {
29+
var queries []string
30+
for i, input := range inputs {
31+
if input.Number > 0 {
32+
queries = append(queries, fmt.Sprintf(`
33+
r%03d: repository(owner:%q, name:%q) {
34+
id
35+
issueOrPullRequest(number:%d) {
36+
...on Issue { id }
37+
...on PullRequest { id }
38+
}
39+
}
40+
`, i, input.Repository.RepoOwner(), input.Repository.RepoName(), input.Number))
41+
} else if input.Label != "" {
42+
queries = append(queries, fmt.Sprintf(`
43+
r%03d: node(id:%q) {
44+
...on Repository {
45+
id
46+
label(name:%q) { id }
47+
}
48+
}
49+
`, i, input.RepositoryID, input.Label))
50+
} else {
51+
queries = append(queries, fmt.Sprintf(`
52+
r%03d: repository(owner:%q, name:%q) { id }
53+
`, i, input.Repository.RepoOwner(), input.Repository.RepoName()))
54+
}
55+
}
56+
57+
result := make(map[string]ResolveResult)
58+
var ids []ResolveResult
59+
60+
err := client.GraphQL(fmt.Sprintf(`{%s}`, strings.Join(queries, "")), nil, &result)
61+
if err != nil {
62+
return ids, err
63+
}
64+
65+
// NOTE: iteration is not ordered
66+
for _, v := range result {
67+
ids = append(ids, v)
68+
}
69+
70+
return ids, nil
71+
}
72+
73+
func BulkAddLabels(client *Client, inputs []BulkInput, labelNames []string) error {
74+
ids, err := ResolveNodeIDs(client, inputs)
75+
if err != nil {
76+
return err
77+
}
78+
79+
var mutations []string
80+
cache := make(map[string][]ResolveResult)
81+
82+
for i, id := range ids {
83+
labelIDs, ok := cache[id.ID]
84+
if !ok {
85+
var labelInputs []BulkInput
86+
for _, l := range labelNames {
87+
labelInputs = append(labelInputs, BulkInput{
88+
RepositoryID: id.ID,
89+
Label: l,
90+
})
91+
}
92+
labelIDs, err = ResolveNodeIDs(client, labelInputs)
93+
if err != nil {
94+
return err
95+
}
96+
cache[id.ID] = labelIDs
97+
}
98+
99+
var labelIDsSerialized []string
100+
for _, l := range labelIDs {
101+
labelIDsSerialized = append(labelIDsSerialized, fmt.Sprintf("%q", l.Label.ID))
102+
}
103+
104+
mutations = append(mutations, fmt.Sprintf(`
105+
m%03d: addLabelsToLabelable(input: {
106+
labelableId: %q
107+
labelIds: [%s]
108+
}) { clientMutationId }
109+
`, i, id.Issue.ID, strings.Join(labelIDsSerialized, ",")))
110+
}
111+
112+
err = client.GraphQL(fmt.Sprintf(`mutation{%s}`, strings.Join(mutations, "")), nil, nil)
113+
if err != nil {
114+
return err
115+
}
116+
117+
return nil
118+
}

command/bulk.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package command
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"regexp"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/cli/cli/api"
12+
"github.com/cli/cli/internal/ghrepo"
13+
"github.com/cli/cli/utils"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func init() {
18+
RootCmd.AddCommand(bulkCmd)
19+
bulkCmd.Flags().StringSliceP("label", "l", nil, "Add label")
20+
}
21+
22+
var bulkCmd = &cobra.Command{
23+
Use: "bulk {add|open|close|browse}",
24+
Args: cobra.ExactArgs(1),
25+
Short: "Perform operation on a set of objects",
26+
Long: `Perform bulk operation on a set of objects passed in via standard input.
27+
28+
The first word of every line of input is interpreted like so:
29+
- number, e.g. "123": an issue number in the current repository
30+
- repo with number, e.g. "owner/repo#123": an issue in a specific repository
31+
- repo, e.g. "owner/repo": a specific repository
32+
33+
Valid operations are:
34+
- "add": add labels
35+
- "close": set issue state to closed
36+
- "open": set issue state to open
37+
- "browse": open items in the web browser
38+
39+
Examples:
40+
41+
$ gh issue list -L 10 | gh bulk add --label "triage"`,
42+
RunE: bulk,
43+
}
44+
45+
func bulk(cmd *cobra.Command, args []string) error {
46+
ctx := contextForCommand(cmd)
47+
48+
baseRepo, err := ctx.BaseRepo()
49+
if err != nil {
50+
return err
51+
}
52+
53+
client, err := apiClientForContext(ctx)
54+
if err != nil {
55+
return err
56+
}
57+
58+
input := bufio.NewScanner(os.Stdin)
59+
var inputItems []api.BulkInput
60+
61+
repoWithIssueRE := regexp.MustCompile(`^([^/]+/[^/]+)#(\d+)$`)
62+
repoNameRE := regexp.MustCompile(`^([^/]+/[^/]+)$`)
63+
64+
for input.Scan() {
65+
line := input.Text()
66+
tokens := strings.Fields(line)
67+
if len(tokens) < 1 {
68+
continue
69+
}
70+
71+
if issueNum, err := strconv.Atoi(tokens[0]); err == nil {
72+
inputItems = append(inputItems, api.BulkInput{
73+
Number: issueNum,
74+
Repository: baseRepo,
75+
})
76+
continue
77+
}
78+
79+
if m := repoWithIssueRE.FindAllStringSubmatch(tokens[0], 1); len(m) > 0 {
80+
repo := ghrepo.FromFullName(m[0][1])
81+
issueNum, _ := strconv.Atoi(m[0][2])
82+
inputItems = append(inputItems, api.BulkInput{
83+
Number: issueNum,
84+
Repository: repo,
85+
})
86+
continue
87+
}
88+
89+
if m := repoNameRE.FindAllStringSubmatch(tokens[0], 1); len(m) > 0 {
90+
repo := ghrepo.FromFullName(m[0][1])
91+
inputItems = append(inputItems, api.BulkInput{
92+
Repository: repo,
93+
})
94+
continue
95+
}
96+
97+
return fmt.Errorf("unrecognized input format: %q", tokens[0])
98+
}
99+
100+
labels, err := cmd.Flags().GetStringSlice("label")
101+
if err != nil {
102+
return err
103+
}
104+
105+
operation := args[0]
106+
// TODO: open, close
107+
switch operation {
108+
case "add":
109+
return api.BulkAddLabels(client, inputItems, labels)
110+
case "browse":
111+
for _, item := range inputItems {
112+
url := fmt.Sprintf("https://github.com/%s/%s", item.Repository.RepoOwner(), item.Repository.RepoName())
113+
if item.Number > 0 {
114+
url += fmt.Sprintf("/issues/%d", item.Number)
115+
}
116+
_ = utils.OpenInBrowser(url)
117+
}
118+
return nil
119+
default:
120+
return fmt.Errorf("unrecognized operation: %q", operation)
121+
}
122+
}

0 commit comments

Comments
 (0)