Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hub pr list command to list pull requests #1666

Merged
merged 1 commit into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions commands/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func listIssues(cmd *Command, args *Args) {
args.NoForward()
}

func formatIssue(issue github.Issue, format string, colorize bool) string {
func formatIssuePlaceholders(issue github.Issue, colorize bool) map[string]string {
var stateColorSwitch string
if colorize {
issueColor := 32
Expand Down Expand Up @@ -323,7 +323,7 @@ func formatIssue(issue github.Issue, format string, colorize bool) string {
updatedAtRelative = utils.TimeAgo(issue.UpdatedAt)
}

placeholders := map[string]string{
return map[string]string{
"I": fmt.Sprintf("%d", issue.Number),
"i": fmt.Sprintf("#%d", issue.Number),
"U": issue.HtmlUrl,
Expand All @@ -348,7 +348,10 @@ func formatIssue(issue github.Issue, format string, colorize bool) string {
"ut": updatedAtUnix,
"ur": updatedAtRelative,
}
}

func formatIssue(issue github.Issue, format string, colorize bool) string {
placeholders := formatIssuePlaceholders(issue, colorize)
return ui.Expand(format, placeholders, colorize)
}

Expand Down
167 changes: 158 additions & 9 deletions commands/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,128 @@ import (
"strconv"

"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
)

var (
cmdPr = &Command{
Run: printHelp,
Usage: "pr checkout <PULLREQ-NUMBER> [<BRANCH>]",
Long: `Check out the head of a pull request as a local branch.
Run: printHelp,
Usage: `
pr list [-s <STATE>] [-h <HEAD>] [-b <BASE>] [-o <SORT_KEY> [-^]] [-L <LIMIT>]
pr checkout <PR-NUMBER> [<BRANCH>]
`,
Long: `Manage GitHub pull requests for the current project.

## Examples:
$ hub pr checkout 73
> git fetch origin pull/73/head:jingweno-feature
> git checkout jingweno-feature
## Commands:

* _list_:
List pull requests in the current project.

* _checkout_:
Check out the head of a pull request in a new branch.

## Options:

-s, --state <STATE>
Display pull requests with state <STATE> (default: "open").

-f, --format <FORMAT>
Pretty print the list of pull requests using format <FORMAT> (default:
"%sC%>(8)%i%Creset %t% l%n"). See the "PRETTY FORMATS" section of the
git-log manual for some additional details on how placeholders are used in
format. The available placeholders are:

%I: pull request number

%i: pull request number prefixed with "#"

%U: the URL of this pull request

%S: state (i.e. "open", "closed")

%sC: set color to red or green, depending on pull request state.

%t: title

%l: colored labels

%L: raw, comma-separated labels

%b: body

%au: login name of author

%as: comma-separated list of assignees

%Mn: milestone number

%Mt: milestone title

%NC: number of comments

%Nc: number of comments wrapped in parentheses, or blank string if zero.

%cD: created date-only (no time of day)

%cr: created date, relative

%ct: created date, UNIX timestamp

%cI: created date, ISO 8601 format

%uD: updated date-only (no time of day)

%ur: updated date, relative

%ut: updated date, UNIX timestamp

%uI: updated date, ISO 8601 format

-o, --sort <SORT_KEY>
Sort displayed issues by "created" (default), "updated", "popularity", or "long-running".

-^ --sort-ascending
Sort by ascending dates instead of descending.

-L, --limit <LIMIT>
Display only the first <LIMIT> issues.

## See also:

hub-merge(1), hub(1), hub-checkout(1)
`,
hub-issue(1), hub-pull-request(1), hub(1)
`,
}

cmdCheckoutPr = &Command{
Key: "checkout",
Run: checkoutPr,
}

cmdListPulls = &Command{
Key: "list",
Run: listPulls,
}

flagPullRequestState,
flagPullRequestFormat,
flagPullRequestSort string

flagPullRequestSortAscending bool

flagPullRequestLimit int
)

func init() {
cmdListPulls.Flag.StringVarP(&flagPullRequestState, "state", "s", "", "STATE")
cmdListPulls.Flag.StringVarP(&flagPullRequestBase, "base", "b", "", "BASE")
cmdListPulls.Flag.StringVarP(&flagPullRequestHead, "head", "h", "", "HEAD")
cmdListPulls.Flag.StringVarP(&flagPullRequestFormat, "format", "f", "%sC%>(8)%i%Creset %t% l%n", "FORMAT")
cmdListPulls.Flag.StringVarP(&flagPullRequestSort, "sort", "o", "created", "SORT_KEY")
cmdListPulls.Flag.BoolVarP(&flagPullRequestSortAscending, "sort-ascending", "^", false, "SORT_KEY")
cmdListPulls.Flag.IntVarP(&flagPullRequestLimit, "limit", "L", -1, "LIMIT")

cmdPr.Use(cmdListPulls)
cmdPr.Use(cmdCheckoutPr)
CmdRunner.Use(cmdPr)
}
Expand All @@ -42,6 +137,46 @@ func printHelp(command *Command, args *Args) {
os.Exit(0)
}

func listPulls(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)

project, err := localRepo.MainProject()
utils.Check(err)

gh := github.NewClient(project.Host)

args.NoForward()
if args.Noop {
ui.Printf("Would request list of pull requests for %s\n", project)
return
}

flagFilters := map[string]string{
"state": flagPullRequestState,
"head": flagPullRequestHead,
"base": flagPullRequestBase,
"sort": flagPullRequestSort,
}
filters := map[string]interface{}{}
for flag, filter := range flagFilters {
if cmd.FlagPassed(flag) {
filters[flag] = filter
}
}
if flagPullRequestSortAscending {
filters["direction"] = "asc"
}

pulls, err := gh.FetchPullRequests(project, filters, flagPullRequestLimit, nil)
utils.Check(err)

colorize := ui.IsTerminal(os.Stdout)
for _, pr := range pulls {
ui.Printf(formatPullRequest(pr, flagPullRequestFormat, colorize))
}
}

func checkoutPr(command *Command, args *Args) {
words := args.Words()
var newBranchName string
Expand Down Expand Up @@ -72,3 +207,17 @@ func checkoutPr(command *Command, args *Args) {

args.Replace(args.Executable, "checkout", newArgs...)
}

func formatPullRequest(pr github.PullRequest, format string, colorize bool) string {
base := pr.Base.Ref
head := pr.Head.Label
if pr.IsSameRepo() {
head = pr.Head.Ref
}

placeholders := formatIssuePlaceholders(github.Issue(pr), colorize)
placeholders["B"] = base
placeholders["H"] = head

return ui.Expand(format, placeholders, colorize)
}
118 changes: 82 additions & 36 deletions github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ type Client struct {
Host *Host
}

func (client *Client) FetchPullRequests(project *Project, filterParams map[string]interface{}, limit int, filter func(*PullRequest) bool) (pulls []PullRequest, err error) {
api, err := client.simpleApi()
if err != nil {
return
}

path := fmt.Sprintf("repos/%s/%s/pulls?per_page=%d", project.Owner, project.Name, perPage(limit, 100))
if filterParams != nil {
query := url.Values{}
for key, value := range filterParams {
switch v := value.(type) {
case string:
query.Add(key, v)
}
}
path += "&" + query.Encode()
}

pulls = []PullRequest{}
var res *simpleResponse

for path != "" {
res, err = api.Get(path)
if err = checkStatus(200, "fetching pull requests", res, err); err != nil {
return
}
path = res.Link("next")

pullsPage := []PullRequest{}
if err = res.Unmarshal(&pullsPage); err != nil {
return
}
for _, pr := range pullsPage {
if filter == nil || filter(&pr) {
pulls = append(pulls, pr)
if limit > 0 && len(pulls) == limit {
path = ""
break
}
}
}
}

return
}

func (client *Client) PullRequest(project *Project, id string) (pr *PullRequest, err error) {
api, err := client.simpleApi()
if err != nil {
Expand Down Expand Up @@ -65,29 +111,6 @@ func (client *Client) PullRequestPatch(project *Project, id string) (patch io.Re
return res.Body, nil
}

type PullRequest struct {
ApiUrl string `json:"url"`
Number int `json:"number"`
HtmlUrl string `json:"html_url"`
Title string `json:"title"`
MaintainerCanModify bool `json:"maintainer_can_modify"`
Head *PullRequestSpec `json:"head"`
Base *PullRequestSpec `json:"base"`
}

type PullRequestSpec struct {
Label string `json:"label"`
Ref string `json:"ref"`
Sha string `json:"sha"`
Repo *Repository `json:"repo"`
}

func (pr *PullRequest) IsSameRepo() bool {
return pr.Head.Repo != nil &&
pr.Head.Repo.Name == pr.Base.Repo.Name &&
pr.Head.Repo.Owner.Login == pr.Base.Repo.Owner.Login
}

func (client *Client) CreatePullRequest(project *Project, params map[string]interface{}) (pr *PullRequest, err error) {
api, err := client.simpleApi()
if err != nil {
Expand Down Expand Up @@ -455,19 +478,42 @@ func (client *Client) ForkRepository(project *Project, params map[string]interfa
}

type Issue struct {
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
Body string `json:"body"`
User *User `json:"user"`
Assignees []User `json:"assignees"`
Labels []IssueLabel `json:"labels"`
PullRequest *PullRequest `json:"pull_request"`
HtmlUrl string `json:"html_url"`
Comments int `json:"comments"`
Milestone *Milestone `json:"milestone"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
Body string `json:"body"`
User *User `json:"user"`

PullRequest *PullRequest `json:"pull_request"`
Head *PullRequestSpec `json:"head"`
Base *PullRequestSpec `json:"base"`

MaintainerCanModify bool `json:"maintainer_can_modify"`

Comments int `json:"comments"`
Labels []IssueLabel `json:"labels"`
Assignees []User `json:"assignees"`
Milestone *Milestone `json:"milestone"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

ApiUrl string `json:"url"`
HtmlUrl string `json:"html_url"`
}

type PullRequest Issue

type PullRequestSpec struct {
Label string `json:"label"`
Ref string `json:"ref"`
Sha string `json:"sha"`
Repo *Repository `json:"repo"`
}

func (pr *PullRequest) IsSameRepo() bool {
return pr.Head != nil && pr.Head.Repo != nil &&
pr.Head.Repo.Name == pr.Base.Repo.Name &&
pr.Head.Repo.Owner.Login == pr.Base.Repo.Owner.Login
}

type IssueLabel struct {
Expand Down