Skip to content

AutoSubscribe to Issue when create or comment to it #9535

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
7cd9881
squash-rebase
6543 Jan 30, 2020
39bfe5b
Dont Drop - Alter table column
6543 Feb 12, 2020
9e17364
remove is_watching from models completly
6543 Feb 12, 2020
038bcef
nit
6543 Feb 12, 2020
94171ed
delete if IssueWatchMode = None
6543 Feb 12, 2020
5cd8c73
update comments
6543 Feb 12, 2020
c6b3ea1
fix test
6543 Feb 12, 2020
d6a764b
format code ...
6543 Feb 12, 2020
3aa7e09
fix & optimize Create/Update-IW
6543 Feb 12, 2020
89e2e6a
tests ...
6543 Feb 12, 2020
9c316f6
fix lint
6543 Feb 12, 2020
483a212
extend Test
6543 Feb 12, 2020
e4a48cd
fix mssql migration
6543 Feb 12, 2020
afa745a
NULL as default
6543 Feb 12, 2020
bd44ba4
rework api stuff
6543 Feb 12, 2020
840f011
extend SET/UNSET issue subscritpion via API
6543 Feb 12, 2020
f4f8aef
fix lint
6543 Feb 12, 2020
48d30f1
new trick
6543 Feb 12, 2020
26acd3d
remove debug msg
6543 Feb 12, 2020
757b29b
make migration great again
6543 Feb 12, 2020
a162f26
add TEST
6543 Feb 13, 2020
d8d2e83
optimize!
6543 Feb 13, 2020
c8070d8
update SWAGGER
6543 Feb 13, 2020
ad12dd9
dont use IssueWatchModeNone
6543 Feb 16, 2020
760a389
Apply suggestions from code review
6543 Feb 16, 2020
9b578b4
new migration func
6543 Feb 16, 2020
370467c
remove IF EXISTS
6543 Feb 17, 2020
0669704
start IssueWatchMode with 1 & remove DEFAULT=1
6543 Feb 17, 2020
38c297a
add testcase to fixtures
6543 Feb 18, 2020
9482024
no race
6543 Feb 18, 2020
51171c2
fix sqlite
6543 Feb 18, 2020
893ad5c
Alter table outside a session
6543 Feb 18, 2020
2b31fc6
fix SQLite migration
6543 Feb 18, 2020
0c7826a
fix CreateOrUpdateIssueWatchMode
6543 Feb 18, 2020
f3b0059
fix
6543 Feb 18, 2020
1f78c80
now it should work
6543 Feb 18, 2020
aedbad5
Update models/migrations/v999.go
6543 Feb 19, 2020
7a0c75b
test async
6543 Feb 19, 2020
4186279
test Sync2 on sqlite again
6543 Feb 19, 2020
9bc4d33
sqlite dont like the dot
6543 Feb 19, 2020
f354e61
test
6543 Feb 19, 2020
4195f43
wait until async task finished
6543 Feb 19, 2020
547b5f9
mssql dont understand IfExist for AlterTable
6543 Feb 19, 2020
220cdfe
postgress fix
6543 Feb 19, 2020
8ab3f79
update README of integrations
6543 Feb 19, 2020
f2e9a75
new atempt
6543 Feb 19, 2020
96abc11
easy solution
6543 Feb 19, 2020
c3efb3d
some tweeks
6543 Feb 19, 2020
04e582e
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 19, 2020
413fa6d
do it within a session
6543 Feb 19, 2020
a069ab3
finalize
6543 Feb 19, 2020
c5d2cf9
no async
6543 Feb 19, 2020
060e28b
no async anymore
6543 Feb 19, 2020
d28af72
simple delete
6543 Feb 19, 2020
a908156
Revert "remove sleep 1 sec"
6543 Feb 19, 2020
6483396
fix deleteIssueWatch
6543 Feb 19, 2020
c0399e7
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 19, 2020
9f7148c
seperate docs update
6543 Feb 21, 2020
f29de15
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 21, 2020
8e44467
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 23, 2020
f6c9883
do not extend api
6543 Feb 23, 2020
e3e1bbd
remove unused
6543 Feb 23, 2020
9718787
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 26, 2020
bc0e963
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 26, 2020
e12849d
Merge branch 'master' into fix_8243-auto-subscribe-issue-on-interact
6543 Feb 27, 2020
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
33 changes: 33 additions & 0 deletions integrations/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository"

