Skip to content

Commit 67bf27b

Browse files
authored
Merge pull request cli#11835 from cli/kw/do-not-request-org-teams-for-reviewer-set
`gh pr edit`: Only fetch org teams for reviewers when required
2 parents 32a9c4b + 43e834a commit 67bf27b

4 files changed

Lines changed: 324 additions & 134 deletions

File tree

api/queries_pr.go

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package api
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
57
"net/http"
68
"net/url"
@@ -629,17 +631,58 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
629631
return pr, nil
630632
}
631633

632-
func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error {
633-
var mutation struct {
634-
RequestReviews struct {
635-
PullRequest struct {
636-
ID string
637-
}
638-
} `graphql:"requestReviews(input: $input)"`
639-
}
640-
variables := map[string]interface{}{"input": params}
641-
err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables)
642-
return err
634+
// AddPullRequestReviews adds the given user and team reviewers to a pull request using the REST API.
635+
func AddPullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error {
636+
if len(users) == 0 && len(teams) == 0 {
637+
return nil
638+
}
639+
640+
path := fmt.Sprintf(
641+
"repos/%s/%s/pulls/%d/requested_reviewers",
642+
url.PathEscape(repo.RepoOwner()),
643+
url.PathEscape(repo.RepoName()),
644+
prNumber,
645+
)
646+
body := struct {
647+
Reviewers []string `json:"reviewers,omitempty"`
648+
TeamReviewers []string `json:"team_reviewers,omitempty"`
649+
}{
650+
Reviewers: users,
651+
TeamReviewers: teams,
652+
}
653+
buf := &bytes.Buffer{}
654+
if err := json.NewEncoder(buf).Encode(body); err != nil {
655+
return err
656+
}
657+
// The endpoint responds with the updated pull request object; we don't need it here.
658+
return client.REST(repo.RepoHost(), "POST", path, buf, nil)
659+
}
660+
661+
// RemovePullRequestReviews removes requested reviewers from a pull request using the REST API.
662+
func RemovePullRequestReviews(client *Client, repo ghrepo.Interface, prNumber int, users, teams []string) error {
663+
if len(users) == 0 && len(teams) == 0 {
664+
return nil
665+
}
666+
667+
path := fmt.Sprintf(
668+
"repos/%s/%s/pulls/%d/requested_reviewers",
669+
url.PathEscape(repo.RepoOwner()),
670+
url.PathEscape(repo.RepoName()),
671+
prNumber,
672+
)
673+
body := struct {
674+
Reviewers []string `json:"reviewers,omitempty"`
675+
TeamReviewers []string `json:"team_reviewers,omitempty"`
676+
}{
677+
Reviewers: users,
678+
TeamReviewers: teams,
679+
}
680+
buf := &bytes.Buffer{}
681+
if err := json.NewEncoder(buf).Encode(body); err != nil {
682+
return err
683+
}
684+
// The endpoint responds with the updated pull request object; we don't need it here.
685+
return client.REST(repo.RepoHost(), "DELETE", path, buf, nil)
643686
}
644687

