提交 66fce6e4 编写于 作者: M Mislav Marohnić

Merge remote-tracking branch 'origin/master' into issue-1930

......@@ -132,10 +132,11 @@ With no arguments, show a list of open issues.
-c, --copy
Put the URL of the new issue to clipboard instead of printing it.
-M, --milestone <ID>
Display only issues for a GitHub milestone with id <ID>.
-M, --milestone <NAME>
Display only issues for a GitHub milestone with the name <NAME>.
When opening an issue, add this issue to a GitHub milestone with id <ID>.
When opening an issue, add this issue to a GitHub milestone with the name <NAME>.
Passing the milestone number is deprecated.
-l, --labels <LABELS>
Display only issues with certain labels.
......@@ -168,7 +169,7 @@ hub-pr(1), hub(1)
-a, --assignee USER
-s, --state STATE
-f, --format FMT
-M, --milestone M
-M, --milestone NAME
-c, --creator USER
-@, --mentioned USER
-l, --labels LIST
......@@ -187,7 +188,7 @@ hub-pr(1), hub(1)
KnownFlags: `
-m, --message MSG
-F, --file FILE
-M, --milestone M
-M, --milestone NAME
-l, --labels LIST
-a, --assign USER
-o, --browse
......@@ -241,7 +242,16 @@ func listIssues(cmd *Command, args *Args) {
filters["assignee"] = args.Flag.Value("--assignee")
}
if args.Flag.HasReceived("--milestone") {
filters["milestone"] = args.Flag.Value("--milestone")
milestoneValue := args.Flag.Value("--milestone")
if milestoneValue == "none" {
filters["milestone"] = milestoneValue
} else {
milestoneNumber, err := milestoneValueToNumber(milestoneValue, gh, project)
utils.Check(err)
if milestoneNumber > 0 {
filters["milestone"] = milestoneNumber
}
}
}
if args.Flag.HasReceived("--creator") {
filters["creator"] = args.Flag.Value("--creator")
......@@ -574,8 +584,10 @@ text is the title and the rest is the description.`, project))
params["assignees"] = flagIssueAssignees
}
if flagIssueMilestone := args.Flag.Int("--milestone"); flagIssueMilestone > 0 {
params["milestone"] = flagIssueMilestone
milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project)
utils.Check(err)
if milestoneNumber > 0 {
params["milestone"] = milestoneNumber
}
args.NoForward()
......@@ -673,3 +685,25 @@ func pickHighContrastTextColor(color *utils.Color) *utils.Color {
}
return utils.Black
}
func milestoneValueToNumber(value string, client *github.Client, project *github.Project) (int, error) {
if value == "" {
return 0, nil
}
if milestoneNumber, err := strconv.Atoi(value); err == nil {
return milestoneNumber, nil
}
milestones, err := client.FetchMilestones(project)
if err != nil {
return 0, err
}
for _, milestone := range milestones {
if strings.EqualFold(milestone.Title, value) {
return milestone.Number, nil
}
}
return 0, fmt.Errorf("error: no milestone found with name '%s'", value)
}
......@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
......@@ -16,6 +17,8 @@ var (
Usage: `
pr list [-s <STATE>] [-h <HEAD>] [-b <BASE>] [-o <SORT_KEY> [-^]] [-f <FORMAT>] [-L <LIMIT>]
pr checkout <PR-NUMBER> [<BRANCH>]
pr show [-uc] [-h <HEAD>]
pr show [-uc] <PR-NUMBER>
`,
Long: `Manage GitHub Pull Requests for the current repository.
......@@ -27,6 +30,9 @@ pr checkout <PR-NUMBER> [<BRANCH>]
* _checkout_:
Check out the head of a pull request in a new branch.
* _show_:
Open a pull request page in a web browser.
## Options:
-s, --state <STATE>
......@@ -133,6 +139,12 @@ pr checkout <PR-NUMBER> [<BRANCH>]
-L, --limit <LIMIT>
Display only the first <LIMIT> issues.
-u, --url
Print the pull request URL instead of opening it.
-c, --copy
Put the pull request URL to clipboard instead of opening it.
## See also:
hub-issue(1), hub-pull-request(1), hub(1)
......@@ -150,11 +162,22 @@ hub-issue(1), hub-pull-request(1), hub(1)
Run: listPulls,
Long: cmdPr.Long,
}
cmdShowPr = &Command{
Key: "show",
Run: showPr,
KnownFlags: `
-h, --head HEAD
-u, --url
-c, --copy
`,
}
)
func init() {
cmdPr.Use(cmdListPulls)
cmdPr.Use(cmdCheckoutPr)
cmdPr.Use(cmdShowPr)
CmdRunner.Use(cmdPr)
}
......@@ -255,6 +278,113 @@ func checkoutPr(command *Command, args *Args) {
args.Replace(args.Executable, "checkout", newArgs...)
}
func showPr(command *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)
baseProject, err := localRepo.MainProject()
utils.Check(err)
words := args.Words()
openUrl := ""
if len(words) > 0 {
if prNumber, err := strconv.Atoi(words[0]); err == nil {
openUrl = baseProject.WebURL("", "", fmt.Sprintf("pull/%d", prNumber))
} else {
utils.Check(fmt.Errorf("invalid pull request number: '%s'", words[0]))
}
} else {
pr, err := findCurrentPullRequest(localRepo, baseProject, args.Flag.Value("--head"))
utils.Check(err)
openUrl = pr.HtmlUrl
}
args.NoForward()
printUrl := args.Flag.Bool("--url")
copyUrl := args.Flag.Bool("--copy")
printBrowseOrCopy(args, openUrl, !printUrl && !copyUrl, copyUrl)
}
func findCurrentPullRequest(localRepo *github.GitHubRepo, baseProject *github.Project, headArg string) (*github.PullRequest, error) {
host, err := github.CurrentConfig().PromptForHost(baseProject.Host)
utils.Check(err)
gh := github.NewClientWithHost(host)
filterParams := map[string]interface{}{
"state": "open",
}
headWithOwner := ""
if headArg != "" {
headWithOwner = headArg
if !strings.Contains(headWithOwner, ":") {
headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, headWithOwner)
}
} else {
currentBranch, err := localRepo.CurrentBranch()
utils.Check(err)
if headBranch, headProject, err := findPushTarget(currentBranch); err == nil {
headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, headBranch.ShortName())
} else if headProject, err := deducePushTarget(currentBranch, host.User); err == nil {
headWithOwner = fmt.Sprintf("%s:%s", headProject.Owner, currentBranch.ShortName())
} else {
headWithOwner = fmt.Sprintf("%s:%s", baseProject.Owner, currentBranch.ShortName())
}
}
filterParams["head"] = headWithOwner
pulls, err := gh.FetchPullRequests(baseProject, filterParams, 1, nil)
if err != nil {
return nil, err
} else if len(pulls) == 1 {
return &pulls[0], nil
} else {
return nil, fmt.Errorf("no open pull requests found for branch '%s'", headWithOwner)
}
}
func findPushTarget(branch *github.Branch) (*github.Branch, *github.Project, error) {
branchRemote, _ := git.Config(fmt.Sprintf("branch.%s.remote", branch.ShortName()))
if branchRemote == "" || branchRemote == "." {
return nil, nil, fmt.Errorf("branch has no upstream configuration")
}
branchMerge, err := git.Config(fmt.Sprintf("branch.%s.merge", branch.ShortName()))
if err != nil {
return nil, nil, err
}
headBranch := &github.Branch{
Repo: branch.Repo,
Name: branchMerge,
}
if headRemote, err := branch.Repo.RemoteByName(branchRemote); err == nil {
headProject, err := headRemote.Project()
if err != nil {
return nil, nil, err
}
return headBranch, headProject, nil
}
remoteUrl, err := git.ParseURL(branchRemote)
if err != nil {
return nil, nil, err
}
headProject, err := github.NewProjectFromURL(remoteUrl)
if err != nil {
return nil, nil, err
}
return headBranch, headProject, nil
}
func deducePushTarget(branch *github.Branch, owner string) (*github.Project, error) {
remote := branch.Repo.RemoteForBranch(branch, owner)
if remote == nil {
return nil, fmt.Errorf("no remote found for branch %s", branch.ShortName())
}
return remote.Project()
}
func formatPullRequest(pr github.PullRequest, format string, colorize bool) string {
placeholders := formatIssuePlaceholders(github.Issue(pr), colorize)
for key, value := range formatPullRequestPlaceholders(pr, colorize) {
......
......@@ -90,6 +90,11 @@ pull-request -i <ISSUE>
-d, --draft
Create the pull request as a draft.
--no-maintainer-edits
When creating a pull request from a fork, this disallows projects
maintainers from being able to push to the head branch of this fork.
Maintainer edits are allowed by default.
## Examples:
$ hub pull-request
[ opens a text editor for writing title and message ]
......@@ -313,17 +318,8 @@ of text is the title and the rest is the description.`, fullBase, fullHead))
}
}
milestoneNumber := 0
if flagPullRequestMilestone := args.Flag.Value("--milestone"); flagPullRequestMilestone != "" {
// BC: Don't try to resolve milestone name if it's an integer
milestoneNumber, err = strconv.Atoi(flagPullRequestMilestone)
if err != nil {
milestones, err := client.FetchMilestones(baseProject)
utils.Check(err)
milestoneNumber, err = findMilestoneNumber(milestones, flagPullRequestMilestone)
utils.Check(err)
}
}
milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), client, baseProject)
utils.Check(err)
var pullRequestURL string
if args.Noop {
......@@ -331,8 +327,9 @@ of text is the title and the rest is the description.`, fullBase, fullHead))
pullRequestURL = "PULL_REQUEST_URL"
} else {
params := map[string]interface{}{
"base": base,
"head": fullHead,
"base": base,
"head": fullHead,
"maintainer_can_modify": !args.Flag.Bool("--no-maintainer-edits"),
}
if args.Flag.Bool("--draft") {
......@@ -469,16 +466,6 @@ func parsePullRequestIssueNumber(url string) string {
return ""
}
func findMilestoneNumber(milestones []github.Milestone, name string) (int, error) {
for _, milestone := range milestones {
if strings.EqualFold(milestone.Title, name) {
return milestone.Number, nil
}
}
return 0, fmt.Errorf("error: no milestone found with name '%s'", name)
}
func commaSeparated(l []string) []string {
res := []string{}
for _, i := range l {
......
......@@ -116,6 +116,34 @@ Feature: hub issue
"""
When I successfully run `hub issue -M none`
Scenario: Fetch issues assigned to milestone by number
Given the GitHub API server:
"""
get('/repos/github/hub/issues') {
assert :milestone => "12"
json []
}
"""
When I successfully run `hub issue -M 12`
Scenario: Fetch issues assigned to milestone by name
Given the GitHub API server:
"""
get('/repos/github/hub/milestones') {
status 200
json [
{ :number => 237, :title => "prerelease" },
{ :number => 1337, :title => "v1" },
{ :number => 41319, :title => "Hello World!" }
]
}
get('/repos/github/hub/issues') {
assert :milestone => "1337"
json []
}
"""
When I successfully run `hub issue -M v1`
Scenario: Fetch issues created by a given user
Given the GitHub API server:
"""
......@@ -381,6 +409,29 @@ Feature: hub issue
https://github.com/github/hub/issues/1337\n
"""
Scenario: Create an issue with milestone by name
Given the GitHub API server:
"""
get('/repos/github/hub/milestones') {
status 200
json [
{ :number => 237, :title => "prerelease" },
{ :number => 1337, :title => "v1" },
{ :number => 41319, :title => "Hello World!" }
]
}
post('/repos/github/hub/issues') {
assert :milestone => 41319
status 201
json :html_url => "https://github.com/github/hub/issues/1337"
}
"""
When I successfully run `hub issue create -m "hello" -M "hello world!"`
Then the output should contain exactly:
"""
https://github.com/github/hub/issues/1337\n
"""
Scenario: Editing empty issue message
Given the git commit editor is "vim"
And the text editor adds:
......
Feature: hub pr show
Background:
Given I am in "git://github.com/ashemesh/hub.git" git repo
And I am "ashemesh" on github.com with OAuth token "OTOKEN"
Scenario: Current branch
Given I am on the "topic" branch
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :state => "open",
:head => "ashemesh:topic"
json [
{ :html_url => "https://github.com/ashemesh/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show`
Then "open https://github.com/ashemesh/hub/pull/102" should be run
Scenario: Current branch output URL
Given I am on the "topic" branch
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :state => "open",
:head => "ashemesh:topic"
json [
{ :html_url => "https://github.com/ashemesh/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show -u`
Then "open https://github.com/ashemesh/hub/pull/102" should not be run
And the output should contain exactly:
"""
https://github.com/ashemesh/hub/pull/102\n
"""
Scenario: Current branch in fork
Given the "upstream" remote has url "git@github.com:github/hub.git"
And I am on the "topic" branch pushed to "origin/topic"
Given the GitHub API server:
"""
get('/repos/github/hub/pulls'){
assert :state => "open",
:head => "ashemesh:topic"
json [
{ :html_url => "https://github.com/github/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show`
Then "open https://github.com/github/hub/pull/102" should be run
Scenario: Differently named branch in fork
Given the "upstream" remote has url "git@github.com:github/hub.git"
And I am on the "local-topic" branch with upstream "origin/remote-topic"
Given the GitHub API server:
"""
get('/repos/github/hub/pulls'){
assert :head => "ashemesh:remote-topic"
json [
{ :html_url => "https://github.com/github/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show`
Then "open https://github.com/github/hub/pull/102" should be run
Scenario: Upstream configuration with HTTPS URL
Given I am on the "local-topic" branch
When I successfully run `git config branch.local-topic.remote https://github.com/octocat/hub.git`
When I successfully run `git config branch.local-topic.merge refs/remotes/remote-topic`
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :head => "octocat:remote-topic"
json [
{ :html_url => "https://github.com/github/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show`
Then "open https://github.com/github/hub/pull/102" should be run
Scenario: Upstream configuration with SSH URL
Given I am on the "local-topic" branch
When I successfully run `git config branch.local-topic.remote git@github.com:octocat/hub.git`
When I successfully run `git config branch.local-topic.merge refs/remotes/remote-topic`
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :head => "octocat:remote-topic"
json [
{ :html_url => "https://github.com/github/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show`
Then "open https://github.com/github/hub/pull/102" should be run
Scenario: Explicit head branch
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :state => "open",
:head => "ashemesh:topic"
json [
{ :html_url => "https://github.com/ashemesh/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show --head topic`
Then "open https://github.com/ashemesh/hub/pull/102" should be run
Scenario: Explicit head branch with owner
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
assert :state => "open",
:head => "github:topic"
json [
{ :html_url => "https://github.com/ashemesh/hub/pull/102" },
]
}
"""
When I successfully run `hub pr show --head github:topic`
Then "open https://github.com/ashemesh/hub/pull/102" should be run
Scenario: No pull request found
Given the GitHub API server:
"""
get('/repos/ashemesh/hub/pulls'){
json []
}
"""
When I run `hub pr show --head topic`
Then the exit status should be 1
And the stderr should contain exactly:
"""
no open pull requests found for branch 'ashemesh:topic'\n
"""
Scenario: Show pull request by number
When I successfully run `hub pr show 102`
Then "open https://github.com/ashemesh/hub/pull/102" should be run
Scenario: Show pull request by invalid number
When I run `hub pr show XYZ`
Then the exit status should be 1
And the stderr should contain exactly:
"""
invalid pull request number: 'XYZ'\n
"""
......@@ -7,13 +7,15 @@ Feature: hub pull-request
Scenario: Basic pull request
Given the GitHub API server:
"""
KNOWN_PARAMS = %w[title body base head draft issue maintainer_can_modify]
post('/repos/mislav/coral/pulls') {
halt 400 unless request.env['HTTP_ACCEPT'] == 'application/vnd.github.shadow-cat-preview+json;charset=utf-8'
halt 400 if (params.keys - %w[title body base head draft issue]).any?
halt 400 if (params.keys - KNOWN_PARAMS).any?
assert :title => 'hello',
:body => nil,
:base => 'master',
:head => 'mislav:master',
:maintainer_can_modify => true,
:draft => nil,
:issue => nil
status 201
......@@ -1295,3 +1297,15 @@ Feature: hub pull-request
"""
When I successfully run `hub pull-request -d -m wip`
Then the output should contain exactly "the://url\n"
Scenario: Disallow edits from maintainers
Given the GitHub API server:
"""
post('/repos/mislav/coral/pulls') {
assert :maintainer_can_modify => false
status 201
json :html_url => "the://url"
}
"""
When I successfully run `hub pull-request -m hello --no-maintainer-edits`
Then the output should contain exactly "the://url\n"
......@@ -23,6 +23,7 @@ func (b *Branch) LongName() string {
return reg.ReplaceAllString(b.Name, "")
}
// see also RemoteForBranch()
func (b *Branch) PushTarget(owner string, preferUpstream bool) (branch *Branch) {
var err error
pushDefault, _ := git.Config("push.default")
......
......@@ -44,14 +44,7 @@ func (client *Client) FetchPullRequests(project *Project, filterParams map[strin
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()
path = addQuery(path, filterParams)
}
pulls = []PullRequest{}
......@@ -642,14 +635,7 @@ func (client *Client) FetchIssues(project *Project, filterParams map[string]inte
path := fmt.Sprintf("repos/%s/%s/issues?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()
path = addQuery(path, filterParams)
}
issues = []Issue{}
......
......@@ -156,6 +156,17 @@ func (r *GitHubRepo) RemoteBranchAndProject(owner string, preferUpstream bool) (
return
}
// duplicates logic from PushTarget()
func (r *GitHubRepo) RemoteForBranch(branch *Branch, owner string) *Remote {
branchName := branch.ShortName()
for _, remote := range r.remotesForPublish(owner) {
if git.HasFile("refs", "remotes", remote.Name, branchName) {
return &remote
}
}
return nil
}
func (r *GitHubRepo) RemoteForRepo(repo *Repository) (*Remote, error) {
if err := r.loadRemotes(); err != nil {
return nil, err
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册