"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -57,6 +58,38 @@ func TestNoLoginViewIssues(t *testing.T) {
MakeRequest(t, req, http.StatusOK)
}

func TestIssueAutoSubscription(t *testing.T) {
defer prepareTestEnv(t)()

user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
otherUser := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
session := loginUser(t, user.Name)

// create repo
repo, err := repo_service.CreateRepository(otherUser, otherUser, models.CreateRepoOptions{
Name: "TESTIssueAutoSubscriptionRepo",
License: "MIT",
Readme: "Default",
IsPrivate: false,
AutoInit: true,
})
assert.NoError(t, err)

//make sure repo is not subscribed
models.AssertNotExistsBean(t, &models.Watch{UserID: user.ID, RepoID: repo.ID})

//create new issue
testNewIssue(t, session, repo.OwnerName, repo.Name, "some title text", "some body text")
newIssue, err := models.GetIssueByIndex(repo.ID, 1)
assert.NoError(t, err)

time.Sleep(1 * time.Second)

models.AssertNotExistsBean(t, &models.Watch{UserID: user.ID, RepoID: repo.ID})
issueWatch := models.AssertExistsAndLoadBean(t, &models.IssueWatch{UserID: user.ID, IssueID: newIssue.ID}).(*models.IssueWatch)
assert.Equal(t, models.IssueWatchModeAuto, issueWatch.Mode)
}