645688
func UpdatePullRequestBranch(client *Client, repo ghrepo.Interface, params githubv4.UpdatePullRequestBranchInput) error {

pkg/cmd/pr/edit/edit.go

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package edit
33
import (
44
"fmt"
55
"net/http"
6+
"slices"
7+
"strings"
68
"time"
79

810
"github.com/MakeNowJust/heredoc"
@@ -13,7 +15,7 @@ import (
1315
shared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
1416
"github.com/cli/cli/v2/pkg/cmdutil"
1517
"github.com/cli/cli/v2/pkg/iostreams"
16-
"github.com/shurcooL/githubv4"
18+
"github.com/cli/cli/v2/pkg/set"
1719
"github.com/spf13/cobra"
1820
"golang.org/x/sync/errgroup"
1921
)
@@ -170,7 +172,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
170172
}
171173

172174
if opts.Interactive && !opts.IO.CanPrompt() {
173-
return cmdutil.FlagErrorf("--tile, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")
175+
return cmdutil.FlagErrorf("--title, --body, --reviewer, --assignee, --label, --project, or --milestone required when not running interactively")
174176
}
175177

176178
if runF != nil {
@@ -237,7 +239,7 @@ func editRun(opts *EditOptions) error {
237239

238240
findOptions := shared.FindOptions{
239241
Selector: opts.SelectorArg,
240-
Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"},
242+
Fields: []string{"id", "author", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"},
241243
Detector: opts.Detector,
242244
}
243245

@@ -298,6 +300,15 @@ func editRun(opts *EditOptions) error {
298300
}
299301

300302
if opts.Interactive {
303+
// Remove PR author from reviewer options;
304+
// REST API errors if author is included (GraphQL silently ignores).
305+
if editable.Reviewers.Edited {
306+
s := set.NewStringSet()
307+
s.AddValues(editable.Reviewers.Options)
308+
s.Remove(pr.Author.Login)
309+
editable.Reviewers.Options = s.ToSlice()
310+
}
311+
301312
editorCommand, err := opts.EditorRetriever.Retrieve()
302313
if err != nil {
303314
return err
@@ -309,7 +320,7 @@ func editRun(opts *EditOptions) error {
309320
}
310321

311322
opts.IO.StartProgressIndicator()
312-
err = updatePullRequest(httpClient, repo, pr.ID, editable)
323+
err = updatePullRequest(httpClient, repo, pr.ID, pr.Number, editable)
313324
opts.IO.StopProgressIndicator()
314325
if err != nil {
315326
return err
@@ -320,36 +331,53 @@ func editRun(opts *EditOptions) error {
320331
return nil
321332
}
322333

323-
func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
334+
func updatePullRequest(httpClient *http.Client, repo ghrepo.Interface, id string, number int, editable shared.Editable) error {
324335
var wg errgroup.Group
325336
wg.Go(func() error {
326337
return shared.UpdateIssue(httpClient, repo, id, true, editable)
327338
})
328339
if editable.Reviewers.Edited {
329340
wg.Go(func() error {
330-
return updatePullRequestReviews(httpClient, repo, id, editable)
341+
return updatePullRequestReviews(httpClient, repo, number, editable)
331342
})
332343
}
333344
return wg.Wait()
334345
}
335346

336-
func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
337-
userIds, teamIds, err := editable.ReviewerIds()
338-
if err != nil {
339-
return err
340-
}
341-
if userIds == nil && teamIds == nil {
347+
func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, number int, editable shared.Editable) error {
348+
if !editable.Reviewers.Edited {
342349
return nil
343350
}
344-
union := githubv4.Boolean(false)
345-
reviewsRequestParams := githubv4.RequestReviewsInput{
346-
PullRequestID: id,
347-
Union: &union,
348-
UserIDs: ghIds(userIds),
349-
TeamIDs: ghIds(teamIds),
351+
352+
// Rebuild the Value slice from non-interactive flag input.
353+
if len(editable.Reviewers.Add) != 0 || len(editable.Reviewers.Remove) != 0 {
354+
s := set.NewStringSet()
355+
s.AddValues(editable.Reviewers.Add)
356+
s.AddValues(editable.Reviewers.Default)
357+
s.RemoveValues(editable.Reviewers.Remove)
358+
editable.Reviewers.Value = s.ToSlice()
359+
}
360+
361+
addUsers, addTeams := partitionUsersAndTeams(editable.Reviewers.Value)
362+
363+
// Reviewers in Default but not in the Value have been removed interactively.
364+
var toRemove []string
365+
for _, r := range editable.Reviewers.Default {
366+
if !slices.Contains(editable.Reviewers.Value, r) {
367+
toRemove = append(toRemove, r)
368+
}
350369
}
370+
removeUsers, removeTeams := partitionUsersAndTeams(toRemove)
371+
351372
client := api.NewClientFromHTTP(httpClient)
352-
return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams)
373+
wg := errgroup.Group{}
374+
wg.Go(func() error {
375+
return api.AddPullRequestReviews(client, repo, number, addUsers, addTeams)
376+
})
377+
wg.Go(func() error {
378+
return api.RemovePullRequestReviews(client, repo, number, removeUsers, removeTeams)
379+
})
380+
return wg.Wait()
353381
}
354382

355383
type Surveyor interface {
@@ -391,13 +419,18 @@ func (e editorRetriever) Retrieve() (string, error) {
391419
return cmdutil.DetermineEditor(e.config)
392420
}
393421

394-
func ghIds(s *[]string) *[]githubv4.ID {
395-
if s == nil {
396-
return nil
397-
}
398-
ids := make([]githubv4.ID, len(*s))
399-
for i, v := range *s {
400-
ids[i] = v
422+
// partitionUsersAndTeams splits reviewer identifiers into user logins and team slugs.
423+
// Team identifiers are in the form "org/slug"; only the slug portion is returned for teams.
424+
func partitionUsersAndTeams(values []string) (users []string, teams []string) {
425+
for _, v := range values {
426+
if strings.ContainsRune(v, '/') {
427+
parts := strings.SplitN(v, "/", 2)
428+
if len(parts) == 2 && parts[1] != "" {
429+
teams = append(teams, parts[1])
430+
}
431+
} else if v != "" {
432+
users = append(users, v)
433+
}
401434
}
402-
return &ids
435+
return
403436
}

0 commit comments

Comments
 (0)