| @@ -17,16 +17,20 @@ const maxBodyBytes = 64 << 10 |
| 17 | 17 | func init() { |
| 18 | 18 | register(Command{Path: []string{"issue", "create"}, |
| 19 | 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 | 21 | Flags: []Flag{ |
| 22 | 22 | {"--title", "<t>", "the issue's title", ""}, |
| 23 | 23 | {"--body", "<b>", "the issue's body", ""}, |
| 24 | 24 | {"--file", "-", "read the body from stdin", ""}, |
| 25 | 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 | 30 | Examples: []string{ |
| 28 | 31 | `issue create krz/gitbay --title "crash on empty repo" --body "steps to reproduce..."`, |
| 29 | 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 | 35 | ReadsStdin: true, Run: runIssueCreate}) |
| 32 | 36 | register(Command{Path: []string{"issue", "list"}, |
| @@ -177,9 +181,15 @@ func issueToOut(i store.Issue, withBody bool) issueOut { |
| 177 | 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 | 187 | func runIssueCreate(c *Ctx, args []string) int { |
| 181 | | f, err := c.parseArgs(args, flagSpec{Values: []string{"--format", "--title", "--body", "--file"}, MaxPos: 1, |
| 182 | | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]"}) |
| 188 | f, err := c.parseArgs(args, flagSpec{ |
| 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 | 193 | if err != nil { |
| 184 | 194 | return c.fail(protocol.ExitUsage, "%v", err) |
| 185 | 195 | } |
| @@ -202,6 +212,18 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 202 | 212 | if code := refuseArchived(c, repo); code >= 0 { |
| 203 | 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 | 227 | b, err := bodyFrom(c, body, file) |
| 206 | 228 | if err != nil { |
| 207 | 229 | return c.failInput(err) |
| @@ -217,8 +239,36 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 217 | 239 | action: fmt.Sprintf("opened issue #%d", n), |
| 218 | 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 { |
| 221 | | notifyMentions(c, repo, issueThread, issue.ID, n, title, b) |
| 242 | issue, err := c.Store.IssueByNumber(repo.ID, n) |
| 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 | 273 | return c.emit(Created{Number: n}, func(w io.Writer) { |
| 224 | 274 | fmt.Fprintf(w, "created %s#%d\n", repo.Path(), n) |