func TestViewIssuesSortByType(t *testing.T) {
defer prepareTestEnv(t)()

Expand Down
16 changes: 12 additions & 4 deletions models/fixtures/issue_watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@
id: 1
user_id: 9
issue_id: 1
is_watching: true
mode: 1 #IssueWatchModeNormal
created_unix: 946684800
updated_unix: 946684800

-
id: 2
user_id: 2
issue_id: 2
is_watching: false
mode: 2 #IssueWatchModeDont
created_unix: 946684800
updated_unix: 946684800

-
id: 3
user_id: 2
issue_id: 7
is_watching: true
mode: 1 #IssueWatchModeNormal
created_unix: 946684800
updated_unix: 946684800

-
id: 4
user_id: 1
issue_id: 7
is_watching: false
mode: 2 #IssueWatchModeDont
created_unix: 946684800
updated_unix: 946684800

-
id: 5
user_id: 1
issue_id: 8
mode: 3 #IssueWatchModeAuto
created_unix: 946684800
updated_unix: 946684800
108 changes: 76 additions & 32 deletions models/issue_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,72 @@
package models

import (
"fmt"
"time"

"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
)

// IssueWatchMode specifies what kind of watch the user has on a issue
type IssueWatchMode int8

const (
// IssueWatchModeNormal watch issue
IssueWatchModeNormal IssueWatchMode = iota + 1 // 1
// IssueWatchModeDont explicit don't watch
IssueWatchModeDont // 2
// IssueWatchModeAuto watch issue (from AutoWatchOnIssueChanges)
IssueWatchModeAuto // 3
)

// IssueWatch is connection request for receiving issue notification.
type IssueWatch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IsWatching bool `xorm:"NOT NULL"`
Mode IssueWatchMode `xorm:"NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
}

// IssueWatchList contains IssueWatch
type IssueWatchList []*IssueWatch

// CreateOrUpdateIssueWatch set watching for a user and issue
func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error {
iw, exists, err := getIssueWatch(x, userID, issueID)
if err != nil {
// CreateOrUpdateIssueWatchMode set IssueWatchMode for a user and issue
func CreateOrUpdateIssueWatchMode(userID, issueID int64, mode IssueWatchMode) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := createOrUpdateIssueWatchMode(sess, userID, issueID, mode); err != nil {
return err
}
return sess.Commit()
}

if !exists {
iw = &IssueWatch{
UserID: userID,
IssueID: issueID,
IsWatching: isWatching,
}

if _, err := x.Insert(iw); err != nil {
return err
}
} else {
iw.IsWatching = isWatching

if _, err := x.ID(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil {
return err
}
func createOrUpdateIssueWatchMode(e Engine, userID, issueID int64, mode IssueWatchMode) error {
if _, err := e.Exec(fmt.Sprintf("INSERT INTO issue_watch(user_id,issue_id,mode,created_unix,updated_unix) SELECT %d,%d,%d,%d,%d WHERE NOT EXISTS(SELECT 1 FROM issue_watch WHERE user_id = %d AND issue_id = %d);", userID, issueID, mode, time.Now().Unix(), time.Now().Unix(), userID, issueID)); err != nil {
return err
}
iw, exist, err := getIssueWatch(e, userID, issueID)
if err != nil && !exist {
return err
}
iw.Mode = mode
iw.UpdatedUnix = timeutil.TimeStampNow()
if _, err := e.ID(iw.ID).Cols("updated_unix", "mode").Update(iw); err != nil {
return err
}
return nil
}

// GetIssueWatch returns all IssueWatch objects from db by user and issue
// the current Web-UI need iw object for watchers AND explicit non-watchers
func GetIssueWatch(userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
return getIssueWatch(x, userID, issueID)
}

// Return watcher AND explicit non-watcher if entry in db exist
func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
iw = new(IssueWatch)
exists, err = e.
Expand All @@ -67,28 +83,40 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetIssueWatchersIDs(issueID int64, watching bool) ([]int64, error) {
return getIssueWatchersIDs(x, issueID, watching)
func GetIssueWatchersIDs(issueID int64, modes ...IssueWatchMode) ([]int64, error) {
if len(modes) == 0 {
modes = []IssueWatchMode{IssueWatchModeNormal, IssueWatchModeAuto}
}
return getIssueWatchersIDs(x, issueID, modes...)
}

func getIssueWatchersIDs(e Engine, issueID int64, watching bool) ([]int64, error) {
func getIssueWatchersIDs(e Engine, issueID int64, modes ...IssueWatchMode) ([]int64, error) {
ids := make([]int64, 0, 64)
if len(modes) == 0 {
return nil, fmt.Errorf("no IssueWatchMode set")
}
return ids, e.Table("issue_watch").
Where("issue_id=?", issueID).
And("is_watching = ?", watching).
In("mode", modes).
Select("user_id").
Find(&ids)
}

// GetIssueWatchers returns watchers/unwatchers of a given issue
func GetIssueWatchers(issueID int64, listOptions ListOptions) (IssueWatchList, error) {
return getIssueWatchers(x, issueID, listOptions)
// GetIssueWatchers returns IssueWatch entry's based on modes of a given issue
func GetIssueWatchers(issueID int64, listOptions ListOptions, modes ...IssueWatchMode) (IssueWatchList, error) {
if len(modes) == 0 {
modes = []IssueWatchMode{IssueWatchModeNormal, IssueWatchModeAuto}
}
return getIssueWatchers(x, issueID, listOptions, modes...)
}

func getIssueWatchers(e Engine, issueID int64, listOptions ListOptions) (IssueWatchList, error) {
func getIssueWatchers(e Engine, issueID int64, listOptions ListOptions, modes ...IssueWatchMode) (IssueWatchList, error) {
if len(modes) == 0 {
return nil, fmt.Errorf("no IssueWatchMode set")
}
sess := e.
Where("`issue_watch`.issue_id = ?", issueID).
And("`issue_watch`.is_watching = ?", true).
In("`issue_watch`.mode", modes).
And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
Expand All @@ -109,3 +137,19 @@ func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error {
Delete(new(IssueWatch))
return err
}

// IsWatching is true if user iw watching either repo or issue (backwards compatibility)
func (iw IssueWatch) IsWatching() bool {
issue, err := GetIssueByID(iw.IssueID)
if err != nil {
// fail silent since template expect only bool
log.Warn("IssueWatch.IsWatching: GetIssueByID: ", err)
return false
}
// if RepoWatch is true ...
if IsWatching(iw.UserID, issue.RepoID) && iw.Mode != IssueWatchModeDont {
return true
}

return iw.Mode == IssueWatchModeNormal || iw.Mode == IssueWatchModeAuto
}
29 changes: 24 additions & 5 deletions models/issue_watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,33 @@ package models

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

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

assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true))
assert.NoError(t, CreateOrUpdateIssueWatchMode(3, 1, IssueWatchModeNormal))
iw := AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch)
assert.True(t, iw.IsWatching)
assert.EqualValues(t, IssueWatchModeNormal, iw.Mode)

assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false))
assert.NoError(t, DeleteIssueWatch(3, 1))
AssertNotExistsBean(t, &IssueWatch{UserID: 3, IssueID: 1})

assert.NoError(t, CreateOrUpdateIssueWatchMode(3, 1, IssueWatchModeAuto))
iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch)
assert.EqualValues(t, IssueWatchModeAuto, iw.Mode)

assert.NoError(t, CreateOrUpdateIssueWatchMode(1, 1, IssueWatchModeAuto))
iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch)
assert.EqualValues(t, IssueWatchModeAuto, iw.Mode)

time.Sleep(1 * time.Second)
assert.NoError(t, CreateOrUpdateIssueWatchMode(1, 1, IssueWatchModeDont))
iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch)
assert.False(t, iw.IsWatching)
assert.EqualValues(t, IssueWatchModeDont, iw.Mode)
}

func TestGetIssueWatch(t *testing.T) {
Expand All @@ -32,11 +45,17 @@ func TestGetIssueWatch(t *testing.T) {
iw, exists, err := GetIssueWatch(2, 2)
assert.True(t, exists)
assert.NoError(t, err)
assert.EqualValues(t, false, iw.IsWatching)
assert.False(t, iw.IsWatching())

_, exists, err = GetIssueWatch(3, 1)
assert.False(t, exists)
assert.NoError(t, err)

assert.False(t, IsWatching(1, 10))
iw, exists, err = GetIssueWatch(1, 8)
assert.True(t, exists)
assert.NoError(t, err)
assert.True(t, iw.IsWatching())
}

func TestGetIssueWatchers(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ var migrations = []Migration{
NewMigration("Fix topic repository count", fixTopicRepositoryCount),
// v127 -> v128
NewMigration("add repository code language statistics", addLanguageStats),
// v128 -> v129
NewMigration("Change from IsWatching to Modes at IssueWatch", addIssueWatchModes),
}

// Migrate database to current version
Expand Down
48 changes: 48 additions & 0 deletions models/migrations/v128.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"fmt"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func addIssueWatchModes(x *xorm.Engine) error {
type IssueWatch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"`
Mode models.IssueWatchMode `xorm:"NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
}

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}

if err := sess.Sync2(new(IssueWatch)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}

if _, err := sess.Where("is_watching = ?", false).Cols("mode").Update(&models.IssueWatch{Mode: models.IssueWatchModeDont}); err != nil {
return err
}
if _, err := sess.Where("is_watching = ?", true).Cols("mode").Update(&models.IssueWatch{Mode: models.IssueWatchModeNormal}); err != nil {
return err
}

if err := dropTableColumns(sess, "issue_watch", "is_watching"); err != nil {
return err
}

return sess.Commit()
}
Loading