diff options
Diffstat (limited to 'models/repo')
-rw-r--r-- | models/repo/archiver.go | 7 | ||||
-rw-r--r-- | models/repo/fork.go | 69 | ||||
-rw-r--r-- | models/repo/fork_test.go | 32 | ||||
-rw-r--r-- | models/repo/main_test.go | 6 | ||||
-rw-r--r-- | models/repo/redirect.go | 82 | ||||
-rw-r--r-- | models/repo/redirect_test.go | 77 | ||||
-rw-r--r-- | models/repo/repo.go | 36 | ||||
-rw-r--r-- | models/repo/repo_test.go | 7 | ||||
-rw-r--r-- | models/repo/repo_unit.go | 26 | ||||
-rw-r--r-- | models/repo/star.go | 90 | ||||
-rw-r--r-- | models/repo/star_test.go | 53 | ||||
-rw-r--r-- | models/repo/topic.go | 369 | ||||
-rw-r--r-- | models/repo/topic_test.go | 80 | ||||
-rw-r--r-- | models/repo/update.go | 179 | ||||
-rw-r--r-- | models/repo/watch.go | 196 | ||||
-rw-r--r-- | models/repo/watch_test.go | 139 |
16 files changed, 1448 insertions, 0 deletions
diff --git a/models/repo/archiver.go b/models/repo/archiver.go index cee6013ca3..c29891397f 100644 --- a/models/repo/archiver.go +++ b/models/repo/archiver.go @@ -107,3 +107,10 @@ func FindRepoArchives(opts FindRepoArchiversOption) ([]*RepoArchiver, error) { Find(&archivers) return archivers, err } + +// SetArchiveRepoState sets if a repo is archived +func SetArchiveRepoState(repo *Repository, isArchived bool) (err error) { + repo.IsArchived = isArchived + _, err = db.GetEngine(db.DefaultContext).Where("id = ?", repo.ID).Cols("is_archived").NoAutoTime().Update(repo) + return +} diff --git a/models/repo/fork.go b/models/repo/fork.go new file mode 100644 index 0000000000..570a5b68ab --- /dev/null +++ b/models/repo/fork.go @@ -0,0 +1,69 @@ +// Copyright 2021 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 repo + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +func getRepositoriesByForkID(e db.Engine, forkID int64) ([]*Repository, error) { + repos := make([]*Repository, 0, 10) + return repos, e. + Where("fork_id=?", forkID). + Find(&repos) +} + +// GetRepositoriesByForkID returns all repositories with given fork ID. +func GetRepositoriesByForkID(ctx context.Context, forkID int64) ([]*Repository, error) { + return getRepositoriesByForkID(db.GetEngine(ctx), forkID) +} + +// GetForkedRepo checks if given user has already forked a repository with given ID. +func GetForkedRepo(ownerID, repoID int64) *Repository { + repo := new(Repository) + has, _ := db.GetEngine(db.DefaultContext). + Where("owner_id=? AND fork_id=?", ownerID, repoID). + Get(repo) + if has { + return repo + } + return nil +} + +// HasForkedRepo checks if given user has already forked a repository with given ID. +func HasForkedRepo(ownerID, repoID int64) bool { + has, _ := db.GetEngine(db.DefaultContext). + Table("repository"). + Where("owner_id=? AND fork_id=?", ownerID, repoID). + Exist() + return has +} + +// GetUserFork return user forked repository from this repository, if not forked return nil +func GetUserFork(repoID, userID int64) (*Repository, error) { + var forkedRepo Repository + has, err := db.GetEngine(db.DefaultContext).Where("fork_id = ?", repoID).And("owner_id = ?", userID).Get(&forkedRepo) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return &forkedRepo, nil +} + +// GetForks returns all the forks of the repository +func GetForks(repo *Repository, listOptions db.ListOptions) ([]*Repository, error) { + if listOptions.Page == 0 { + forks := make([]*Repository, 0, repo.NumForks) + return forks, db.GetEngine(db.DefaultContext).Find(&forks, &Repository{ForkID: repo.ID}) + } + + sess := db.GetPaginatedSession(&listOptions) + forks := make([]*Repository, 0, listOptions.PageSize) + return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) +} diff --git a/models/repo/fork_test.go b/models/repo/fork_test.go new file mode 100644 index 0000000000..bf6b90b388 --- /dev/null +++ b/models/repo/fork_test.go @@ -0,0 +1,32 @@ +// Copyright 2021 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 repo + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestGetUserFork(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // User13 has repo 11 forked from repo10 + repo, err := GetRepositoryByID(10) + assert.NoError(t, err) + assert.NotNil(t, repo) + repo, err = GetUserFork(repo.ID, 13) + assert.NoError(t, err) + assert.NotNil(t, repo) + + repo, err = GetRepositoryByID(9) + assert.NoError(t, err) + assert.NotNil(t, repo) + repo, err = GetUserFork(repo.ID, 13) + assert.NoError(t, err) + assert.Nil(t, repo) +} diff --git a/models/repo/main_test.go b/models/repo/main_test.go index f40a976281..fdd6c3f4d3 100644 --- a/models/repo/main_test.go +++ b/models/repo/main_test.go @@ -18,5 +18,11 @@ func TestMain(m *testing.M) { "repository.yml", "repo_unit.yml", "repo_indexer_status.yml", + "repo_redirect.yml", + "watch.yml", + "star.yml", + "topic.yml", + "repo_topic.yml", + "user.yml", ) } diff --git a/models/repo/redirect.go b/models/repo/redirect.go new file mode 100644 index 0000000000..88fad6f3e3 --- /dev/null +++ b/models/repo/redirect.go @@ -0,0 +1,82 @@ +// Copyright 2017 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 repo + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" +) + +// ErrRedirectNotExist represents a "RedirectNotExist" kind of error. +type ErrRedirectNotExist struct { + OwnerID int64 + RepoName string +} + +// IsErrRedirectNotExist check if an error is an ErrRepoRedirectNotExist. +func IsErrRedirectNotExist(err error) bool { + _, ok := err.(ErrRedirectNotExist) + return ok +} + +func (err ErrRedirectNotExist) Error() string { + return fmt.Sprintf("repository redirect does not exist [uid: %d, name: %s]", err.OwnerID, err.RepoName) +} + +// Redirect represents that a repo name should be redirected to another +type Redirect struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s)"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + RedirectRepoID int64 // repoID to redirect to +} + +// TableName represents real table name in database +func (Redirect) TableName() string { + return "repo_redirect" +} + +func init() { + db.RegisterModel(new(Redirect)) +} + +// LookupRedirect look up if a repository has a redirect name +func LookupRedirect(ownerID int64, repoName string) (int64, error) { + repoName = strings.ToLower(repoName) + redirect := &Redirect{OwnerID: ownerID, LowerName: repoName} + if has, err := db.GetEngine(db.DefaultContext).Get(redirect); err != nil { + return 0, err + } else if !has { + return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName} + } + return redirect.RedirectRepoID, nil +} + +// NewRedirect create a new repo redirect +func NewRedirect(ctx context.Context, ownerID, repoID int64, oldRepoName, newRepoName string) error { + oldRepoName = strings.ToLower(oldRepoName) + newRepoName = strings.ToLower(newRepoName) + + if err := DeleteRedirect(ctx, ownerID, newRepoName); err != nil { + return err + } + + return db.Insert(ctx, &Redirect{ + OwnerID: ownerID, + LowerName: oldRepoName, + RedirectRepoID: repoID, + }) +} + +// DeleteRedirect delete any redirect from the specified repo name to +// anything else +func DeleteRedirect(ctx context.Context, ownerID int64, repoName string) error { + repoName = strings.ToLower(repoName) + _, err := db.GetEngine(ctx).Delete(&Redirect{OwnerID: ownerID, LowerName: repoName}) + return err +} diff --git a/models/repo/redirect_test.go b/models/repo/redirect_test.go new file mode 100644 index 0000000000..2dca2cbbfd --- /dev/null +++ b/models/repo/redirect_test.go @@ -0,0 +1,77 @@ +// Copyright 2017 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 repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestLookupRedirect(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repoID, err := LookupRedirect(2, "oldrepo1") + assert.NoError(t, err) + assert.EqualValues(t, 1, repoID) + + _, err = LookupRedirect(unittest.NonexistentID, "doesnotexist") + assert.True(t, IsErrRedirectNotExist(err)) +} + +func TestNewRedirect(t *testing.T) { + // redirect to a completely new name + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) + + unittest.AssertExistsAndLoadBean(t, &Redirect{ + OwnerID: repo.OwnerID, + LowerName: repo.LowerName, + RedirectRepoID: repo.ID, + }) + unittest.AssertExistsAndLoadBean(t, &Redirect{ + OwnerID: repo.OwnerID, + LowerName: "oldrepo1", + RedirectRepoID: repo.ID, + }) +} + +func TestNewRedirect2(t *testing.T) { + // redirect to previously used name + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "oldrepo1")) + + unittest.AssertExistsAndLoadBean(t, &Redirect{ + OwnerID: repo.OwnerID, + LowerName: repo.LowerName, + RedirectRepoID: repo.ID, + }) + unittest.AssertNotExistsBean(t, &Redirect{ + OwnerID: repo.OwnerID, + LowerName: "oldrepo1", + RedirectRepoID: repo.ID, + }) +} + +func TestNewRedirect3(t *testing.T) { + // redirect for a previously-unredirected repo + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) + assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) + + unittest.AssertExistsAndLoadBean(t, &Redirect{ + OwnerID: repo.OwnerID, + LowerName: repo.LowerName, + RedirectRepoID: repo.ID, + }) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 9353e813bc..8907691dde 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -25,6 +25,20 @@ import ( "code.gitea.io/gitea/modules/util" ) +var ( + reservedRepoNames = []string{".", ".."} + reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} +) + +// IsUsableRepoName returns true when repository is usable +func IsUsableRepoName(name string) error { + if db.AlphaDashDotPattern.MatchString(name) { + // Note: usually this error is normally caught up earlier in the UI + return db.ErrNameCharsNotAllowed{Name: name} + } + return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name) +} + // TrustModelType defines the types of trust model for this repository type TrustModelType int @@ -734,3 +748,25 @@ func GetPublicRepositoryCount(u *user_model.User) (int64, error) { func GetPrivateRepositoryCount(u *user_model.User) (int64, error) { return getPrivateRepositoryCount(db.GetEngine(db.DefaultContext), u) } + +// IterateRepository iterate repositories +func IterateRepository(f func(repo *Repository) error) error { + var start int + batchSize := setting.Database.IterateBufferSize + for { + repos := make([]*Repository, 0, batchSize) + if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&repos); err != nil { + return err + } + if len(repos) == 0 { + return nil + } + start += len(repos) + + for _, repo := range repos { + if err := f(repo); err != nil { + return err + } + } + } +} diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6f48a22e49..92b95f1d41 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -42,3 +42,10 @@ func TestGetPrivateRepositoryCount(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(2), count) } + +func TestRepoAPIURL(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL()) +} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 5f6c43f02f..1957f88ff3 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -242,3 +242,29 @@ func UpdateRepoUnit(unit *RepoUnit) error { _, err := db.GetEngine(db.DefaultContext).ID(unit.ID).Update(unit) return err } + +// UpdateRepositoryUnits updates a repository's units +func UpdateRepositoryUnits(repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + // Delete existing settings of units before adding again + for _, u := range units { + deleteUnitTypes = append(deleteUnitTypes, u.Type) + } + + if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil { + return err + } + + if len(units) > 0 { + if err = db.Insert(ctx, units); err != nil { + return err + } + } + + return committer.Commit() +} diff --git a/models/repo/star.go b/models/repo/star.go new file mode 100644 index 0000000000..8db297e3b4 --- /dev/null +++ b/models/repo/star.go @@ -0,0 +1,90 @@ +// Copyright 2016 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 repo + +import ( + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" +) + +// Star represents a starred repo by an user. +type Star struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"UNIQUE(s)"` + RepoID int64 `xorm:"UNIQUE(s)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func init() { + db.RegisterModel(new(Star)) +} + +// StarRepo or unstar repository. +func StarRepo(userID, repoID int64, star bool) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + staring := isStaring(db.GetEngine(ctx), userID, repoID) + + if star { + if staring { + return nil + } + + if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + return err + } + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil { + return err + } + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil { + return err + } + } else { + if !staring { + return nil + } + + if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + return err + } + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil { + return err + } + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil { + return err + } + } + + return committer.Commit() +} + +// IsStaring checks if user has starred given repository. +func IsStaring(userID, repoID int64) bool { + return isStaring(db.GetEngine(db.DefaultContext), userID, repoID) +} + +func isStaring(e db.Engine, userID, repoID int64) bool { + has, _ := e.Get(&Star{UID: userID, RepoID: repoID}) + return has +} + +// GetStargazers returns the users that starred the repo. +func GetStargazers(repo *Repository, opts db.ListOptions) ([]*user_model.User, error) { + sess := db.GetEngine(db.DefaultContext).Where("star.repo_id = ?", repo.ID). + Join("LEFT", "star", "`user`.id = star.uid") + 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) +} diff --git a/models/repo/star_test.go b/models/repo/star_test.go new file mode 100644 index 0000000000..20c4b6bef4 --- /dev/null +++ b/models/repo/star_test.go @@ -0,0 +1,53 @@ +// Copyright 2017 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 repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestStarRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + const userID = 2 + const repoID = 1 + unittest.AssertNotExistsBean(t, &Star{UID: userID, RepoID: repoID}) + assert.NoError(t, StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &Star{UID: userID, RepoID: repoID}) + assert.NoError(t, StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &Star{UID: userID, RepoID: repoID}) + assert.NoError(t, StarRepo(userID, repoID, false)) + unittest.AssertNotExistsBean(t, &Star{UID: userID, RepoID: repoID}) +} + +func TestIsStaring(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.True(t, IsStaring(2, 4)) + assert.False(t, IsStaring(3, 4)) +} + +func TestRepository_GetStargazers(t *testing.T) { + // repo with stargazers + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository) + gazers, err := GetStargazers(repo, db.ListOptions{Page: 0}) + assert.NoError(t, err) + if assert.Len(t, gazers, 1) { + assert.Equal(t, int64(2), gazers[0].ID) + } +} + +func TestRepository_GetStargazers2(t *testing.T) { + // repo with stargazers + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) + gazers, err := GetStargazers(repo, db.ListOptions{Page: 0}) + assert.NoError(t, err) + assert.Len(t, gazers, 0) +} diff --git a/models/repo/topic.go b/models/repo/topic.go new file mode 100644 index 0000000000..121863519b --- /dev/null +++ b/models/repo/topic.go @@ -0,0 +1,369 @@ +// Copyright 2018 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 repo + +import ( + "context" + "fmt" + "regexp" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +func init() { + db.RegisterModel(new(Topic)) + db.RegisterModel(new(RepoTopic)) +} + +var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + +// Topic represents a topic of repositories +type Topic struct { + ID int64 `xorm:"pk autoincr"` + Name string `xorm:"UNIQUE VARCHAR(50)"` + RepoCount int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +// RepoTopic represents associated repositories and topics +type RepoTopic struct { //revive:disable-line:exported + RepoID int64 `xorm:"pk"` + TopicID int64 `xorm:"pk"` +} + +// ErrTopicNotExist represents an error that a topic is not exist +type ErrTopicNotExist struct { + Name string +} + +// IsErrTopicNotExist checks if an error is an ErrTopicNotExist. +func IsErrTopicNotExist(err error) bool { + _, ok := err.(ErrTopicNotExist) + return ok +} + +// Error implements error interface +func (err ErrTopicNotExist) Error() string { + return fmt.Sprintf("topic is not exist [name: %s]", err.Name) +} + +// ValidateTopic checks a topic by length and match pattern rules +func ValidateTopic(topic string) bool { + return len(topic) <= 35 && topicPattern.MatchString(topic) +} + +// SanitizeAndValidateTopics sanitizes and checks an array or topics +func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []string) { + validTopics = make([]string, 0) + mValidTopics := make(map[string]struct{}) + invalidTopics = make([]string, 0) + + for _, topic := range topics { + topic = strings.TrimSpace(strings.ToLower(topic)) + // ignore empty string + if len(topic) == 0 { + continue + } + // ignore same topic twice + if _, ok := mValidTopics[topic]; ok { + continue + } + if ValidateTopic(topic) { + validTopics = append(validTopics, topic) + mValidTopics[topic] = struct{}{} + } else { + invalidTopics = append(invalidTopics, topic) + } + } + + return validTopics, invalidTopics +} + +// GetTopicByName retrieves topic by name +func GetTopicByName(name string) (*Topic, error) { + var topic Topic + if has, err := db.GetEngine(db.DefaultContext).Where("name = ?", name).Get(&topic); err != nil { + return nil, err + } else if !has { + return nil, ErrTopicNotExist{name} + } + return &topic, nil +} + +// addTopicByNameToRepo adds a topic name to a repo and increments the topic count. +// Returns topic after the addition +func addTopicByNameToRepo(e db.Engine, repoID int64, topicName string) (*Topic, error) { + var topic Topic + has, err := e.Where("name = ?", topicName).Get(&topic) + if err != nil { + return nil, err + } + if !has { + topic.Name = topicName + topic.RepoCount = 1 + if _, err := e.Insert(&topic); err != nil { + return nil, err + } + } else { + topic.RepoCount++ + if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil { + return nil, err + } + } + + if _, err := e.Insert(&RepoTopic{ + RepoID: repoID, + TopicID: topic.ID, + }); err != nil { + return nil, err + } + + return &topic, nil +} + +// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count +func removeTopicFromRepo(e db.Engine, repoID int64, topic *Topic) error { + topic.RepoCount-- + if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil { + return err + } + + if _, err := e.Delete(&RepoTopic{ + RepoID: repoID, + TopicID: topic.ID, + }); err != nil { + return err + } + + return nil +} + +// RemoveTopicsFromRepo remove all topics from the repo and decrements respective topics repo count +func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error { + e := db.GetEngine(ctx) + _, err := e.Where( + builder.In("id", + builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}), + ), + ).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{}) + if err != nil { + return err + } + + if _, err = e.Delete(&RepoTopic{RepoID: repoID}); err != nil { + return err + } + + return nil +} + +// FindTopicOptions represents the options when fdin topics +type FindTopicOptions struct { + db.ListOptions + RepoID int64 + Keyword string +} + +func (opts *FindTopicOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID}) + } + + if opts.Keyword != "" { + cond = cond.And(builder.Like{"topic.name", opts.Keyword}) + } + + return cond +} + +// FindTopics retrieves the topics via FindTopicOptions +func FindTopics(opts *FindTopicOptions) ([]*Topic, int64, error) { + sess := db.GetEngine(db.DefaultContext).Select("topic.*").Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + } + if opts.PageSize != 0 && opts.Page != 0 { + sess = db.SetSessionPagination(sess, opts) + } + topics := make([]*Topic, 0, 10) + total, err := sess.Desc("topic.repo_count").FindAndCount(&topics) + return topics, total, err +} + +// CountTopics counts the number of topics matching the FindTopicOptions +func CountTopics(opts *FindTopicOptions) (int64, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toConds()) + if opts.RepoID > 0 { + sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + } + return sess.Count(new(Topic)) +} + +// GetRepoTopicByName retrieves topic from name for a repo if it exist +func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) { + return getRepoTopicByName(db.GetEngine(db.DefaultContext), repoID, topicName) +} + +func getRepoTopicByName(e db.Engine, repoID int64, topicName string) (*Topic, error) { + cond := builder.NewCond() + var topic Topic + cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName}) + sess := e.Table("topic").Where(cond) + sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + has, err := sess.Get(&topic) + if has { + return &topic, err + } + return nil, err +} + +// AddTopic adds a topic name to a repository (if it does not already have it) +func AddTopic(repoID int64, topicName string) (*Topic, error) { + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + topic, err := getRepoTopicByName(sess, repoID, topicName) + if err != nil { + return nil, err + } + if topic != nil { + // Repo already have topic + return topic, nil + } + + topic, err = addTopicByNameToRepo(sess, repoID, topicName) + if err != nil { + return nil, err + } + + topicNames := make([]string, 0, 25) + if err := sess.Select("name").Table("topic"). + Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id"). + Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil { + return nil, err + } + + if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ + Topics: topicNames, + }); err != nil { + return nil, err + } + + return topic, committer.Commit() +} + +// DeleteTopic removes a topic name from a repository (if it has it) +func DeleteTopic(repoID int64, topicName string) (*Topic, error) { + topic, err := GetRepoTopicByName(repoID, topicName) + if err != nil { + return nil, err + } + if topic == nil { + // Repo doesn't have topic, can't be removed + return nil, nil + } + + err = removeTopicFromRepo(db.GetEngine(db.DefaultContext), repoID, topic) + + return topic, err +} + +// SaveTopics save topics to a repository +func SaveTopics(repoID int64, topicNames ...string) error { + topics, _, err := FindTopics(&FindTopicOptions{ + RepoID: repoID, + }) + if err != nil { + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + var addedTopicNames []string + for _, topicName := range topicNames { + if strings.TrimSpace(topicName) == "" { + continue + } + + var found bool + for _, t := range topics { + if strings.EqualFold(topicName, t.Name) { + found = true + break + } + } + if !found { + addedTopicNames = append(addedTopicNames, topicName) + } + } + + var removeTopics []*Topic + for _, t := range topics { + var found bool + for _, topicName := range topicNames { + if strings.EqualFold(topicName, t.Name) { + found = true + break + } + } + if !found { + removeTopics = append(removeTopics, t) + } + } + + for _, topicName := range addedTopicNames { + _, err := addTopicByNameToRepo(sess, repoID, topicName) + if err != nil { + return err + } + } + + for _, topic := range removeTopics { + err := removeTopicFromRepo(sess, repoID, topic) + if err != nil { + return err + } + } + + topicNames = make([]string, 0, 25) + if err := sess.Table("topic").Cols("name"). + Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id"). + Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil { + return err + } + + if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ + Topics: topicNames, + }); err != nil { + return err + } + + return committer.Commit() +} + +// GenerateTopics generates topics from a template repository +func GenerateTopics(ctx context.Context, templateRepo, generateRepo *Repository) error { + for _, topic := range templateRepo.Topics { + if _, err := addTopicByNameToRepo(db.GetEngine(ctx), generateRepo.ID, topic); err != nil { + return err + } + } + return nil +} diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go new file mode 100644 index 0000000000..353d96ef3e --- /dev/null +++ b/models/repo/topic_test.go @@ -0,0 +1,80 @@ +// Copyright 2018 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 repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestAddTopic(t *testing.T) { + totalNrOfTopics := 6 + repo1NrOfTopics := 3 + + assert.NoError(t, unittest.PrepareTestDatabase()) + + topics, _, err := FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.Len(t, topics, totalNrOfTopics) + + topics, total, err := FindTopics(&FindTopicOptions{ + ListOptions: db.ListOptions{Page: 1, PageSize: 2}, + }) + assert.NoError(t, err) + assert.Len(t, topics, 2) + assert.EqualValues(t, 6, total) + + topics, _, err = FindTopics(&FindTopicOptions{ + RepoID: 1, + }) + assert.NoError(t, err) + assert.Len(t, topics, repo1NrOfTopics) + + assert.NoError(t, SaveTopics(2, "golang")) + repo2NrOfTopics := 1 + topics, _, err = FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.Len(t, topics, totalNrOfTopics) + + topics, _, err = FindTopics(&FindTopicOptions{ + RepoID: 2, + }) + assert.NoError(t, err) + assert.Len(t, topics, repo2NrOfTopics) + + assert.NoError(t, SaveTopics(2, "golang", "gitea")) + repo2NrOfTopics = 2 + totalNrOfTopics++ + topic, err := GetTopicByName("gitea") + assert.NoError(t, err) + assert.EqualValues(t, 1, topic.RepoCount) + + topics, _, err = FindTopics(&FindTopicOptions{}) + assert.NoError(t, err) + assert.Len(t, topics, totalNrOfTopics) + + topics, _, err = FindTopics(&FindTopicOptions{ + RepoID: 2, + }) + assert.NoError(t, err) + assert.Len(t, topics, repo2NrOfTopics) +} + +func TestTopicValidator(t *testing.T) { + assert.True(t, ValidateTopic("12345")) + assert.True(t, ValidateTopic("2-test")) + assert.True(t, ValidateTopic("test-3")) + assert.True(t, ValidateTopic("first")) + assert.True(t, ValidateTopic("second-test-topic")) + assert.True(t, ValidateTopic("third-project-topic-with-max-length")) + + assert.False(t, ValidateTopic("$fourth-test,topic")) + assert.False(t, ValidateTopic("-fifth-test-topic")) + assert.False(t, ValidateTopic("sixth-go-project-topic-with-excess-length")) +} diff --git a/models/repo/update.go b/models/repo/update.go new file mode 100644 index 0000000000..efc562a405 --- /dev/null +++ b/models/repo/update.go @@ -0,0 +1,179 @@ +// Copyright 2021 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 repo + +import ( + "context" + "fmt" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" +) + +// UpdateRepositoryOwnerNames updates repository owner_names (this should only be used when the ownerName has changed case) +func UpdateRepositoryOwnerNames(ownerID int64, ownerName string) error { + if ownerID == 0 { + return nil + } + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{ + OwnerName: ownerName, + }); err != nil { + return err + } + + return committer.Commit() +} + +// UpdateRepositoryUpdatedTime updates a repository's updated time +func UpdateRepositoryUpdatedTime(repoID int64, updateTime time.Time) error { + _, err := db.GetEngine(db.DefaultContext).Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", updateTime.Unix(), repoID) + return err +} + +// UpdateRepositoryColsCtx updates repository's columns +func UpdateRepositoryColsCtx(ctx context.Context, repo *Repository, cols ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo) + return err +} + +// UpdateRepositoryCols updates repository's columns +func UpdateRepositoryCols(repo *Repository, cols ...string) error { + return UpdateRepositoryColsCtx(db.DefaultContext, repo, cols...) +} + +// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error. +type ErrReachLimitOfRepo struct { + Limit int +} + +// IsErrReachLimitOfRepo checks if an error is a ErrReachLimitOfRepo. +func IsErrReachLimitOfRepo(err error) bool { + _, ok := err.(ErrReachLimitOfRepo) + return ok +} + +func (err ErrReachLimitOfRepo) Error() string { + return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit) +} + +// ErrRepoAlreadyExist represents a "RepoAlreadyExist" kind of error. +type ErrRepoAlreadyExist struct { + Uname string + Name string +} + +// IsErrRepoAlreadyExist checks if an error is a ErrRepoAlreadyExist. +func IsErrRepoAlreadyExist(err error) bool { + _, ok := err.(ErrRepoAlreadyExist) + return ok +} + +func (err ErrRepoAlreadyExist) Error() string { + return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) +} + +// ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error. +type ErrRepoFilesAlreadyExist struct { + Uname string + Name string +} + +// IsErrRepoFilesAlreadyExist checks if an error is a ErrRepoAlreadyExist. +func IsErrRepoFilesAlreadyExist(err error) bool { + _, ok := err.(ErrRepoFilesAlreadyExist) + return ok +} + +func (err ErrRepoFilesAlreadyExist) Error() string { + return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name) +} + +// CheckCreateRepository check if could created a repository +func CheckCreateRepository(doer, u *user_model.User, name string, overwriteOrAdopt bool) error { + if !doer.CanCreateRepo() { + return ErrReachLimitOfRepo{u.MaxRepoCreation} + } + + if err := IsUsableRepoName(name); err != nil { + return err + } + + has, err := IsRepositoryExist(u, name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %v", err) + } else if has { + return ErrRepoAlreadyExist{u.Name, name} + } + + repoPath := RepoPath(u.Name, name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if !overwriteOrAdopt && isExist { + return ErrRepoFilesAlreadyExist{u.Name, name} + } + return nil +} + +// ChangeRepositoryName changes all corresponding setting from old repository name to new one. +func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName string) (err error) { + oldRepoName := repo.Name + newRepoName = strings.ToLower(newRepoName) + if err = IsUsableRepoName(newRepoName); err != nil { + return err + } + + if err := repo.GetOwner(db.DefaultContext); err != nil { + return err + } + + has, err := IsRepositoryExist(repo.Owner, newRepoName) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %v", err) + } else if has { + return ErrRepoAlreadyExist{repo.Owner.Name, newRepoName} + } + + newRepoPath := RepoPath(repo.Owner.Name, newRepoName) + if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil { + return fmt.Errorf("rename repository directory: %v", err) + } + + wikiPath := repo.WikiPath() + isExist, err := util.IsExist(wikiPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) + return err + } + if isExist { + if err = util.Rename(wikiPath, WikiPath(repo.Owner.Name, newRepoName)); err != nil { + return fmt.Errorf("rename repository wiki: %v", err) + } + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err := NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/repo/watch.go b/models/repo/watch.go new file mode 100644 index 0000000000..8e54f0970d --- /dev/null +++ b/models/repo/watch.go @@ -0,0 +1,196 @@ +// Copyright 2017 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 repo + +import ( + "context" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" +) + +// WatchMode specifies what kind of watch the user has on a repository +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 +) + +// 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"` +} + +func init() { + db.RegisterModel(new(Watch)) +} + +// 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} + has, err := db.GetEngine(ctx).Get(&watch) + if err != nil { + return watch, err + } + if !has { + watch.Mode = WatchModeNone + } + return watch, nil +} + +// IsWatchMode Decodes watchability of WatchMode +func IsWatchMode(mode WatchMode) bool { + return mode != WatchModeNone && mode != WatchModeDont +} + +// IsWatching checks if user has watched given repository. +func IsWatching(userID, repoID int64) bool { + watch, err := GetWatch(db.DefaultContext, userID, repoID) + return err == nil && IsWatchMode(watch.Mode) +} + +func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) { + if watch.Mode == mode { + return nil + } + if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) { + // Don't auto watch if already watching or deliberately not watching + return nil + } + + hadrec := watch.Mode != WatchModeNone + needsrec := mode != WatchModeNone + repodiff := 0 + + if IsWatchMode(mode) && !IsWatchMode(watch.Mode) { + repodiff = 1 + } else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) { + repodiff = -1 + } + + watch.Mode = mode + + e := db.GetEngine(ctx) + + if !hadrec && needsrec { + watch.Mode = mode + if _, err = e.Insert(watch); err != nil { + return err + } + } else if needsrec { + watch.Mode = mode + if _, err := e.ID(watch.ID).AllCols().Update(watch); err != nil { + return err + } + } else if _, err = e.Delete(Watch{ID: watch.ID}); err != nil { + return err + } + if repodiff != 0 { + _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID) + } + return err +} + +// WatchRepoMode watch repository in specific mode. +func WatchRepoMode(userID, repoID int64, mode WatchMode) (err error) { + var watch Watch + if watch, err = GetWatch(db.DefaultContext, userID, repoID); err != nil { + return err + } + return watchRepoMode(db.DefaultContext, watch, mode) +} + +// WatchRepoCtx watch or unwatch repository. +func WatchRepoCtx(ctx context.Context, userID, repoID int64, doWatch bool) (err error) { + var watch Watch + if watch, err = GetWatch(ctx, userID, repoID); err != nil { + return err + } + if !doWatch && watch.Mode == WatchModeAuto { + err = watchRepoMode(ctx, watch, WatchModeDont) + } else if !doWatch { + err = watchRepoMode(ctx, watch, WatchModeNone) + } else { + err = watchRepoMode(ctx, watch, WatchModeNormal) + } + return err +} + +// WatchRepo watch or unwatch repository. +func WatchRepo(userID, repoID int64, watch bool) (err error) { + return WatchRepoCtx(db.DefaultContext, userID, repoID, watch) +} + +// GetWatchers returns all watchers of given repository. +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("`user`.is_active=?", true). + And("`user`.prohibit_login=?", false). + Join("INNER", "`user`", "`user`.id = `watch`.user_id"). + Find(&watches) +} + +// GetRepoWatchersIDs returns IDs of watchers for a given repo ID +// but avoids joining with `user` for performance reasons +// User permissions must be verified elsewhere if required +func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) { + ids := make([]int64, 0, 64) + return ids, db.GetEngine(ctx).Table("watch"). + Where("watch.repo_id=?", repoID). + And("watch.mode<>?", WatchModeDont). + Select("user_id"). + Find(&ids) +} + +// GetRepoWatchers returns range of users watching given repository. +func GetRepoWatchers(repoID int64, 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("`watch`.mode<>?", WatchModeDont) + 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) +} + +func watchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error { + if !isWrite || !setting.Service.AutoWatchOnChanges { + return nil + } + watch, err := GetWatch(ctx, userID, repoID) + if err != nil { + return err + } + if watch.Mode != WatchModeNone { + return nil + } + return watchRepoMode(ctx, watch, WatchModeAuto) +} + +// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set +func WatchIfAuto(userID, repoID int64, isWrite bool) error { + return watchIfAuto(db.DefaultContext, userID, repoID, isWrite) +} diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go new file mode 100644 index 0000000000..2ff3ced2dc --- /dev/null +++ b/models/repo/watch_test.go @@ -0,0 +1,139 @@ +// Copyright 2017 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 repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestIsWatching(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + assert.True(t, IsWatching(1, 1)) + assert.True(t, IsWatching(4, 1)) + assert.True(t, IsWatching(11, 1)) + + assert.False(t, IsWatching(1, 5)) + assert.False(t, IsWatching(8, 1)) + assert.False(t, IsWatching(unittest.NonexistentID, unittest.NonexistentID)) +} + +func TestGetWatchers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + watches, err := GetWatchers(db.DefaultContext, repo.ID) + assert.NoError(t, err) + // One watchers are inactive, thus minus 1 + assert.Len(t, watches, repo.NumWatches-1) + for _, watch := range watches { + assert.EqualValues(t, repo.ID, watch.RepoID) + } + + watches, err = GetWatchers(db.DefaultContext, unittest.NonexistentID) + assert.NoError(t, err) + assert.Len(t, watches, 0) +} + +func TestRepository_GetWatchers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + watchers, err := GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, repo.NumWatches) + for _, watcher := range watchers { + unittest.AssertExistsAndLoadBean(t, &Watch{UserID: watcher.ID, RepoID: repo.ID}) + } + + repo = unittest.AssertExistsAndLoadBean(t, &Repository{ID: 9}).(*Repository) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, 0) +} + +func TestWatchIfAuto(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + watchers, err := GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, repo.NumWatches) + + setting.Service.AutoWatchOnChanges = false + + prevCount := repo.NumWatches + + // Must not add watch + assert.NoError(t, WatchIfAuto(8, 1, true)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should not add watch + assert.NoError(t, WatchIfAuto(10, 1, true)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + setting.Service.AutoWatchOnChanges = true + + // Must not add watch + assert.NoError(t, WatchIfAuto(8, 1, true)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should not add watch + assert.NoError(t, WatchIfAuto(12, 1, false)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should add watch + assert.NoError(t, WatchIfAuto(12, 1, true)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount+1) + + // Should remove watch, inhibit from adding auto + assert.NoError(t, WatchRepo(12, 1, false)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Must not add watch + assert.NoError(t, WatchIfAuto(12, 1, true)) + watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) +} + +func TestWatchRepoMode(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + + assert.NoError(t, WatchRepoMode(12, 1, WatchModeAuto)) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeAuto}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, WatchModeNormal)) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeNormal}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, WatchModeDont)) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeDont}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, WatchModeNone)) + unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) +} |