diff --git a/commands/issue.go b/commands/issue.go index 36bd259303558c94df3a70ca9ebc814e14646930..29d5aa4c61547dcf2b589c1a23c9ffcc21e9e731 100644 --- a/commands/issue.go +++ b/commands/issue.go @@ -20,6 +20,7 @@ var ( issue [-a ] [-c ] [-@ ] [-s ] [-f ] [-M ] [-l ] [-d ] [-o [-^]] [-L ] issue show [-f ] issue create [-oc] [-m |-F ] [--edit] [-a ] [-M ] [-l ] +issue update [-m |-F ] [--edit] [-a ] [-M ] [-l ] [-s ] issue labels [--color] issue transfer `, @@ -35,6 +36,10 @@ With no arguments, show a list of open issues. * _create_: Open an issue in the current repository. + * _update_: + Update fields of an existing issue specified by . Use ''--edit'' + to edit the title and message interactively in the text editor. + * _labels_: List the labels available in this repository. @@ -227,6 +232,20 @@ hub-pr(1), hub(1) Key: "transfer", Run: transferIssue, } + + cmdUpdate = &Command{ + Key: "update", + Run: updateIssue, + KnownFlags: ` + -m, --message MSG + -F, --file FILE + -M, --milestone NAME + -l, --labels LIST + -a, --assign USER + -e, --edit + -s, --state STATE +`, + } ) func init() { @@ -234,6 +253,7 @@ func init() { cmdIssue.Use(cmdCreateIssue) cmdIssue.Use(cmdLabel) cmdIssue.Use(cmdTransfer) + cmdIssue.Use(cmdUpdate) CmdRunner.Use(cmdIssue) } @@ -587,21 +607,11 @@ text is the title and the rest is the description.`, project)) "body": body, } - flagIssueLabels := commaSeparated(args.Flag.AllValues("--labels")) - if len(flagIssueLabels) > 0 { - params["labels"] = flagIssueLabels - } + setLabelsFromArgs(params, args) - flagIssueAssignees := commaSeparated(args.Flag.AllValues("--assign")) - if len(flagIssueAssignees) > 0 { - params["assignees"] = flagIssueAssignees - } + setAssigneesFromArgs(params, args) - milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project) - utils.Check(err) - if milestoneNumber > 0 { - params["milestone"] = milestoneNumber - } + setMilestoneFromArgs(params, args, gh, project) args.NoForward() if args.Noop { @@ -618,6 +628,79 @@ text is the title and the rest is the description.`, project)) messageBuilder.Cleanup() } +func updateIssue(cmd *Command, args *Args) { + issueNumber := 0 + if args.ParamsSize() > 0 { + issueNumber, _ = strconv.Atoi(args.GetParam(0)) + } + if issueNumber == 0 { + utils.Check(cmd.UsageError("")) + } + if !hasField(args, "--message", "--file", "--labels", "--milestone", "--assign", "--state", "--edit") { + utils.Check(cmd.UsageError("please specify fields to update")) + } + + localRepo, err := github.LocalRepo() + utils.Check(err) + + project, err := localRepo.MainProject() + utils.Check(err) + + gh := github.NewClient(project.Host) + + params := map[string]interface{}{} + setLabelsFromArgs(params, args) + setAssigneesFromArgs(params, args) + setMilestoneFromArgs(params, args, gh, project) + + if args.Flag.HasReceived("--state") { + params["state"] = args.Flag.Value("--state") + } + + if hasField(args, "--message", "--file", "--edit") { + messageBuilder := &github.MessageBuilder{ + Filename: "ISSUE_EDITMSG", + Title: "issue", + } + + messageBuilder.AddCommentedSection(fmt.Sprintf(`Editing issue #%d for %s + +Update the message for this issue. The first block of +text is the title and the rest is the description.`, issueNumber, project)) + + messageBuilder.Edit = args.Flag.Bool("--edit") + flagIssueMessage := args.Flag.AllValues("--message") + if len(flagIssueMessage) > 0 { + messageBuilder.Message = strings.Join(flagIssueMessage, "\n\n") + } else if args.Flag.HasReceived("--file") { + messageBuilder.Message, err = msgFromFile(args.Flag.Value("--file")) + utils.Check(err) + } else { + issue, err := gh.FetchIssue(project, strconv.Itoa(issueNumber)) + utils.Check(err) + existingMessage := fmt.Sprintf("%s\n\n%s", issue.Title, issue.Body) + messageBuilder.Message = strings.Replace(existingMessage, "\r\n", "\n", -1) + } + + title, body, err := messageBuilder.Extract() + utils.Check(err) + if title == "" { + utils.Check(fmt.Errorf("Aborting creation due to empty issue title")) + } + params["title"] = title + params["body"] = body + defer messageBuilder.Cleanup() + } + + args.NoForward() + if args.Noop { + ui.Printf("Would update issue #%d for %s\n", issueNumber, project) + } else { + err := gh.UpdateIssue(project, issueNumber, params) + utils.Check(err) + } +} + func listLabels(cmd *Command, args *Args) { localRepo, err := github.LocalRepo() utils.Check(err) @@ -642,6 +725,43 @@ func listLabels(cmd *Command, args *Args) { } } +func hasField(args *Args, names ...string) bool { + found := false + for _, name := range names { + if args.Flag.HasReceived(name) { + found = true + } + } + return found +} + +func setLabelsFromArgs(params map[string]interface{}, args *Args) { + if !args.Flag.HasReceived("--labels") { + return + } + params["labels"] = commaSeparated(args.Flag.AllValues("--labels")) +} + +func setAssigneesFromArgs(params map[string]interface{}, args *Args) { + if !args.Flag.HasReceived("--assign") { + return + } + params["assignees"] = commaSeparated(args.Flag.AllValues("--assign")) +} + +func setMilestoneFromArgs(params map[string]interface{}, args *Args, gh *github.Client, project *github.Project) { + if !args.Flag.HasReceived("--milestone") { + return + } + milestoneNumber, err := milestoneValueToNumber(args.Flag.Value("--milestone"), gh, project) + utils.Check(err) + if milestoneNumber == 0 { + params["milestone"] = nil + } else { + params["milestone"] = milestoneNumber + } +} + func colorizeOutput(colorSet bool, when string) bool { if !colorSet || when == "auto" { colorConfig, _ := git.Config("color.ui") diff --git a/commands/pull_request.go b/commands/pull_request.go index 18812771635cb2c71c654da08db0448b976c1e56..1ad743d7173e5f8c16c1a6e3e4feb9b0beebea92 100644 --- a/commands/pull_request.go +++ b/commands/pull_request.go @@ -475,6 +475,9 @@ func parsePullRequestIssueNumber(url string) string { func commaSeparated(l []string) []string { res := []string{} for _, i := range l { + if i == "" { + continue + } res = append(res, strings.Split(i, ",")...) } return res diff --git a/commands/release.go b/commands/release.go index f65f012e7c2cd7220b22c9a56fb95e79cc46ecb4..251fa1f33f89996d114c648b05ce6afce036e66f 100644 --- a/commands/release.go +++ b/commands/release.go @@ -596,7 +596,7 @@ text is the title and the rest is the description.`, tagName, project)) messageBuilder.Edit = args.Flag.Bool("--edit") } else { messageBuilder.Edit = true - messageBuilder.Message = fmt.Sprintf("%s\n\n%s", release.Name, release.Body) + messageBuilder.Message = strings.Replace(fmt.Sprintf("%s\n\n%s", release.Name, release.Body), "\r\n", "\n", -1) } title, body, err := messageBuilder.Extract() diff --git a/features/issue.feature b/features/issue.feature index 26c0747a1bc88f859ea1578c0ba3b682b7093286..2759ee485eaaf131d5d55caec8712f5368d22766 100644 --- a/features/issue.feature +++ b/features/issue.feature @@ -593,6 +593,165 @@ Feature: hub issue https://github.com/github/hub/issues/1337\n """ + Scenario: Update an issue's title + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "Not workie, pls fix", + :body => "", + :milestone => :no, + :assignees => :no, + :labels => :no, + :state => :no + } + """ + Then I successfully run `hub issue update 1337 -m "Not workie, pls fix"` + + Scenario: Update an issue's state + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :labels => :no, + :state => "closed" + } + """ + Then I successfully run `hub issue update 1337 -s closed` + + Scenario: Update an issue's labels + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => :no, + :assignees => :no, + :labels => ["bug", "important"] + } + """ + Then I successfully run `hub issue update 1337 -l bug,important` + + Scenario: Update an issue's milestone + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => 42, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -M 42` + + Scenario: Upate an issue's milestone by name + Given the GitHub API server: + """ + get('/repos/github/hub/milestones') { + status 200 + json [ + { :number => 237, :title => "prerelease" }, + { :number => 42, :title => "Hello World!" } + ] + } + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => 42, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -M "hello world!"` + + Scenario: Update an issue's assignees + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => :no, + :assignees => ["Cornwe19"], + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -a Cornwe19` + + Scenario: Update an issue's title, labels, milestone, and assignees + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "Not workie, pls fix", + :body => "", + :milestone => 42, + :assignees => ["Cornwe19"], + :labels => ["bug", "important"] + } + """ + Then I successfully run `hub issue update 1337 -m "Not workie, pls fix" -M 42 -l bug,important -a Cornwe19` + + Scenario: Clear existing issue labels, assignees, milestone + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => :no, + :body => :no, + :milestone => nil, + :assignees => [], + :labels => [] + } + """ + Then I successfully run `hub issue update 1337 --milestone= --assign= --labels=` + + Scenario: Update an issue's title and body manually + Given the git commit editor is "vim" + And the text editor adds: + """ + My new title + """ + Given the GitHub API server: + """ + get('/repos/github/hub/issues/1337') { + json \ + :number => 1337, + :title => "My old title", + :body => "My old body" + } + patch('/repos/github/hub/issues/1337') { + assert :title => "My new title", + :body => "My old title\n\nMy old body", + :milestone => :no, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 --edit` + + Scenario: Update an issue's title and body via a file + Given a file named "my-issue.md" with: + """ + My new title + + My new body + """ + Given the GitHub API server: + """ + patch('/repos/github/hub/issues/1337') { + assert :title => "My new title", + :body => "My new body", + :milestone => :no, + :assignees => :no, + :labels => :no + } + """ + Then I successfully run `hub issue update 1337 -F my-issue.md` + + Scenario: Update an issue without specifying fields to update + When I run `hub issue update 1337` + Then the exit status should be 1 + Then the stderr should contain "please specify fields to update" + Then the stderr should contain "Usage: hub issue" + Scenario: Fetch issue labels Given the GitHub API server: """