Skip to content

Allow watching custom events #26865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion models/activities/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,14 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
}
toNotify.AddMultiple(issueWatches...)
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
var repoWatches []int64

if issue.IsPull {
repoWatches, err = repo_model.GetRepoWatchersEventIDs(ctx, issue.RepoID, repo_model.WatchEventTypePullRequest)
} else {
repoWatches, err = repo_model.GetRepoWatchersEventIDs(ctx, issue.RepoID, repo_model.WatchEventTypeIssue)
}

if err != nil {
return err
}
Expand Down
121 changes: 114 additions & 7 deletions models/repo/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
)

// WatchMode specifies what kind of watch the user has on a repository
Expand All @@ -24,22 +27,64 @@ const (
WatchModeDont // 2
// WatchModeAuto watch repository (from AutoWatchOnChanges)
WatchModeAuto // 3
// WatchModeCustom watch repository for custom events
WatchModeCustom // 4
)

func (mode WatchMode) IsWatchModeNone() bool {
return mode == WatchModeNone
}

func (mode WatchMode) IsWatchModeNormal() bool {
return mode == WatchModeNormal
}

func (mode WatchMode) IsWatchModeDont() bool {
return mode == WatchModeDont
}

func (mode WatchMode) IsWatchModeAuto() bool {
return mode == WatchModeAuto
}

func (mode WatchMode) IsWatchModeCustom() bool {
return mode == WatchModeCustom
}

// WatchEventType specifies what kind of event is wanted
type WatchEventType int8

const (
// WatchEventTypeIssue watch issues
WatchEventTypeIssue WatchEventType = iota
// WatchEventTypeIssue watch pull requests
WatchEventTypePullRequest
// WatchEventTypeIssue watch releases
WatchEventTypeRelease
)

// 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"`
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"`
CustomIssues bool `xorm:"NOT NULL DEFAULT FALSE"`
CustomPullRequests bool `xorm:"NOT NULL DEFAULT FALSE"`
CustomReleases bool `xorm:"NOT NULL DEFAULT FALSE"`
}

func init() {
db.RegisterModel(new(Watch))
}

// IsWatching checks if user has watched the Repo
func (watch Watch) IsWatching() bool {
return IsWatchMode(watch.Mode)
}

// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
watch := Watch{UserID: userID, RepoID: repoID}
Expand All @@ -65,7 +110,7 @@ func IsWatching(ctx context.Context, userID, repoID int64) bool {
}

func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
if watch.Mode == mode && mode != WatchModeCustom {
return nil
}
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) {
Expand All @@ -85,6 +130,12 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error)

watch.Mode = mode

if mode != WatchModeCustom {
watch.CustomIssues = false
watch.CustomPullRequests = false
watch.CustomReleases = false
}

e := db.GetEngine(ctx)

if !hadrec && needsrec {
Expand Down Expand Up @@ -131,6 +182,19 @@ func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err err
return err
}

func WatchRepoCustom(ctx context.Context, userID, repoID int64, watchTypes api.RepoCustomWatchOptions) error {
watch, err := GetWatch(ctx, userID, repoID)
if err != nil {
return err
}

watch.CustomIssues = watchTypes.Issues
watch.CustomPullRequests = watchTypes.PullRequests
watch.CustomReleases = watchTypes.Releases

return watchRepoMode(ctx, watch, WatchModeCustom)
}

// GetWatchers returns all watchers of given repository.
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10)
Expand Down Expand Up @@ -170,6 +234,49 @@ func GetRepoWatchers(ctx context.Context, repoID int64, opts db.ListOptions) ([]
return users, sess.Find(&users)
}

func getRepoWatchEventCond(event WatchEventType) builder.Cond {
cond := builder.NewCond()

switch event {
case WatchEventTypeIssue:
cond = cond.And(builder.Eq{"`watch`.mode": WatchModeCustom}, builder.Eq{"`watch`.custom_issues": true})
case WatchEventTypePullRequest:
cond = cond.And(builder.Eq{"`watch`.mode": WatchModeCustom}, builder.Eq{"`watch`.custom_pull_requests": true})
case WatchEventTypeRelease:
cond = cond.And(builder.Eq{"`watch`.mode": WatchModeCustom}, builder.Eq{"`watch`.custom_releases": true})
}

return builder.Or(builder.Eq{"`watch`.mode": WatchModeNormal}, builder.Eq{"`watch`.mode": WatchModeAuto}, cond)
}

// GetRepoWatchers returns range of users watching the given event in the given repository.
func GetRepoWatchersEvent(repoID int64, event WatchEventType, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(db.DefaultContext).Where("watch.repo_id=?", repoID).
Join("LEFT", "watch", "`user`.id=`watch`.user_id").
And(getRepoWatchEventCond(event))
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
users := make([]*user_model.User, 0, opts.PageSize)

return users, sess.Find(&users)
}

users := make([]*user_model.User, 0, 8)
return users, sess.Find(&users)
}

// GetRepoWatchersIDs returns IDs of watchers for a given repo ID and the givene Event
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetRepoWatchersEventIDs(ctx context.Context, repoID int64, event WatchEventType) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, db.GetEngine(ctx).Table("watch").
Where("watch.repo_id=?", repoID).
And(getRepoWatchEventCond(event)).
Select("user_id").
Find(&ids)
}

// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set
func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error {
if !isWrite || !setting.Service.AutoWatchOnChanges {
Expand Down
41 changes: 41 additions & 0 deletions models/repo/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -137,3 +138,43 @@ func TestWatchRepoMode(t *testing.T) {
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone))
unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
}

