| @@ -17,16 +17,20 @@ const maxBodyBytes = 64 << 10 |
| 17 | func init() { |
17 | func init() { |
| 18 | register(Command{Path: []string{"issue", "create"}, |
18 | register(Command{Path: []string{"issue", "create"}, |
| 19 | Summary: "open an issue", |
19 | Summary: "open an issue", |
| 20 | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]", |
20 | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org] [--label <l>]... [--milestone <title>] [--assignee <user>]...", |
| 21 | Flags: []Flag{ |
21 | Flags: []Flag{ |
| 22 | {"--title", "<t>", "the issue's title", ""}, |
22 | {"--title", "<t>", "the issue's title", ""}, |
| 23 | {"--body", "<b>", "the issue's body", ""}, |
23 | {"--body", "<b>", "the issue's body", ""}, |
| 24 | {"--file", "-", "read the body from stdin", ""}, |
24 | {"--file", "-", "read the body from stdin", ""}, |
| 25 | {"--format", "md|org", "the body's markup", "md"}, |
25 | {"--format", "md|org", "the body's markup", "md"}, |
| |
26 | {"--label", "<l>", "label to add, may repeat", ""}, |
| |
27 | {"--milestone", "<title>", "milestone to set", ""}, |
| |
28 | {"--assignee", "<user>", "user to assign, may repeat", ""}, |
| 26 | }, |
29 | }, |
| 27 | Examples: []string{ |
30 | Examples: []string{ |
| 28 | `issue create krz/gitbay --title "crash on empty repo" --body "steps to reproduce..."`, |
31 | `issue create krz/gitbay --title "crash on empty repo" --body "steps to reproduce..."`, |
| 29 | "issue create krz/gitbay --title notes --file - < notes.md", |
32 | "issue create krz/gitbay --title notes --file - < notes.md", |
| |
33 | "issue create krz/gitbay --title bug --label bug --label priority --milestone v1 --assignee cmc", |
| 30 | }, |
34 | }, |
| 31 | ReadsStdin: true, Run: runIssueCreate}) |
35 | ReadsStdin: true, Run: runIssueCreate}) |
| 32 | register(Command{Path: []string{"issue", "list"}, |
36 | register(Command{Path: []string{"issue", "list"}, |
| @@ -177,9 +181,15 @@ func issueToOut(i store.Issue, withBody bool) issueOut { |
| 177 | return o |
181 | return o |
| 178 | } |
182 | } |
| 179 | |
183 | |
| |
184 | // runIssueCreate opens an issue. The CLI opens $EDITOR for the body |
| |
185 | // when neither --body nor --file is given (cmd/gitbay's issueCmd, |
| |
186 | // editor: "issue"); over stock ssh the body must be one of the two. |
| 180 | func runIssueCreate(c *Ctx, args []string) int { |
187 | func runIssueCreate(c *Ctx, args []string) int { |
| 181 | f, err := c.parseArgs(args, flagSpec{Values: []string{"--format", "--title", "--body", "--file"}, MaxPos: 1, |
188 | f, err := c.parseArgs(args, flagSpec{ |
| 182 | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]"}) |
189 | Values: []string{"--format", "--title", "--body", "--file", "--milestone"}, |
| |
190 | Multi: []string{"--label", "--assignee"}, |
| |
191 | MaxPos: 1, |
| |
192 | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org] [--label <l>]... [--milestone <title>] [--assignee <user>]..."}) |
| 183 | if err != nil { |
193 | if err != nil { |
| 184 | return c.fail(protocol.ExitUsage, "%v", err) |
194 | return c.fail(protocol.ExitUsage, "%v", err) |
| 185 | } |
195 | } |
| @@ -202,6 +212,18 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 202 | if code := refuseArchived(c, repo); code >= 0 { |
212 | if code := refuseArchived(c, repo); code >= 0 { |
| 203 | return code |
213 | return code |
| 204 | } |
214 | } |
| |
215 | // Filing an issue only needs read access; setting a label, milestone |
| |
216 | // or assignee on it needs the same write access issue label/issue |
| |
217 | // milestone/issue assign require. |
| |
218 | if len(f.List("--label")) > 0 || f.Value("--milestone") != "" || len(f.List("--assignee")) > 0 { |
| |
219 | grant, err := c.Store.AccessRole(repo.ID, c.User.ID) |
| |
220 | if err != nil { |
| |
221 | return c.fail(protocol.ExitFailure, "checking access: %v", err) |
| |
222 | } |
| |
223 | if !policy.CanWrite(c.User, repo, grant) { |
| |
224 | return c.fail(protocol.ExitDenied, "permission denied on %s; ask its owner for access", path) |
| |
225 | } |
| |
226 | } |
| 205 | b, err := bodyFrom(c, body, file) |
227 | b, err := bodyFrom(c, body, file) |
| 206 | if err != nil { |
228 | if err != nil { |
| 207 | return c.failInput(err) |
229 | return c.failInput(err) |
| @@ -217,8 +239,36 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 217 | action: fmt.Sprintf("opened issue #%d", n), |
239 | action: fmt.Sprintf("opened issue #%d", n), |
| 218 | excerpt: b, path: fmt.Sprintf("%s/issues/%d", repo.Path(), n)}) |
240 | excerpt: b, path: fmt.Sprintf("%s/issues/%d", repo.Path(), n)}) |
| 219 | } |
241 | } |
| 220 | if issue, err := c.Store.IssueByNumber(repo.ID, n); err == nil { |
242 | issue, err := c.Store.IssueByNumber(repo.ID, n) |
| 221 | notifyMentions(c, repo, issueThread, issue.ID, n, title, b) |
243 | if err != nil { |
| |
244 | return c.fail(protocol.ExitFailure, "%v", err) |
| |
245 | } |
| |
246 | notifyMentions(c, repo, issueThread, issue.ID, n, title, b) |
| |
247 | for _, l := range f.List("--label") { |
| |
248 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, true); err != nil { |
| |
249 | return c.fail(protocol.ExitFailure, "%v", err) |
| |
250 | } |
| |
251 | } |
| |
252 | if m := f.Value("--milestone"); m != "" { |
| |
253 | ms, err := c.Store.MilestoneByTitle(repo, m) |
| |
254 | if err != nil { |
| |
255 | return milestoneErr(c, repo, m, err) |
| |
256 | } |
| |
257 | if err := c.Store.SetIssueMilestone(issue.ID, ms.ID); err != nil { |
| |
258 | return c.fail(protocol.ExitFailure, "%v", err) |
| |
259 | } |
| |
260 | } |
| |
261 | for _, name := range f.List("--assignee") { |
| |
262 | u, err := c.Store.UserByUsername(name) |
| |
263 | if errors.Is(err, store.ErrNotFound) { |
| |
264 | return c.fail(protocol.ExitNotFound, "no such user %q", name) |
| |
265 | } |
| |
266 | if err != nil { |
| |
267 | return c.fail(protocol.ExitFailure, "%v", err) |
| |
268 | } |
| |
269 | if err := c.Store.SetIssueAssignee(issue.ID, u.ID, true); err != nil { |
| |
270 | return c.fail(protocol.ExitFailure, "%v", err) |
| |
271 | } |
| 222 | } |
272 | } |
| 223 | return c.emit(Created{Number: n}, func(w io.Writer) { |
273 | return c.emit(Created{Number: n}, func(w io.Writer) { |
| 224 | fmt.Fprintf(w, "created %s#%d\n", repo.Path(), n) |
274 | fmt.Fprintf(w, "created %s#%d\n", repo.Path(), n) |