From 5e4d21acd56b8fca628196c56778b292ec5ba249 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 16 Aug 2026 11:00:59 +0800 Subject: [PATCH] chore: fix repo watch (#38921) --- modelmigration/v28/v347.go | 6 +- models/activities/notification_test.go | 2 +- models/issues/issue_watch.go | 2 +- models/repo/repo_test.go | 4 +- models/repo/watch.go | 154 ++++++++++++---------- models/repo/watch_test.go | 28 ++-- routers/api/v1/user/watch.go | 6 +- routers/web/repo/repo.go | 2 +- routers/web/repo/star.go | 2 +- routers/web/repo/transfer.go | 4 +- routers/web/repo/watch.go | 25 ++-- services/feed/feed.go | 6 +- services/feed/feed_test.go | 4 +- services/mailer/mail_release_test.go | 4 +- services/org/org_test.go | 2 +- services/org/team.go | 2 +- services/org/team_test.go | 4 +- services/org/user.go | 2 +- services/repository/collaboration.go | 4 +- services/repository/collaboration_test.go | 4 +- services/repository/create.go | 2 +- services/repository/repo_team.go | 6 +- services/repository/transfer.go | 4 +- services/user/block.go | 2 +- templates/repo/header/watch.tmpl | 6 +- templates/shared/repo/list.tmpl | 6 +- 26 files changed, 156 insertions(+), 137 deletions(-) diff --git a/modelmigration/v28/v347.go b/modelmigration/v28/v347.go index b1b19248d67..ac4b7c73555 100644 --- a/modelmigration/v28/v347.go +++ b/modelmigration/v28/v347.go @@ -13,9 +13,9 @@ import ( func AddWatchOptions(_ context.Context, x base.EngineMigration) error { type Watch struct { - PullRequests bool `xorm:"NOT NULL DEFAULT true"` - Issues bool `xorm:"NOT NULL DEFAULT true"` - Releases bool `xorm:"NOT NULL DEFAULT true"` + IncludePullRequests bool `xorm:"NOT NULL DEFAULT true"` + IncludeIssues bool `xorm:"NOT NULL DEFAULT true"` + IncludeReleases bool `xorm:"NOT NULL DEFAULT true"` } _, err := x.SyncWithOptions(xorm.SyncOptions{ IgnoreConstrains: true, diff --git a/models/activities/notification_test.go b/models/activities/notification_test.go index e1d2ecc9fc9..1438afb3cce 100644 --- a/models/activities/notification_test.go +++ b/models/activities/notification_test.go @@ -54,7 +54,7 @@ func TestCreateOrUpdateIssueNotificationsIgnored(t *testing.T) { // user 4 watches repo 1 and would be notified about issue 1 repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - assert.NoError(t, repo_model.WatchIgnoreRepo(t.Context(), user, repo)) + assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeDont})) notified, err := activities_model.CreateOrUpdateIssueNotifications(t.Context(), 1, 0, 2, 0) assert.NoError(t, err) diff --git a/models/issues/issue_watch.go b/models/issues/issue_watch.go index e16621a7ec3..8ff41b80190 100644 --- a/models/issues/issue_watch.go +++ b/models/issues/issue_watch.go @@ -82,7 +82,7 @@ func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) ( if err != nil { return false, err } - if repo_model.IsWatchMode(w.Mode) && util.Iif(issue.IsPull, w.PullRequests, w.Issues) { + if repo_model.IsWatchModeWatching(w.Mode) && util.Iif(issue.IsPull, w.IncludePullRequests, w.IncludeIssues) { return true, nil } return IsUserParticipantsOfIssue(ctx, user, issue), nil diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index f23e4c64681..1d825568109 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -67,11 +67,11 @@ func TestWatchRepo(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, WatchRepo(t.Context(), user, repo, true)) + assert.NoError(t, WatchRepoAuto(t.Context(), user, repo, true)) unittest.AssertExistsAndLoadBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) - assert.NoError(t, WatchRepo(t.Context(), user, repo, false)) + assert.NoError(t, WatchRepoAuto(t.Context(), user, repo, false)) unittest.AssertNotExistsBean(t, &Watch{RepoID: repo.ID, UserID: user.ID}) unittest.CheckConsistencyFor(t, &Repository{ID: repo.ID}) } diff --git a/models/repo/watch.go b/models/repo/watch.go index 2519fe46ab6..f8c413b14b2 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -18,14 +18,11 @@ import ( type WatchMode int8 const ( - // WatchModeNone don't watch - WatchModeNone WatchMode = iota // 0 - // WatchModeNormal watch repository (from other sources) - WatchModeNormal // 1 - // WatchModeDont explicit don't auto-watch - WatchModeDont // 2 - // WatchModeAuto watch repository (from AutoWatchOnChanges) - WatchModeAuto // 3 + WatchModeNone WatchMode = iota // 0 watch nothing unless mentioned + + WatchModeNormal // 1 proactively watching (all or custom) + WatchModeDont // 2 ignore the repo + WatchModeAuto // 3 automatically watching (from AutoWatchOnChanges) ) // WatchType is the `watch` column gating one kind of notification @@ -39,15 +36,16 @@ const ( // Watch is connection request for receiving repository notification. type Watch struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"UNIQUE(watch)"` - RepoID int64 `xorm:"UNIQUE(watch)"` - Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - PullRequests bool `xorm:"NOT NULL DEFAULT true"` - Issues bool `xorm:"NOT NULL DEFAULT true"` - Releases bool `xorm:"NOT NULL DEFAULT true"` + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(watch)"` + RepoID int64 `xorm:"UNIQUE(watch)"` + Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + IncludePullRequests bool `xorm:"NOT NULL DEFAULT true"` + IncludeIssues bool `xorm:"NOT NULL DEFAULT true"` + IncludeReleases bool `xorm:"NOT NULL DEFAULT true"` } func init() { @@ -61,7 +59,7 @@ func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) { return watch, err } if watch == nil { // the dummy record must mirror the column defaults - watch = &Watch{UserID: userID, RepoID: repoID, PullRequests: true, Issues: true, Releases: true} + watch = &Watch{UserID: userID, RepoID: repoID, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true} } if !has { watch.Mode = WatchModeNone @@ -76,12 +74,16 @@ func (w *Watch) IsIgnoring() bool { // IsWatching reports whether the watch counts the user as a watcher of the repository func (w *Watch) IsWatching() bool { - return IsWatchMode(w.Mode) + return IsWatchModeWatching(w.Mode) } // IsWatchingAll reports whether every event is enabled, which is the "all activity" mode func (w *Watch) IsWatchingAll() bool { - return w.PullRequests && w.Issues && w.Releases + return w.IncludePullRequests && w.IncludeIssues && w.IncludeReleases +} + +func (w *Watch) IsWatchingAny() bool { + return w.IncludePullRequests || w.IncludeIssues || w.IncludeReleases } // SelectedMode returns the mode the user picked in the watch menu @@ -89,7 +91,7 @@ func (w *Watch) SelectedMode() string { switch { case w.IsIgnoring(): return "ignore" - case !IsWatchMode(w.Mode), !(w.PullRequests || w.Issues || w.Releases): + case !IsWatchModeWatching(w.Mode), !w.IsWatchingAny(): return "participate" // also the default while there is no watch row case w.IsWatchingAll(): return "all" @@ -97,110 +99,105 @@ func (w *Watch) SelectedMode() string { return "custom" } -// IsWatchMode Decodes watchability of WatchMode -func IsWatchMode(mode WatchMode) bool { +// IsWatchModeWatching Decodes watchability of WatchMode +func IsWatchModeWatching(mode WatchMode) bool { return mode != WatchModeNone && mode != WatchModeDont } -// IsWatching checks if user has watched given repository. -func IsWatching(ctx context.Context, userID, repoID int64) bool { +// IsWatchingRepo checks if user has watched given repository. +func IsWatchingRepo(ctx context.Context, userID, repoID int64) bool { watch, err := GetWatch(ctx, userID, repoID) - return err == nil && IsWatchMode(watch.Mode) + return err == nil && IsWatchModeWatching(watch.Mode) } -func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) { +func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) { if watch.Mode == mode { return nil } - if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) { + if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchModeWatching(watch.Mode)) { // Don't auto watch if already watching or deliberately not watching return nil } - hadrec := watch.Mode != WatchModeNone - needsrec := mode != WatchModeNone - repodiff := 0 + hadWatchModeSet := watch.Mode != WatchModeNone + needSetWatchMode := mode != WatchModeNone + repoWatchDelta := 0 - if IsWatchMode(mode) && !IsWatchMode(watch.Mode) { - repodiff = 1 - } else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) { - repodiff = -1 + if IsWatchModeWatching(mode) && !IsWatchModeWatching(watch.Mode) { + repoWatchDelta = 1 + } else if !IsWatchModeWatching(mode) && IsWatchModeWatching(watch.Mode) { + repoWatchDelta = -1 } - if repodiff == 1 { // starting to watch resets the options, otherwise a custom selection survives - watch.PullRequests, watch.Issues, watch.Releases = true, true, true + if repoWatchDelta == 1 { // starting to watch resets the options, otherwise a custom selection survives + watch.IncludePullRequests, watch.IncludeIssues, watch.IncludeReleases = true, true, true } watch.Mode = mode - if !hadrec && needsrec { + if !hadWatchModeSet && needSetWatchMode { if err = db.Insert(ctx, watch); err != nil { return err } - } else if needsrec { + } else if needSetWatchMode { if _, err := db.GetEngine(ctx).ID(watch.ID).AllCols().Update(watch); err != nil { return err } } else if _, err = db.DeleteByID[Watch](ctx, watch.ID); err != nil { return err } - if repodiff != 0 { - _, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID) + if repoWatchDelta != 0 { + _, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repoWatchDelta, watch.RepoID) } return err } -// WatchRepo watch or unwatch repository. -func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error { +// WatchRepoAuto watch or unwatch repository. +func WatchRepoAuto(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error { watch, err := GetWatch(ctx, doer.ID, repo.ID) if err != nil { return err } if !doWatch && watch.Mode == WatchModeAuto { - return watchRepoMode(ctx, watch, WatchModeDont) + return watchRepoByMode(ctx, watch, WatchModeDont) } else if !doWatch { - return watchRepoMode(ctx, watch, WatchModeNone) + return watchRepoByMode(ctx, watch, WatchModeNone) } if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) { return user_model.ErrBlockedUser } - return watchRepoMode(ctx, watch, WatchModeNormal) -} - -// WatchIgnoreRepo mutes the repository (unwatch), so nothing about it reaches the user. -func WatchIgnoreRepo(ctx context.Context, doer *user_model.User, repo *Repository) error { - watch, err := GetWatch(ctx, doer.ID, repo.ID) - if err != nil { - return err - } - return watchRepoMode(ctx, watch, WatchModeDont) + return watchRepoByMode(ctx, watch, WatchModeNormal) } type WatchOptions struct { - PullRequests bool - Issues bool - Releases bool + Mode WatchMode + + WatchPullRequests bool + WatchIssues bool + WatchReleases bool } // WatchRepoWithOptions starts watching the repository and subscribes to the given events func WatchRepoWithOptions(ctx context.Context, doer *user_model.User, repo *Repository, opts WatchOptions) error { return db.WithTx(ctx, func(ctx context.Context) error { - if err := WatchRepo(ctx, doer, repo, true); err != nil { + watch, err := GetWatch(ctx, doer.ID, repo.ID) + if err != nil { return err } - return SetWatchOptions(ctx, doer.ID, repo.ID, opts) + err = watchRepoByMode(ctx, watch, opts.Mode) + if err != nil { + return err + } + if opts.Mode == WatchModeNormal { + _, err = db.GetEngine(ctx).Where("user_id=? AND repo_id=?", doer.ID, repo.ID). + Cols("include_pull_requests", "include_issues", "include_releases"). + Update(&Watch{IncludePullRequests: opts.WatchPullRequests, IncludeIssues: opts.WatchIssues, IncludeReleases: opts.WatchReleases}) + } + return err }) } -// SetWatchOptions updates the per-event options of a watch, callers must run WatchRepo first -func SetWatchOptions(ctx context.Context, userID, repoID int64, opts WatchOptions) error { - _, err := db.GetEngine(ctx).Where("user_id=? AND repo_id=?", userID, repoID). - Cols(string(WatchPullRequests), string(WatchIssues), string(WatchReleases)). - Update(&Watch{PullRequests: opts.PullRequests, Issues: opts.Issues, Releases: opts.Releases}) - return err -} - // GetUserWatches returns the watches of one user, keyed by repository ID func GetUserWatches(ctx context.Context, userID int64, repoIDs []int64) (map[int64]*Watch, error) { if len(repoIDs) == 0 { @@ -225,7 +222,11 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) { watches := make([]*Watch, 0, 10) return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID). And("`watch`.mode<>?", WatchModeDont). - And(builder.Or(builder.Eq{"`watch`.pull_requests": true}, builder.Eq{"`watch`.issues": true}, builder.Eq{"`watch`.releases": true})). + And(builder.Or( + builder.Eq{"`watch`.include_pull_requests": true}, + builder.Eq{"`watch`.include_issues": true}, + builder.Eq{"`watch`.include_releases": true}, + )). And("`user`.is_active=?", true). And("`user`.prohibit_login=?", false). Join("INNER", "`user`", "`user`.id = `watch`.user_id"). @@ -247,10 +248,21 @@ func GetRepoIgnorersIDs(ctx context.Context, repoID int64) ([]int64, error) { // User permissions must be verified elsewhere if required func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) { ids := make([]int64, 0, 64) + var watchColName string + switch watchType { + case WatchPullRequests: + watchColName = "include_pull_requests" + case WatchIssues: + watchColName = "include_issues" + case WatchReleases: + watchColName = "include_releases" + default: + panic("invalid WatchType") + } return ids, db.GetEngine(ctx).Table("watch"). Where("watch.repo_id=?", repoID). And("watch.mode<>?", WatchModeDont). - And(builder.Eq{"watch." + string(watchType): true}). + And(builder.Eq{watchColName: true}). Select("user_id"). Find(&ids) } @@ -283,7 +295,7 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error if watch.Mode != WatchModeNone { return nil } - return watchRepoMode(ctx, watch, WatchModeAuto) + return watchRepoByMode(ctx, watch, WatchModeAuto) } // ClearRepoWatches clears all watches for a repository and from the user that watched it. diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index a2d394bd84f..104154102e7 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -19,13 +19,13 @@ import ( func TestIsWatching(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, repo_model.IsWatching(t.Context(), 1, 1)) - assert.True(t, repo_model.IsWatching(t.Context(), 4, 1)) - assert.True(t, repo_model.IsWatching(t.Context(), 11, 1)) + assert.True(t, repo_model.IsWatchingRepo(t.Context(), 1, 1)) + assert.True(t, repo_model.IsWatchingRepo(t.Context(), 4, 1)) + assert.True(t, repo_model.IsWatchingRepo(t.Context(), 11, 1)) - assert.False(t, repo_model.IsWatching(t.Context(), 1, 5)) - assert.False(t, repo_model.IsWatching(t.Context(), 8, 1)) - assert.False(t, repo_model.IsWatching(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) + assert.False(t, repo_model.IsWatchingRepo(t.Context(), 1, 5)) + assert.False(t, repo_model.IsWatchingRepo(t.Context(), 8, 1)) + assert.False(t, repo_model.IsWatchingRepo(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) } func TestGetWatchers(t *testing.T) { @@ -109,7 +109,7 @@ func TestWatchIfAuto(t *testing.T) { assert.Len(t, watchers, prevCount+1) // Should remove watch, inhibit from adding auto - assert.NoError(t, repo_model.WatchRepo(t.Context(), user12, repo, false)) + assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false)) watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) @@ -145,7 +145,7 @@ func TestWatchOptions(t *testing.T) { // repo 1 is watched by users 1, 4, 9 and 11, all with every event enabled repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - assert.NoError(t, repo_model.SetWatchOptions(t.Context(), user.ID, repo.ID, repo_model.WatchOptions{PullRequests: true})) + assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchPullRequests: true})) for watchType, expected := range map[repo_model.WatchType][]int64{ repo_model.WatchPullRequests: {1, 4, 9, 11}, @@ -160,11 +160,11 @@ func TestWatchOptions(t *testing.T) { // the options of one user must not show up for another watches, err := repo_model.GetUserWatches(t.Context(), 4, []int64{repo.ID}) assert.NoError(t, err) - assert.True(t, watches[repo.ID].Issues) + assert.True(t, watches[repo.ID].IncludeIssues) // watching again resets a custom selection - assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, false)) - assert.NoError(t, repo_model.WatchRepo(t.Context(), user, repo, true)) + assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user, repo, false)) + assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user, repo, true)) watch, err := repo_model.GetWatch(t.Context(), user.ID, repo.ID) assert.NoError(t, err) assert.True(t, watch.IsWatchingAll()) @@ -172,9 +172,9 @@ func TestWatchOptions(t *testing.T) { func TestWatchSelectedMode(t *testing.T) { // a user without a watch row gets the dummy record, whose flags are the column defaults - assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNone, PullRequests: true, Issues: true, Releases: true}).SelectedMode()) + assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNone, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode()) assert.Equal(t, "participate", (&repo_model.Watch{Mode: repo_model.WatchModeNormal}).SelectedMode()) assert.Equal(t, "ignore", (&repo_model.Watch{Mode: repo_model.WatchModeDont}).SelectedMode()) - assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, Issues: true}).SelectedMode()) - assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, PullRequests: true, Issues: true, Releases: true}).SelectedMode()) + assert.Equal(t, "custom", (&repo_model.Watch{Mode: repo_model.WatchModeNormal, IncludeIssues: true}).SelectedMode()) + assert.Equal(t, "all", (&repo_model.Watch{Mode: repo_model.WatchModeAuto, IncludePullRequests: true, IncludeIssues: true, IncludeReleases: true}).SelectedMode()) } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index e0396efea02..d5f3fc223ee 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -132,7 +132,7 @@ func IsWatching(ctx *context.APIContext) { // "404": // description: User is not watching this repo or repo do not exist - if repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { + if repo_model.IsWatchingRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.JSON(http.StatusOK, api.WatchInfo{ Subscribed: true, Ignored: false, @@ -170,7 +170,7 @@ func Watch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) + err := repo_model.WatchRepoAuto(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { ctx.APIError(http.StatusForbidden, err.Error()) @@ -211,7 +211,7 @@ func Unwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) + err := repo_model.WatchRepoAuto(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index c235f687cb1..2e195dd32da 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -284,7 +284,7 @@ func CreatePost(ctx *context.Context) { handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) } -func handleActionError(ctx *context.Context, err error) { +func handleRepoActionError(ctx *context.Context, err error) { var errLimitReached repo_service.LimitReachedError switch { case errors.Is(err, user_model.ErrBlockedUser): diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go index 79ff8628236..46c969d021f 100644 --- a/routers/web/repo/star.go +++ b/routers/web/repo/star.go @@ -16,7 +16,7 @@ const tplStarUnstar templates.TplName = "repo/header/star" func ActionStar(ctx *context.Context) { err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "star") if err != nil { - handleActionError(ctx, err) + handleRepoActionError(ctx, err) return } diff --git a/routers/web/repo/transfer.go b/routers/web/repo/transfer.go index b09ef7ccb2b..7bbcacdd056 100644 --- a/routers/web/repo/transfer.go +++ b/routers/web/repo/transfer.go @@ -15,7 +15,7 @@ func acceptTransfer(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.Repository.Link()) return } - handleActionError(ctx, err) + handleRepoActionError(ctx, err) } func rejectTransfer(ctx *context.Context) { @@ -25,7 +25,7 @@ func rejectTransfer(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.Repository.Link()) return } - handleActionError(ctx, err) + handleRepoActionError(ctx, err) } func ActionTransfer(ctx *context.Context) { diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go index b2cdf80ecce..adf2d38b7a8 100644 --- a/routers/web/repo/watch.go +++ b/routers/web/repo/watch.go @@ -16,14 +16,18 @@ const tplWatch templates.TplName = "repo/header/watch" func ActionWatch(ctx *context.Context) { action := ctx.PathParam("action") var err error - if action == "ignore" { - err = repo_model.WatchIgnoreRepo(ctx, ctx.Doer, ctx.Repo.Repository) - } else { - all := action == "watch" // "participate" is a watch that subscribes to no event on its own - err = repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{PullRequests: all, Issues: all, Releases: all}) + switch action { + case "ignore": + err = repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{Mode: repo_model.WatchModeDont}) + case "participate": + err = repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{Mode: repo_model.WatchModeNone}) + case "watch": + err = repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchPullRequests: true, WatchIssues: true, WatchReleases: true}) + default: + return // impossible } if err != nil { - handleActionError(ctx, err) + handleRepoActionError(ctx, err) return } @@ -45,12 +49,13 @@ func ActionWatch(ctx *context.Context) { // ActionWatchOptions watches the repository with a custom selection of events func ActionWatchOptions(ctx *context.Context) { opts := repo_model.WatchOptions{ // clearing every event is allowed, it leaves the participating state - PullRequests: ctx.FormBool(string(repo_model.WatchPullRequests)), - Issues: ctx.FormBool(string(repo_model.WatchIssues)), - Releases: ctx.FormBool(string(repo_model.WatchReleases)), + Mode: repo_model.WatchModeNormal, + WatchPullRequests: ctx.FormBool("pull_requests"), + WatchIssues: ctx.FormBool("issues"), + WatchReleases: ctx.FormBool("releases"), } if err := repo_model.WatchRepoWithOptions(ctx, ctx.Doer, ctx.Repo.Repository, opts); err != nil { - handleActionError(ctx, err) + handleRepoActionError(ctx, err) return } ctx.JSONRedirect("") diff --git a/services/feed/feed.go b/services/feed/feed.go index d8b7daeb60b..ade9581778d 100644 --- a/services/feed/feed.go +++ b/services/feed/feed.go @@ -74,13 +74,13 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch: allowed = permCode[i] && watcher.IsWatchingAll() case activities_model.ActionPublishRelease: - allowed = permCode[i] && watcher.Releases + allowed = permCode[i] && watcher.IncludeReleases case activities_model.ActionCreateIssue, activities_model.ActionCommentIssue, activities_model.ActionCloseIssue, activities_model.ActionReopenIssue: - allowed = permIssue[i] && watcher.Issues + allowed = permIssue[i] && watcher.IncludeIssues case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest, activities_model.ActionApprovePullRequest, activities_model.ActionRejectPullRequest, activities_model.ActionPullReviewDismissed, activities_model.ActionPullRequestReadyForReview: - allowed = permPR[i] && watcher.PullRequests + allowed = permPR[i] && watcher.IncludePullRequests default: allowed = watcher.IsWatchingAll() // repository events have no watch option of their own } diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go index 8dc539c14ba..9e066c4ebdc 100644 --- a/services/feed/feed_test.go +++ b/services/feed/feed_test.go @@ -205,7 +205,9 @@ func TestNotifyWatchersRespectsWatchOptions(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // user 1 watches repo 1 for issues only, user 4 keeps every event - assert.NoError(t, repo_model.SetWatchOptions(t.Context(), 1, 1, repo_model.WatchOptions{Issues: true})) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), user1, repo1, repo_model.WatchOptions{Mode: repo_model.WatchModeNormal, WatchIssues: true})) assert.NoError(t, NotifyWatchers(t.Context(), &activities_model.Action{ActUserID: 8, RepoID: 1, OpType: activities_model.ActionCreateIssue}, diff --git a/services/mailer/mail_release_test.go b/services/mailer/mail_release_test.go index e71b2132b6b..9c84090e419 100644 --- a/services/mailer/mail_release_test.go +++ b/services/mailer/mail_release_test.go @@ -40,8 +40,8 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) { admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) unauthorized := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - assert.NoError(t, repo_model.WatchRepo(t.Context(), admin, repo, true)) - assert.NoError(t, repo_model.WatchRepo(t.Context(), unauthorized, repo, true)) + assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), admin, repo, true)) + assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), unauthorized, repo, true)) rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 11}) rel.Repo = nil diff --git a/services/org/org_test.go b/services/org/org_test.go index 2f30db327ab..38bc5796fa5 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -77,7 +77,7 @@ func TestOrg(t *testing.T) { // an outside user watches and stars the repo while the org is still visible watcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) - require.NoError(t, repo_model.WatchRepo(t.Context(), watcher, repo, true)) + require.NoError(t, repo_model.WatchRepoAuto(t.Context(), watcher, repo, true)) require.NoError(t, repo_model.StarRepo(t.Context(), watcher, repo, true)) unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID}) diff --git a/services/org/team.go b/services/org/team.go index b079313306d..63242418bc5 100644 --- a/services/org/team.go +++ b/services/org/team.go @@ -263,7 +263,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode go func(repos []*repo_model.Repository) { for _, repo := range repos { - if err = repo_model.WatchRepo(graceful.GetManager().ShutdownContext(), user, repo, true); err != nil { + if err = repo_model.WatchRepoAuto(graceful.GetManager().ShutdownContext(), user, repo, true); err != nil { log.Error("watch repo failed: %v", err) } } diff --git a/services/org/team_test.go b/services/org/team_test.go index 5275c377b9f..a75cca60645 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -72,7 +72,7 @@ func TestRemoveTeamMemberRemovesSubscriptionsAndStopwatches(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID}) - assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true)) + assert.NoError(t, repo_model.WatchRepoAuto(ctx, user, repo, true)) assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, true)) ok, err := issues_model.CreateIssueStopwatch(ctx, user, issue) assert.NoError(t, err) @@ -82,7 +82,7 @@ func TestRemoveTeamMemberRemovesSubscriptionsAndStopwatches(t *testing.T) { watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID) assert.NoError(t, err) - assert.False(t, repo_model.IsWatchMode(watch.Mode)) + assert.False(t, repo_model.IsWatchModeWatching(watch.Mode)) _, exists, err := issues_model.GetIssueWatch(ctx, user.ID, issue.ID) assert.NoError(t, err) diff --git a/services/org/user.go b/services/org/user.go index 754ffa44de0..c518c4683e4 100644 --- a/services/org/user.go +++ b/services/org/user.go @@ -69,7 +69,7 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us if err != nil { return err } - if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { + if err = repo_model.WatchRepoAuto(ctx, user, repo, false); err != nil { return err } } diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go index 37c41a8a2bd..ac935ce4a14 100644 --- a/services/repository/collaboration.go +++ b/services/repository/collaboration.go @@ -88,7 +88,7 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, colla return err } - if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil { + if err = repo_model.WatchRepoAuto(ctx, collaborator, repo, false); err != nil { return err } @@ -118,7 +118,7 @@ func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *u if has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo); err != nil || has { return err } - if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { + if err := repo_model.WatchRepoAuto(ctx, user, repo, false); err != nil { return err } diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go index ce405919f76..b77fd7d3fe9 100644 --- a/services/repository/collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -59,7 +59,7 @@ func TestRepository_DeleteCollaborationRemovesSubscriptionsAndStopwatches(t *tes user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22}) assert.NoError(t, repo.LoadOwner(ctx)) - assert.NoError(t, repo_model.WatchRepo(ctx, user, repo, true)) + assert.NoError(t, repo_model.WatchRepoAuto(ctx, user, repo, true)) hasAccess, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo) assert.NoError(t, err) @@ -88,7 +88,7 @@ func TestRepository_DeleteCollaborationRemovesSubscriptionsAndStopwatches(t *tes watch, err := repo_model.GetWatch(ctx, user.ID, repo.ID) assert.NoError(t, err) - assert.False(t, repo_model.IsWatchMode(watch.Mode)) + assert.False(t, repo_model.IsWatchModeWatching(watch.Mode)) _, exists, err := issues_model.GetIssueWatch(ctx, user.ID, tempIssue.ID) assert.NoError(t, err) diff --git a/services/repository/create.go b/services/repository/create.go index b8efb9dbb9c..4df886c2dc2 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -438,7 +438,7 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r } if setting.Service.AutoWatchNewRepos { - if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil { + if err = repo_model.WatchRepoAuto(ctx, doer, repo, true); err != nil { return fmt.Errorf("WatchRepo: %w", err) } } diff --git a/services/repository/repo_team.go b/services/repository/repo_team.go index 3a11d1099d2..54706ebd11b 100644 --- a/services/repository/repo_team.go +++ b/services/repository/repo_team.go @@ -50,7 +50,7 @@ func addRepositoryToTeam(ctx context.Context, t *organization.Team, repo *repo_m return fmt.Errorf("getMembers: %w", err) } for _, u := range t.Members { - if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil { + if err = repo_model.WatchRepoAuto(ctx, u, repo, true); err != nil { return fmt.Errorf("watchRepo: %w", err) } } @@ -117,7 +117,7 @@ func removeAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (e continue } - if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { + if err = repo_model.WatchRepoAuto(ctx, user, repo, false); err != nil { return err } @@ -198,7 +198,7 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r continue } - if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil { + if err = repo_model.WatchRepoAuto(ctx, member, repo, false); err != nil { return err } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 92825cb45b0..b0f7cba2bde 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -267,13 +267,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName return fmt.Errorf("decrease old owner repository count: %w", err) } - if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil { + if err := repo_model.WatchRepoAuto(ctx, doer, repo, true); err != nil { return fmt.Errorf("watchRepo: %w", err) } if oldOwner.IsOrganization() { // Remove watch for organization. - if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { + if err := repo_model.WatchRepoAuto(ctx, oldOwner, repo, false); err != nil { return fmt.Errorf("watchRepo [false]: %w", err) } diff --git a/services/user/block.go b/services/user/block.go index 19b4dfa5960..48e6ed353d8 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -183,7 +183,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro } for _, repo := range repos { - if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil { + if err := repo_model.WatchRepoAuto(ctx, watcher, repo, false); err != nil { return err } } diff --git a/templates/repo/header/watch.tmpl b/templates/repo/header/watch.tmpl index 6ce31385074..5ee916b3909 100644 --- a/templates/repo/header/watch.tmpl +++ b/templates/repo/header/watch.tmpl @@ -39,9 +39,9 @@ {{svg "octicon-check" 16 (Iif $isCustom "" "tw-invisible")}}
{{$textCustom}}
{{ctx.Locale.Tr "repo.watch.mode.custom.desc"}}
diff --git a/templates/shared/repo/list.tmpl b/templates/shared/repo/list.tmpl index f1f0a617dc4..545b1c98f8b 100644 --- a/templates/shared/repo/list.tmpl +++ b/templates/shared/repo/list.tmpl @@ -60,9 +60,9 @@ {{end}}