| @@ -224,6 +224,18 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 224 | return c.fail(protocol.ExitDenied, "permission denied on %s; ask its owner for access", path) |
224 | return c.fail(protocol.ExitDenied, "permission denied on %s; ask its owner for access", path) |
| 225 | } |
225 | } |
| 226 | } |
226 | } |
| |
227 | // Resolve everything that can be refused before the issue exists, so |
| |
228 | // a typo in a milestone or an assignee creates nothing. |
| |
229 | var milestone store.Milestone |
| |
230 | if m := f.Value("--milestone"); m != "" { |
| |
231 | if milestone, err = c.Store.MilestoneByTitle(repo, m); err != nil { |
| |
232 | return milestoneErr(c, repo, m, err) |
| |
233 | } |
| |
234 | } |
| |
235 | assignees, code := resolveUsers(c, f.List("--assignee")) |
| |
236 | if code >= 0 { |
| |
237 | return code |
| |
238 | } |
| 227 | b, err := bodyFrom(c, body, file) |
239 | b, err := bodyFrom(c, body, file) |
| 228 | if err != nil { |
240 | if err != nil { |
| 229 | return c.failInput(err) |
241 | return c.failInput(err) |
| @@ -244,30 +256,21 @@ func runIssueCreate(c *Ctx, args []string) int { |
| 244 | return c.fail(protocol.ExitFailure, "%v", err) |
256 | return c.fail(protocol.ExitFailure, "%v", err) |
| 245 | } |
257 | } |
| 246 | notifyMentions(c, repo, issueThread, issue.ID, n, title, b) |
258 | notifyMentions(c, repo, issueThread, issue.ID, n, title, b) |
| 247 | for _, l := range f.List("--label") { |
259 | if labels := f.List("--label"); len(labels) > 0 { |
| 248 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, true); err != nil { |
260 | if _, code := labelIssue(c, repo, issue, labels, nil); code >= 0 { |
| 249 | return c.fail(protocol.ExitFailure, "%v", err) |
261 | return code |
| 250 | } |
262 | } |
| 251 | } |
263 | } |
| 252 | if m := f.Value("--milestone"); m != "" { |
264 | if milestone.ID != 0 { |
| 253 | ms, err := c.Store.MilestoneByTitle(repo, m) |
265 | if err := recordItemMilestone(c, repo, "issue", n, milestone.ID, milestone.Title, func(id int64) error { |
| 254 | if err != nil { |
266 | return c.Store.SetIssueMilestone(issue.ID, id) |
| 255 | return milestoneErr(c, repo, m, err) |
267 | }); err != nil { |
| 256 | } |
| |
| 257 | if err := c.Store.SetIssueMilestone(issue.ID, ms.ID); err != nil { |
| |
| 258 | return c.fail(protocol.ExitFailure, "%v", err) |
268 | return c.fail(protocol.ExitFailure, "%v", err) |
| 259 | } |
269 | } |
| 260 | } |
270 | } |
| 261 | for _, name := range f.List("--assignee") { |
271 | if len(assignees) > 0 { |
| 262 | u, err := c.Store.UserByUsername(name) |
272 | if _, code := assignIssue(c, repo, issue, assignees, nil); code >= 0 { |
| 263 | if errors.Is(err, store.ErrNotFound) { |
273 | return code |
| 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 | } |
274 | } |
| 272 | } |
275 | } |
| 273 | return c.emit(Created{Number: n}, func(w io.Writer) { |
276 | return c.emit(Created{Number: n}, func(w io.Writer) { |
| @@ -523,28 +526,39 @@ func runIssueLabel(c *Ctx, args []string) int { |
| 523 | if code := refuseArchived(c, repo); code >= 0 { |
526 | if code := refuseArchived(c, repo); code >= 0 { |
| 524 | return code |
527 | return code |
| 525 | } |
528 | } |
| |
529 | labels, code := labelIssue(c, repo, issue, adds, removes) |
| |
530 | if code >= 0 { |
| |
531 | return code |
| |
532 | } |
| |
533 | return c.emit(map[string]any{"number": issue.Number, "labels": labels}, func(w io.Writer) { |
| |
534 | fmt.Fprintf(w, "labels on %s#%d: %s\n", repo.Path(), issue.Number, strings.Join(labels, ", ")) |
| |
535 | }) |
| |
536 | } |
| |
537 | |
| |
538 | // labelIssue adds and removes labels on issue and records the |
| |
539 | // issue.labeled event, returning the labels it carries afterwards. It |
| |
540 | // backs issue label and issue create --label. |
| |
541 | func labelIssue(c *Ctx, repo store.Repo, issue store.Issue, adds, removes []string) ([]string, int) { |
| 526 | for _, l := range adds { |
542 | for _, l := range adds { |
| 527 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, true); err != nil { |
543 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, true); err != nil { |
| 528 | return c.fail(protocol.ExitFailure, "%v", err) |
544 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 529 | } |
545 | } |
| 530 | } |
546 | } |
| 531 | for _, l := range removes { |
547 | for _, l := range removes { |
| 532 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, false); err != nil { |
548 | if err := c.Store.SetIssueLabel(repo, issue.ID, l, false); err != nil { |
| 533 | if errors.Is(err, store.ErrNotFound) { |
549 | if errors.Is(err, store.ErrNotFound) { |
| 534 | return c.fail(protocol.ExitNotFound, "%v", err) |
550 | return nil, c.fail(protocol.ExitNotFound, "%v", err) |
| 535 | } |
551 | } |
| 536 | return c.fail(protocol.ExitFailure, "%v", err) |
552 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 537 | } |
553 | } |
| 538 | } |
554 | } |
| 539 | updated, err := c.Store.IssueByNumber(repo.ID, issue.Number) |
555 | updated, err := c.Store.IssueByNumber(repo.ID, issue.Number) |
| 540 | if err != nil { |
556 | if err != nil { |
| 541 | return c.fail(protocol.ExitFailure, "%v", err) |
557 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 542 | } |
558 | } |
| 543 | c.Store.RecordEvent(repo.ID, c.User.ID, "issue.labeled", |
559 | c.Store.RecordEvent(repo.ID, c.User.ID, "issue.labeled", |
| 544 | fmt.Sprintf(`{"number":%d,"labels":%s}`, issue.Number, jsonStrings(updated.Labels))) |
560 | fmt.Sprintf(`{"number":%d,"labels":%s}`, issue.Number, jsonStrings(updated.Labels))) |
| 545 | return c.emit(map[string]any{"number": issue.Number, "labels": updated.Labels}, func(w io.Writer) { |
561 | return updated.Labels, -1 |
| 546 | fmt.Fprintf(w, "labels on %s#%d: %s\n", repo.Path(), issue.Number, strings.Join(updated.Labels, ", ")) |
| |
| 547 | }) |
| |
| 548 | } |
562 | } |
| 549 | |
563 | |
| 550 | func runIssueAssign(c *Ctx, args []string) int { |
564 | func runIssueAssign(c *Ctx, args []string) int { |
| @@ -562,16 +576,44 @@ func runIssueAssign(c *Ctx, args []string) int { |
| 562 | if code := refuseArchived(c, repo); code >= 0 { |
576 | if code := refuseArchived(c, repo); code >= 0 { |
| 563 | return code |
577 | return code |
| 564 | } |
578 | } |
| 565 | resolve := func(name string) (store.User, int) { |
579 | add, code := resolveUsers(c, adds) |
| |
580 | if code >= 0 { |
| |
581 | return code |
| |
582 | } |
| |
583 | remove, code := resolveUsers(c, removes) |
| |
584 | if code >= 0 { |
| |
585 | return code |
| |
586 | } |
| |
587 | assignees, code := assignIssue(c, repo, issue, add, remove) |
| |
588 | if code >= 0 { |
| |
589 | return code |
| |
590 | } |
| |
591 | return c.emit(map[string]any{"number": issue.Number, "assignees": assignees}, func(w io.Writer) { |
| |
592 | fmt.Fprintf(w, "assignees on %s#%d: %s\n", repo.Path(), issue.Number, strings.Join(assignees, ", ")) |
| |
593 | }) |
| |
594 | } |
| |
595 | |
| |
596 | // resolveUsers looks up every name, failing on the first that does not |
| |
597 | // exist, so a typo changes nothing. |
| |
598 | func resolveUsers(c *Ctx, names []string) ([]store.User, int) { |
| |
599 | users := make([]store.User, 0, len(names)) |
| |
600 | for _, name := range names { |
| 566 | u, err := c.Store.UserByUsername(name) |
601 | u, err := c.Store.UserByUsername(name) |
| 567 | if errors.Is(err, store.ErrNotFound) { |
602 | if errors.Is(err, store.ErrNotFound) { |
| 568 | return u, c.fail(protocol.ExitNotFound, "no such user %q", name) |
603 | return nil, c.fail(protocol.ExitNotFound, "no such user %q", name) |
| 569 | } |
604 | } |
| 570 | if err != nil { |
605 | if err != nil { |
| 571 | return u, c.fail(protocol.ExitFailure, "%v", err) |
606 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 572 | } |
607 | } |
| 573 | return u, -1 |
608 | users = append(users, u) |
| 574 | } |
609 | } |
| |
610 | return users, -1 |
| |
611 | } |
| |
612 | |
| |
613 | // assignIssue adds and removes assignees on issue, records the |
| |
614 | // issue.assigned event and tells each newly added account, returning the |
| |
615 | // assignees afterwards. It backs issue assign and issue create --assignee. |
| |
616 | func assignIssue(c *Ctx, repo store.Repo, issue store.Issue, adds, removes []store.User) ([]string, int) { |
| 575 | // issue is the read from before the update, so its Assignees are who |
617 | // issue is the read from before the update, so its Assignees are who |
| 576 | // was already on it. SetIssueAssignee inserts ON CONFLICT DO NOTHING |
618 | // was already on it. SetIssueAssignee inserts ON CONFLICT DO NOTHING |
| 577 | // and returns nil whether or not it inserted, and the notice below is |
619 | // and returns nil whether or not it inserted, and the notice below is |
| @@ -582,13 +624,9 @@ func runIssueAssign(c *Ctx, args []string) int { |
| 582 | assigned[name] = true |
624 | assigned[name] = true |
| 583 | } |
625 | } |
| 584 | var added []int64 |
626 | var added []int64 |
| 585 | for _, name := range adds { |
627 | for _, u := range adds { |
| 586 | u, code := resolve(name) |
| |
| 587 | if code >= 0 { |
| |
| 588 | return code |
| |
| 589 | } |
| |
| 590 | if err := c.Store.SetIssueAssignee(issue.ID, u.ID, true); err != nil { |
628 | if err := c.Store.SetIssueAssignee(issue.ID, u.ID, true); err != nil { |
| 591 | return c.fail(protocol.ExitFailure, "%v", err) |
629 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 592 | } |
630 | } |
| 593 | if assigned[u.Username] { |
631 | if assigned[u.Username] { |
| 594 | continue |
632 | continue |
| @@ -596,21 +634,17 @@ func runIssueAssign(c *Ctx, args []string) int { |
| 596 | assigned[u.Username] = true |
634 | assigned[u.Username] = true |
| 597 | added = append(added, u.ID) |
635 | added = append(added, u.ID) |
| 598 | } |
636 | } |
| 599 | for _, name := range removes { |
637 | for _, u := range removes { |
| 600 | u, code := resolve(name) |
| |
| 601 | if code >= 0 { |
| |
| 602 | return code |
| |
| 603 | } |
| |
| 604 | if err := c.Store.SetIssueAssignee(issue.ID, u.ID, false); err != nil { |
638 | if err := c.Store.SetIssueAssignee(issue.ID, u.ID, false); err != nil { |
| 605 | if errors.Is(err, store.ErrNotFound) { |
639 | if errors.Is(err, store.ErrNotFound) { |
| 606 | return c.fail(protocol.ExitNotFound, "%s is not assigned", name) |
640 | return nil, c.fail(protocol.ExitNotFound, "%s is not assigned", u.Username) |
| 607 | } |
641 | } |
| 608 | return c.fail(protocol.ExitFailure, "%v", err) |
642 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 609 | } |
643 | } |
| 610 | } |
644 | } |
| 611 | updated, err := c.Store.IssueByNumber(repo.ID, issue.Number) |
645 | updated, err := c.Store.IssueByNumber(repo.ID, issue.Number) |
| 612 | if err != nil { |
646 | if err != nil { |
| 613 | return c.fail(protocol.ExitFailure, "%v", err) |
647 | return nil, c.fail(protocol.ExitFailure, "%v", err) |
| 614 | } |
648 | } |
| 615 | c.Store.RecordEvent(repo.ID, c.User.ID, "issue.assigned", |
649 | c.Store.RecordEvent(repo.ID, c.User.ID, "issue.assigned", |
| 616 | fmt.Sprintf(`{"number":%d,"assignees":%s}`, issue.Number, jsonStrings(updated.Assignees))) |
650 | fmt.Sprintf(`{"number":%d,"assignees":%s}`, issue.Number, jsonStrings(updated.Assignees))) |
| @@ -624,7 +658,5 @@ func runIssueAssign(c *Ctx, args []string) int { |
| 624 | action: fmt.Sprintf("assigned you to #%d", issue.Number), |
658 | action: fmt.Sprintf("assigned you to #%d", issue.Number), |
| 625 | path: fmt.Sprintf("%s/issues/%d", repo.Path(), issue.Number)}) |
659 | path: fmt.Sprintf("%s/issues/%d", repo.Path(), issue.Number)}) |
| 626 | } |
660 | } |
| 627 | return c.emit(map[string]any{"number": issue.Number, "assignees": updated.Assignees}, func(w io.Writer) { |
661 | return updated.Assignees, -1 |
| 628 | fmt.Fprintf(w, "assignees on %s#%d: %s\n", repo.Path(), issue.Number, strings.Join(updated.Assignees, ", ")) |
| |
| 629 | }) |
| |
| 630 | } |
662 | } |