diff --git a/commands/issue.go b/commands/issue.go index 3dd9959206ea3240018aaf755f2ddd6148235d05..b9a8c59253d93bc2f3841c1696ad627cf6ba2f3e 100644 --- a/commands/issue.go +++ b/commands/issue.go @@ -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 @@ -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, @@ -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) } diff --git a/commands/pr.go b/commands/pr.go index 612bf572ed058e38af267ad2edcc8c92aabe6321..e876c2e1259799046e78b2c670b96e39dead0ee5 100644 --- a/commands/pr.go +++ b/commands/pr.go @@ -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 []", - Long: `Check out the head of a pull request as a local branch. + Run: printHelp, + Usage: ` +pr list [-s ] [-h ] [-b ] [-o [-^]] [-L ] +pr checkout [] +`, + 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 + Display pull requests with state (default: "open"). + + -f, --format + Pretty print the list of pull requests using 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 displayed issues by "created" (default), "updated", "popularity", or "long-running". + + -^ --sort-ascending + Sort by ascending dates instead of descending. + + -L, --limit + Display only the first 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) } @@ -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 @@ -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) +} diff --git a/github/client.go b/github/client.go index e225d60c52a7efed04dbf4a5bee107441f903843..19d2dd48105fc1a0fc505cabaa9fb6e6829cd044 100644 --- a/github/client.go +++ b/github/client.go @@ -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 { @@ -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 { @@ -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 {