Commit c3f59fc38f
Verified · cmc ci/build: success ci/test: success
Layout: unified · split
.gitbay/wiki/Parity.org +6 −3
| @@ -142,7 +142,8 @@ always markdown. | |||
| 142 | | fork | yes | yes | yes | | 142 | | fork | yes | yes | yes | |
| 143 | | pin | yes | yes | yes | | 143 | | pin | yes | yes | yes | |
| 144 | | bookmark, bookmark list | yes | yes | yes | | 144 | | bookmark, bookmark list | yes | yes | yes | |
| 145 | | watch, mute | yes | yes | yes | | 145 | | watch, unwatch | yes | yes | yes | |
| 146 | | mute | yes | no | yes | | ||
| 146 | | settings, protection | yes | yes | yes | | 147 | | settings, protection | yes | yes | yes | |
| 147 | | topics, website | yes | yes | yes | | 148 | | topics, website | yes | yes | yes | |
| 148 | | visibility | yes | yes | yes | | 149 | | visibility | yes | yes | yes | |
| @@ -263,8 +264,10 @@ instance sends mail, and mail is the second half when SMTP is | |||
| 263 | configured. =notifications list= reads it, unread by default; | 264 | configured. =notifications list= reads it, unread by default; |
| 264 | =notifications read <id>... | --all= clears it; the dashboard and the | 265 | =notifications read <id>... | --all= clears it; the dashboard and the |
| 265 | web rail carry the unread count. =repo watch= adds you to a | 266 | web rail carry the unread count. =repo watch= adds you to a |
| 266 | repository's notifications and =repo unwatch= mutes it, and a mute wins | 267 | repository's notifications, =repo mute= silences it, and =repo unwatch= |
| 267 | over owning the repository or having written the thread. | 268 | returns you to the default from either — told about work you are part |
| 269 | of, nothing more. A mute wins over owning the repository or having | ||
| 270 | written the thread. | ||
| 268 | 271 | ||
| 269 | A login link is requested from the login page by username or verified | 272 | A login link is requested from the login page by username or verified |
| 270 | address, and arrives by mail: it works once and expires in fifteen | 273 | address, and arrives by mail: it works once and expires in fifteen |
.gitbay/wiki/Users.org +2 −1
| @@ -162,7 +162,8 @@ gitbay repo delete you/project --yes | |||
| 162 | #+end_src | 162 | #+end_src |
| 163 | 163 | ||
| 164 | =repo show= also reports your own state on the repository — =watch= | 164 | =repo show= also reports your own state on the repository — =watch= |
| 165 | (=watching= or =muted=) and =bookmarked= — and =fork_of= when it is a | 165 | (=watching= or =muted=; =repo unwatch= clears either) and =bookmarked= — |
| 166 | and =fork_of= when it is a | ||
| 166 | fork whose parent you can read, so a client draws a toggle rather than | 167 | fork whose parent you can read, so a client draws a toggle rather than |
| 167 | two blind buttons. Absent means none. | 168 | two blind buttons. Absent means none. |
| 168 | 169 | ||
cmd/gitbay/main.go +2 −1
| @@ -418,7 +418,8 @@ func repoCmd() *cobra.Command { | |||
| 418 | pass("unbookmark", "remove a bookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}), | 418 | pass("unbookmark", "remove a bookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}), |
| 419 | pass("bookmarks", "list the repositories you have bookmarked", passOpts{server: []string{"repo", "bookmarks"}}), | 419 | pass("bookmarks", "list the repositories you have bookmarked", passOpts{server: []string{"repo", "bookmarks"}}), |
| 420 | pass("watch", "hear about all activity on a repository", passOpts{server: []string{"repo", "watch"}, needsRepo: true}), | 420 | pass("watch", "hear about all activity on a repository", passOpts{server: []string{"repo", "watch"}, needsRepo: true}), |
| 421 | pass("unwatch", "mute a repository", passOpts{server: []string{"repo", "unwatch"}, needsRepo: true}), | 421 | pass("unwatch", "stop watching a repository", passOpts{server: []string{"repo", "unwatch"}, needsRepo: true}), |
| 422 | pass("mute", "mute a repository, including work you are part of", passOpts{server: []string{"repo", "mute"}, needsRepo: true}), | ||
| 422 | pass("archive", "archive a repository (read-only)", passOpts{server: []string{"repo", "archive"}, needsRepo: true}), | 423 | pass("archive", "archive a repository (read-only)", passOpts{server: []string{"repo", "archive"}, needsRepo: true}), |
| 423 | pass("unarchive", "unarchive a repository", passOpts{server: []string{"repo", "unarchive"}, needsRepo: true}), | 424 | pass("unarchive", "unarchive a repository", passOpts{server: []string{"repo", "unarchive"}, needsRepo: true}), |
| 424 | local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone), | 425 | local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone), |
e2e/inbox_test.go +2 −2
| @@ -91,8 +91,8 @@ func TestNotificationInbox(t *testing.T) { | |||
| 91 | 91 | ||
| 92 | // Muting beats being a participant: bob wrote the issue and still | 92 | // Muting beats being a participant: bob wrote the issue and still |
| 93 | // hears nothing more, while the watcher does. | 93 | // hears nothing more, while the watcher does. |
| 94 | if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "unwatch", "alice/app"); code != 0 { | 94 | if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "mute", "alice/app"); code != 0 { |
| 95 | t.Fatalf("repo unwatch: %s", errOut) | 95 | t.Fatalf("repo mute: %s", errOut) |
| 96 | } | 96 | } |
| 97 | if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'more'"); code != 0 { | 97 | if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'more'"); code != 0 { |
| 98 | t.Fatal("alice comment failed") | 98 | t.Fatal("alice comment failed") |
e2e/reposhowstate_test.go +15
| @@ -32,6 +32,21 @@ func TestRepoShowCarriesViewerState(t *testing.T) { | |||
| 32 | if !strings.Contains(out, `"watch":"watching"`) || !strings.Contains(out, `"bookmarked":true`) { | 32 | if !strings.Contains(out, `"watch":"watching"`) || !strings.Contains(out, `"bookmarked":true`) { |
| 33 | t.Fatalf("bob's state not reported:\n%s", out) | 33 | t.Fatalf("bob's state not reported:\n%s", out) |
| 34 | } | 34 | } |
| 35 | // unwatch is the way back to the default from either state, and | ||
| 36 | // mute is its own state, not what unwatch means (#180). | ||
| 37 | inst.ssh(t, bobKey, "", "repo", "unwatch", "alice/app") | ||
| 38 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app", "--json"); strings.Contains(out, `"watch"`) { | ||
| 39 | t.Errorf("unwatch did not clear the state:\n%s", out) | ||
| 40 | } | ||
| 41 | inst.ssh(t, bobKey, "", "repo", "mute", "alice/app") | ||
| 42 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app", "--json"); !strings.Contains(out, `"watch":"muted"`) { | ||
| 43 | t.Errorf("mute not reported:\n%s", out) | ||
| 44 | } | ||
| 45 | inst.ssh(t, bobKey, "", "repo", "unwatch", "alice/app") | ||
| 46 | if out, _, _ := inst.ssh(t, bobKey, "", "repo", "show", "alice/app", "--json"); strings.Contains(out, `"watch"`) { | ||
| 47 | t.Errorf("unwatch did not clear a mute:\n%s", out) | ||
| 48 | } | ||
| 49 | inst.ssh(t, bobKey, "", "repo", "watch", "alice/app") | ||
| 35 | // It is the caller's state: alice sees her own, not bob's. | 50 | // It is the caller's state: alice sees her own, not bob's. |
| 36 | if out, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/app", "--json"); strings.Contains(out, `"bookmarked"`) { | 51 | if out, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/app", "--json"); strings.Contains(out, `"bookmarked"`) { |
| 37 | t.Errorf("alice sees bob's bookmark:\n%s", out) | 52 | t.Errorf("alice sees bob's bookmark:\n%s", out) |
internal/control/notifications.go +18 −9
| @@ -23,8 +23,11 @@ func init() { | |||
| 23 | Summary: "hear about all activity on a repository", | 23 | Summary: "hear about all activity on a repository", |
| 24 | Usage: "repo watch <owner/name>", Run: runRepoWatch}) | 24 | Usage: "repo watch <owner/name>", Run: runRepoWatch}) |
| 25 | register(Command{Path: []string{"repo", "unwatch"}, | 25 | register(Command{Path: []string{"repo", "unwatch"}, |
| 26 | Summary: "mute a repository, including work you are part of", | 26 | Summary: "back to the default: only work you are part of", |
| 27 | Usage: "repo unwatch <owner/name>", Run: runRepoUnwatch}) | 27 | Usage: "repo unwatch <owner/name>", Run: runRepoUnwatch}) |
| 28 | register(Command{Path: []string{"repo", "mute"}, | ||
| 29 | Summary: "mute a repository, including work you are part of", | ||
| 30 | Usage: "repo mute <owner/name>", Run: runRepoMute}) | ||
| 28 | } | 31 | } |
| 29 | 32 | ||
| 30 | // notice is one thing that happened, in the shape both delivery routes | 33 | // notice is one thing that happened, in the shape both delivery routes |
| @@ -171,14 +174,14 @@ func runNotificationsRead(c *Ctx, args []string) int { | |||
| 171 | }) | 174 | }) |
| 172 | } | 175 | } |
| 173 | 176 | ||
| 174 | func runRepoWatch(c *Ctx, args []string) int { return setWatch(c, args, "watching") } | 177 | func runRepoWatch(c *Ctx, args []string) int { return setWatch(c, args, "watch", "watching") } |
| 175 | func runRepoUnwatch(c *Ctx, args []string) int { return setWatch(c, args, "muted") } | 178 | func runRepoMute(c *Ctx, args []string) int { return setWatch(c, args, "mute", "muted") } |
| 179 | func runRepoUnwatch(c *Ctx, args []string) int { return setWatch(c, args, "unwatch", "default") } | ||
| 176 | 180 | ||
| 177 | func setWatch(c *Ctx, args []string, state string) int { | 181 | // setWatch records the caller's state on a repository. "default" deletes |
| 178 | verb := "watch" | 182 | // the row: watch then unwatch leaves no trace, and a mute is undone the |
| 179 | if state == "muted" { | 183 | // same way. |
| 180 | verb = "unwatch" | 184 | func setWatch(c *Ctx, args []string, verb, state string) int { |
| 181 | } | ||
| 182 | if len(args) != 1 { | 185 | if len(args) != 1 { |
| 183 | return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) | 186 | return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) |
| 184 | } | 187 | } |
| @@ -186,7 +189,13 @@ func setWatch(c *Ctx, args []string, state string) int { | |||
| 186 | if code >= 0 { | 189 | if code >= 0 { |
| 187 | return code | 190 | return code |
| 188 | } | 191 | } |
| 189 | if err := c.Store.SetRepoWatch(repo.ID, c.User.ID, state); err != nil { | 192 | var err error |
| 193 | if state == "default" { | ||
| 194 | err = c.Store.ClearRepoWatch(repo.ID, c.User.ID) | ||
| 195 | } else { | ||
| 196 | err = c.Store.SetRepoWatch(repo.ID, c.User.ID, state) | ||
| 197 | } | ||
| 198 | if err != nil { | ||
| 190 | return c.fail(protocol.ExitFailure, "%v", err) | 199 | return c.fail(protocol.ExitFailure, "%v", err) |
| 191 | } | 200 | } |
| 192 | return c.emit(map[string]string{"repo": repo.Path(), "state": state}, func(w io.Writer) { | 201 | return c.emit(map[string]string{"repo": repo.Path(), "state": state}, func(w io.Writer) { |
internal/httpd/notifyweb.go +3 −3
| @@ -63,10 +63,10 @@ func (s *Server) watchToggle(w http.ResponseWriter, r *http.Request, u store.Use | |||
| 63 | if !ok { | 63 | if !ok { |
| 64 | return | 64 | return |
| 65 | } | 65 | } |
| 66 | state := "watching" | ||
| 67 | if s.st.RepoWatchState(repo.ID, u.ID) == "watching" { | 66 | if s.st.RepoWatchState(repo.ID, u.ID) == "watching" { |
| 68 | state = "muted" | 67 | s.st.ClearRepoWatch(repo.ID, u.ID) |
| 68 | } else { | ||
| 69 | s.st.SetRepoWatch(repo.ID, u.ID, "watching") | ||
| 69 | } | 70 | } |
| 70 | s.st.SetRepoWatch(repo.ID, u.ID, state) | ||
| 71 | http.Redirect(w, r, "/"+repo.Path(), http.StatusSeeOther) | 71 | http.Redirect(w, r, "/"+repo.Path(), http.StatusSeeOther) |
| 72 | } | 72 | } |
internal/store/inbox.go +7
| @@ -111,6 +111,13 @@ func (s *Store) SetRepoWatch(repoID, userID int64, state string) error { | |||
| 111 | return err | 111 | return err |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | // ClearRepoWatch returns the user to the default: notified about work | ||
| 115 | // they are part of, nothing more. | ||
| 116 | func (s *Store) ClearRepoWatch(repoID, userID int64) error { | ||
| 117 | _, err := s.DB.Exec("DELETE FROM repo_watchers WHERE repo_id = ? AND user_id = ?", repoID, userID) | ||
| 118 | return err | ||
| 119 | } | ||
| 120 | |||
| 114 | // RepoWatchState returns "watching", "muted", or "" for the default. | 121 | // RepoWatchState returns "watching", "muted", or "" for the default. |
| 115 | func (s *Store) RepoWatchState(repoID, userID int64) string { | 122 | func (s *Store) RepoWatchState(repoID, userID int64) string { |
| 116 | var state string | 123 | var state string |
internal/store/inbox_test.go +13
| @@ -146,6 +146,19 @@ func TestNotifyRecipients(t *testing.T) { | |||
| 146 | if s.RepoWatchState(repoID, owner) != "muted" { | 146 | if s.RepoWatchState(repoID, owner) != "muted" { |
| 147 | t.Fatal("watch state not recorded") | 147 | t.Fatal("watch state not recorded") |
| 148 | } | 148 | } |
| 149 | // Clearing returns the owner to the default and they are told again. | ||
| 150 | if err := s.ClearRepoWatch(repoID, owner); err != nil { | ||
| 151 | t.Fatal(err) | ||
| 152 | } | ||
| 153 | if s.RepoWatchState(repoID, owner) != "" { | ||
| 154 | t.Fatal("clear left a state") | ||
| 155 | } | ||
| 156 | if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}); len(got) != 2 { | ||
| 157 | t.Fatalf("cleared owner not notified: %v", got) | ||
| 158 | } | ||
| 159 | if err := s.SetRepoWatch(repoID, owner, "muted"); err != nil { | ||
| 160 | t.Fatal(err) | ||
| 161 | } | ||
| 149 | 162 | ||
| 150 | // Watching after muting replaces the row rather than adding one. | 163 | // Watching after muting replaces the row rather than adding one. |
| 151 | if err := s.SetRepoWatch(repoID, owner, "watching"); err != nil { | 164 | if err := s.SetRepoWatch(repoID, owner, "watching"); err != nil { |