Commit 90af4498e8

90af4498e80f7d80cfa784e16adbc747dbd593af

parent: c89859bef9

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 10:09 UTC

control: issue assign files a notice

The dashboard surfaced assigned work and nothing announced it. Direct,
as a mention is, so watchers are not told they were assigned.

Ref #89
internal/control/issue.go +12
@@ -445,6 +445,7 @@ func runIssueAssign(c *Ctx, args []string) int {
445445 }
446446 return u, -1
447447 }
448 var added []int64
448449 for _, name := range adds {
449450 u, code := resolve(name)
450451 if code >= 0 {
@@ -453,6 +454,7 @@ func runIssueAssign(c *Ctx, args []string) int {
453454 if err := c.Store.SetIssueAssignee(issue.ID, u.ID, true); err != nil {
454455 return c.fail(protocol.ExitFailure, "%v", err)
455456 }
457 added = append(added, u.ID)
456458 }
457459 for _, name := range removes {
458460 u, code := resolve(name)
@@ -472,6 +474,16 @@ func runIssueAssign(c *Ctx, args []string) int {
472474 }
473475 c.Store.RecordEvent(repo.ID, c.User.ID, "issue.assigned",
474476 fmt.Sprintf(`{"number":%d,"assignees":%s}`, issue.Number, jsonStrings(updated.Assignees)))
477 if len(added) > 0 {
478 // direct, as a mention is: an assignment is addressed to someone,
479 // and widening it to watchers would tell them "assigned you".
480 // Removals file nothing, and notify drops the actor, so assigning
481 // yourself is silent.
482 notify(c, added, notice{repo: repo, kind: "issue", direct: true,
483 subject: fmt.Sprintf("[%s] #%d: %s", repo.Path(), issue.Number, issue.Title),
484 action: fmt.Sprintf("assigned you to #%d", issue.Number),
485 path: fmt.Sprintf("%s/issues/%d", repo.Path(), issue.Number)})
486 }
475487 return c.emit(map[string]any{"number": issue.Number, "assignees": updated.Assignees}, func(w io.Writer) {
476488 fmt.Fprintf(w, "assignees on %s#%d: %s\n", repo.Path(), issue.Number, strings.Join(updated.Assignees, ", "))
477489 })
internal/control/issue_test.go added +34
@@ -0,0 +1,34 @@
1package control
2
3import "testing"
4
5func TestIssueAssignNotifiesTheAssignee(t *testing.T) {
6 c, repo, bob := testRepoWithWatcher(t)
7
8 if code := runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"}); code != 0 {
9 t.Fatalf("exit %d", code)
10 }
11 rows, _ := c.Store.Inbox(bob, false, 20, 0)
12 if len(rows) != 1 || rows[0].Summary != "assigned you to #1" {
13 t.Fatalf("got %+v", rows)
14 }
15}
16
17func TestIssueAssignIsSilentForTheActorAndForRemovals(t *testing.T) {
18 c, repo, bob := testRepoWithWatcher(t)
19
20 // Assigning yourself announces nothing: notify drops the actor.
21 runIssueAssign(c, []string{repo.Path(), "1", "--add", "alice"})
22 if rows, _ := c.Store.Inbox(c.User.ID, false, 20, 0); len(rows) != 0 {
23 t.Fatalf("self-assignment notified: %+v", rows)
24 }
25
26 // Unassigning files nothing.
27 runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"})
28 before, _ := c.Store.Inbox(bob, false, 20, 0)
29 runIssueAssign(c, []string{repo.Path(), "1", "--remove", "bob"})
30 after, _ := c.Store.Inbox(bob, false, 20, 0)
31 if len(after) != len(before) {
32 t.Fatalf("removal filed a row: %d then %d", len(before), len(after))
33 }
34}
internal/control/notifications_test.go +5 −2
@@ -37,8 +37,8 @@ func notifTestCtx(t *testing.T, username string) *Ctx {
3737}
3838
3939// testRepoWithWatcher returns a Ctx acting as alice, a repository she
40// owns, and bob's user id with a watch row on it — the shared setup for
41// notify's recipient-widening tests.
40// owns with issue #1 open on it, and bob's user id with a watch row on
41// it — the shared setup for notify's recipient-widening tests.
4242func testRepoWithWatcher(t *testing.T) (*Ctx, store.Repo, int64) {
4343 t.Helper()
4444 c := notifTestCtx(t, "alice")
@@ -50,6 +50,9 @@ func testRepoWithWatcher(t *testing.T) (*Ctx, store.Repo, int64) {
5050 if err != nil {
5151 t.Fatal(err)
5252 }
53 if _, err := c.Store.CreateIssue(repo.ID, c.User.ID, "title", "", "markdown"); err != nil {
54 t.Fatal(err)
55 }
5356 bob, err := c.Store.CreateUser("bob", false)
5457 if err != nil {
5558 t.Fatal(err)