Skip to content

Commit

Permalink
feat: Add --reviewers to av pr create (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
twavv authored Nov 27, 2023
1 parent dc11025 commit b8c2a31
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 18 deletions.
43 changes: 30 additions & 13 deletions cmd/av/pr_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"io"
"os"
"strings"

"emperror.dev/errors"
"github.com/aviator-co/av/internal/actions"
Expand All @@ -13,19 +12,19 @@ import (
)

var prCreateFlags struct {
Draft bool
Force bool
NoPush bool
Title string
Body string
Edit bool
Draft bool
Force bool
NoPush bool
Title string
Body string
Edit bool
Reviewers []string
}

var prCreateCmd = &cobra.Command{
Use: "create",
Short: "create a pull request for the current branch",
Long: strings.TrimSpace(`
Create a pull request for the current branch.
Long: `Create a pull request for the current branch.
Examples:
Create a PR with an empty body:
Expand All @@ -36,7 +35,10 @@ Examples:
> Implement my very fancy feature.
> Can you please review it?
> EOF
`),
Create a pull request, assigning reviewers:
$ av pr create --reviewers "example,@example-org/example-team"
`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) (reterr error) {
repo, err := getRepo()
Expand Down Expand Up @@ -74,8 +76,9 @@ Examples:
draft = prCreateFlags.Draft
}

if _, err := actions.CreatePullRequest(
context.Background(), repo, client, tx,
ctx := context.Background()
res, err := actions.CreatePullRequest(
ctx, repo, client, tx,
actions.CreatePullRequestOpts{
BranchName: branchName,
Title: prCreateFlags.Title,
Expand All @@ -85,12 +88,22 @@ Examples:
Draft: draft,
Edit: prCreateFlags.Edit,
},
); err != nil {
)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}

// Do this after creating the PR and committing the transaction so that
// our local database is up-to-date even if this fails.
if len(prCreateFlags.Reviewers) > 0 {
if err := actions.AddPullRequestReviewers(ctx, client, res.Pull.ID, prCreateFlags.Reviewers); err != nil {
return err
}
}

return nil
},
}
Expand Down Expand Up @@ -122,4 +135,8 @@ func init() {
&prCreateFlags.Edit, "edit", false,
"always open an editor to edit the pull request title and description",
)
prCreateCmd.Flags().StringSliceVar(
&prCreateFlags.Reviewers, "reviewers", nil,
"add reviewers to the pull request (can be usernames or team names)",
)
}
10 changes: 5 additions & 5 deletions internal/actions/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ import (
)