func checkRepoWatchersEvent(t *testing.T, event repo_model.WatchEventType, isWatching bool) {
watchers, err := repo_model.GetRepoWatchersEventIDs(db.DefaultContext, 1, event)
assert.NoError(t, err)

if isWatching {
assert.Len(t, watchers, 1)
assert.Equal(t, int64(12), watchers[0])
} else {
assert.Len(t, watchers, 0)
}
}

func TestWatchRepoCustom(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)

// Make sure nobody is watching this repo
watchers, err := repo_model.GetRepoWatchersIDs(db.DefaultContext, 1)
assert.NoError(t, err)
for _, watcher := range watchers {
assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, watcher, 1, repo_model.WatchModeNone))
}

assert.NoError(t, repo_model.WatchRepoCustom(db.DefaultContext, 12, 1, api.RepoCustomWatchOptions{Issues: true}))
checkRepoWatchersEvent(t, repo_model.WatchEventTypeIssue, true)
checkRepoWatchersEvent(t, repo_model.WatchEventTypePullRequest, false)
checkRepoWatchersEvent(t, repo_model.WatchEventTypeRelease, false)

assert.NoError(t, repo_model.WatchRepoCustom(db.DefaultContext, 12, 1, api.RepoCustomWatchOptions{PullRequests: true}))
checkRepoWatchersEvent(t, repo_model.WatchEventTypeIssue, false)
checkRepoWatchersEvent(t, repo_model.WatchEventTypePullRequest, true)
checkRepoWatchersEvent(t, repo_model.WatchEventTypeRelease, false)

assert.NoError(t, repo_model.WatchRepoCustom(db.DefaultContext, 12, 1, api.RepoCustomWatchOptions{Releases: true}))
checkRepoWatchersEvent(t, repo_model.WatchEventTypeIssue, false)
checkRepoWatchersEvent(t, repo_model.WatchEventTypePullRequest, false)
checkRepoWatchersEvent(t, repo_model.WatchEventTypeRelease, true)
}
6 changes: 6 additions & 0 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
if ctx.IsSigned {
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID)
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)

ctx.Data["RepoWatchData"], err = repo_model.GetWatch(ctx, ctx.Doer.ID, repo.ID)
if err != nil {
ctx.ServerError("getWatch", err)
return nil
}
}

if repo.IsFork {
Expand Down
21 changes: 15 additions & 6 deletions modules/structs/repo_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ import (
"time"
)

// RepoCustomWatchOptions options when watching custom events of a repo
type RepoCustomWatchOptions struct {
Issues bool `json:"issues"`
PullRequests bool `json:"pull_requests"`
Releases bool `json:"releases"`
}

// WatchInfo represents an API watch status of one repository
type WatchInfo struct {
Subscribed bool `json:"subscribed"`
Ignored bool `json:"ignored"`
Reason any `json:"reason"`
CreatedAt time.Time `json:"created_at"`
URL string `json:"url"`
RepositoryURL string `json:"repository_url"`
Subscribed bool `json:"subscribed"`
Ignored bool `json:"ignored"`
IsCustom bool `json:"is_custom"`
Reason any `json:"reason"`
CreatedAt time.Time `json:"created_at"`
URL string `json:"url"`
RepositoryURL string `json:"repository_url"`
CustomWatchOptions RepoCustomWatchOptions `json:"custom_watch_options"`
}
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1151,10 +1151,12 @@ func Routes() *web.Route {
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
m.Get("/stargazers", repo.ListStargazers)
m.Get("/subscribers", repo.ListSubscribers)
m.Get("/subscribers/{event}", repo.ListSubscribersEvent)
m.Group("/subscription", func() {
m.Get("", user.IsWatching)
m.Put("", reqToken(), user.Watch)
m.Delete("", reqToken(), user.Unwatch)
m.Put("/custom", reqToken(), bind(api.RepoCustomWatchOptions{}), user.WatchCustom)
})
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
Expand Down
65 changes: 65 additions & 0 deletions routers/api/v1/repo/subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package repo

import (
"fmt"
"net/http"
"strings"

repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
Expand Down Expand Up @@ -58,3 +60,66 @@ func ListSubscribers(ctx *context.APIContext) {
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches))
ctx.JSON(http.StatusOK, users)
}

// ListSubscribers list a repo's subscribers (i.e. watchers) for the given event
func ListSubscribersEvent(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/subscribers/{event} repository repoListSubscribersEvent
// ---
// summary: List a repo's watchers for the given event
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: event
// in: path
// description: the event
// type: string
// enum: [issues,pullrequests,releases]
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
var eventConst repo_model.WatchEventType

switch strings.ToLower(ctx.Params("event")) {
case "issues":
eventConst = repo_model.WatchEventTypeIssue
case "pullrequests":
eventConst = repo_model.WatchEventTypePullRequest
case "releases":
eventConst = repo_model.WatchEventTypeRelease
default:
ctx.Error(http.StatusBadRequest, "InvalidEvent", fmt.Errorf("%s is not a valid event", ctx.Params("event")))
return
}

subscribers, err := repo_model.GetRepoWatchersEvent(ctx.Repo.Repository.ID, eventConst, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetRepoWatchersEvent", err)
return
}
users := make([]*api.User, len(subscribers))
for i, subscriber := range subscribers {
users[i] = convert.ToUser(ctx, subscriber, ctx.Doer)
}

ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches))
ctx.JSON(http.StatusOK, users)
}
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,7 @@ type swaggerParameterBodies struct {

// in:body
CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption

// in:body
RepoCustomWatchOptions api.RepoCustomWatchOptions
}
Loading