type CreatePullRequestOpts struct {
// The HEAD branch to create a pull request for.
BranchName string
Title string
Body string
//LabelNames []string

// The pull request title.
Title string
// The pull request body (description).
Body string
// If true, create the pull request as a GitHub draft PR.
Draft bool
// If true, do not push the branch to GitHub
Expand All @@ -42,7 +43,6 @@ type CreatePullRequestOpts struct {
ForcePush bool
// If true, create a PR even if we think one already exists
Force bool

// If true, open an editor for editing the title and body
Edit bool
}
Expand Down
74 changes: 74 additions & 0 deletions internal/actions/reviewers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package actions

import (
"context"
"fmt"
"os"
"strings"

"emperror.dev/errors"
"github.com/aviator-co/av/internal/gh"
"github.com/aviator-co/av/internal/utils/colors"
"github.com/shurcooL/githubv4"
)

// AddPullRequestReviewers adds the given reviewers to the given pull request.
// It accepts a list of reviewers, which can be either GitHub user logins or
// team names in the format `@organization/team`.
func AddPullRequestReviewers(
ctx context.Context,
client *gh.Client,
prID githubv4.ID,
reviewers []string,
) error {
_, _ = fmt.Fprint(os.Stderr,
" - adding ", colors.UserInput(len(reviewers)), " reviewer(s) to pull request\n",
)

// We need to map the given reviewers to GitHub node IDs.
var reviewerIDs []githubv4.ID
var teamIDs []githubv4.ID
for _, reviewer := range reviewers {
if ok, org, team := isTeamName(reviewer); ok {
team, err := client.OrganizationTeam(ctx, org, team)
if err != nil {
return err
}
teamIDs = append(teamIDs, team.ID)
} else {
user, err := client.User(ctx, reviewer)
if err != nil {
return err
}
reviewerIDs = append(reviewerIDs, user.ID)
}
}

if _, err := client.RequestReviews(ctx, githubv4.RequestReviewsInput{
PullRequestID: prID,
UserIDs: &reviewerIDs,
TeamIDs: &teamIDs,
Union: gh.Ptr[githubv4.Boolean](true),
}); err != nil {
return errors.WrapIf(err, "requesting reviews")
}

return nil
}

func isTeamName(s string) (bool, string, string) {
before, after, found := strings.Cut(s, "/")
if !found || before == "" || after == "" {
return false, "", ""
}

// It's common to specify team names as `@aviator-co/engineering`. We want
// just the organization name (`aviator-co`) and team slug (`engineering`)
// here, so strip the leading `@` if it exists.
// This shouldn't cause any ambiguity since GitHub user login's can't
// contain a slash character.
if strings.HasPrefix(before, "@") {
before = before[1:]
}
return true, before, after
}
21 changes: 21 additions & 0 deletions internal/gh/pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ func (c *Client) UpdatePullRequest(
return &mutation.UpdatePullRequest.PullRequest, nil
}

// RequestReviews requests reviews from the given users on the given pull
// request.
func (c *Client) RequestReviews(
ctx context.Context,
input githubv4.RequestReviewsInput,
) (*PullRequest, error) {
if input.Union == nil {
// Add reviewers instead of replacing them by default.
input.Union = Ptr[githubv4.Boolean](true)
}
var mutation struct {
RequestReviews struct {
PullRequest PullRequest
} `graphql:"requestReviews(input: $input)"`
}
if err := c.mutate(ctx, &mutation, input, nil); err != nil {
return nil, errors.Wrap(err, "failed to request pull request reviews")
}
return &mutation.RequestReviews.PullRequest, nil
}

func (c *Client) ConvertPullRequestToDraft(ctx context.Context, id string) (*PullRequest, error) {
var mutation struct {
ConvertPullRequestToDraft struct {
Expand Down
45 changes: 45 additions & 0 deletions internal/gh/team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package gh

import (
"context"

"emperror.dev/errors"
"github.com/shurcooL/githubv4"
)

type Team struct {
ID githubv4.ID `graphql:"id"`
Name string `graphql:"name"`
Slug string `graphql:"slug"`
}

// OrganizationTeam returns information about the given team in the given organization.
func (c *Client) OrganizationTeam(
ctx context.Context,
organizationLogin string,
teamSlug string,
) (*Team, error) {
var query struct {
Organization struct {
ID githubv4.ID `graphql:"id"`
Team Team `graphql:"team(slug: $teamSlug)"`
} `graphql:"organization(login: $organizationLogin)"`
}
if err := c.query(ctx, &query, map[string]any{
"organizationLogin": githubv4.String(organizationLogin),
"teamSlug": githubv4.String(teamSlug),
}); err != nil {
return nil, err
}
if query.Organization.ID == "" {
return nil, errors.Errorf("GitHub organization %q not found", organizationLogin)
}
if query.Organization.Team.ID == "" {
return nil, errors.Errorf(
"GitHub team %q not found within organization %q",
teamSlug,
organizationLogin,
)
}
return &query.Organization.Team, nil
}
29 changes: 29 additions & 0 deletions internal/gh/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gh

import (
"context"

"emperror.dev/errors"
"github.com/shurcooL/githubv4"
)

type User struct {
ID githubv4.ID `graphql:"id"`
Login string `graphql:"login"`
}

// User returns information about the given user.
func (c *Client) User(ctx context.Context, login string) (*User, error) {
var query struct {
User User `graphql:"user(login: $login)"`
}
if err := c.query(ctx, &query, map[string]any{
"login": githubv4.String(login),
}); err != nil {
return nil, err
}
if query.User.ID == "" {
return nil, errors.Errorf("GitHub user %q not found", login)
}
return &query.User, nil
}

0 comments on commit b8c2a31

Please sign in to comment.