diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2022-06-06 16:01:49 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-06 16:01:49 +0800 |
commit | 26095115f4ae90e3fdc6ab695978efd16e317f75 (patch) | |
tree | 92ec1c7ff54e0a65f4f0662baa8c0244dd9f324b /models/repo | |
parent | ebeb6e7c71a0c763b52153f4eb427e7c5b89a95e (diff) | |
download | gitea-26095115f4ae90e3fdc6ab695978efd16e317f75.tar.gz gitea-26095115f4ae90e3fdc6ab695978efd16e317f75.zip |
Move some repository related code into sub package (#19711)
* Move some repository related code into sub package
* Move more repository functions out of models
* Fix lint
* Some performance optimization for webhooks and others
* some refactors
* Fix lint
* Fix
* Update modules/repository/delete.go
Co-authored-by: delvh <dev.lh@web.de>
* Fix test
* Merge
* Fix test
* Fix test
* Fix test
* Fix test
Co-authored-by: delvh <dev.lh@web.de>
Diffstat (limited to 'models/repo')
-rw-r--r-- | models/repo/attachment_test.go | 33 | ||||
-rw-r--r-- | models/repo/collaboration_test.go | 13 | ||||
-rw-r--r-- | models/repo/fork.go | 50 | ||||
-rw-r--r-- | models/repo/fork_test.go | 11 | ||||
-rw-r--r-- | models/repo/main_test.go | 21 | ||||
-rw-r--r-- | models/repo/mirror.go | 13 | ||||
-rw-r--r-- | models/repo/pushmirror_test.go | 13 | ||||
-rw-r--r-- | models/repo/redirect_test.go | 31 | ||||
-rw-r--r-- | models/repo/repo_list.go | 661 | ||||
-rw-r--r-- | models/repo/repo_list_test.go | 385 | ||||
-rw-r--r-- | models/repo/repo_test.go | 21 | ||||
-rw-r--r-- | models/repo/star_test.go | 29 | ||||
-rw-r--r-- | models/repo/topic_test.go | 43 | ||||
-rw-r--r-- | models/repo/update.go | 8 | ||||
-rw-r--r-- | models/repo/user_repo.go | 122 | ||||
-rw-r--r-- | models/repo/user_repo_test.go | 74 | ||||
-rw-r--r-- | models/repo/watch_test.go | 87 | ||||
-rw-r--r-- | models/repo/wiki_test.go | 13 |
18 files changed, 1460 insertions, 168 deletions
diff --git a/models/repo/attachment_test.go b/models/repo/attachment_test.go index da486fdb2b..d7c2f529db 100644 --- a/models/repo/attachment_test.go +++ b/models/repo/attachment_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,7 +17,7 @@ import ( func TestIncreaseDownloadCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attachment, err := GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") assert.NoError(t, err) assert.Equal(t, int64(0), attachment.DownloadCount) @@ -24,7 +25,7 @@ func TestIncreaseDownloadCount(t *testing.T) { err = attachment.IncreaseDownloadCount() assert.NoError(t, err) - attachment, err = GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + attachment, err = repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") assert.NoError(t, err) assert.Equal(t, int64(1), attachment.DownloadCount) } @@ -33,11 +34,11 @@ func TestGetByCommentOrIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // count of attachments from issue ID - attachments, err := GetAttachmentsByIssueID(db.DefaultContext, 1) + attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 1) assert.NoError(t, err) assert.Len(t, attachments, 1) - attachments, err = GetAttachmentsByCommentID(db.DefaultContext, 1) + attachments, err = repo_model.GetAttachmentsByCommentID(db.DefaultContext, 1) assert.NoError(t, err) assert.Len(t, attachments, 2) } @@ -45,33 +46,33 @@ func TestGetByCommentOrIssueID(t *testing.T) { func TestDeleteAttachments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := DeleteAttachmentsByIssue(4, false) + count, err := repo_model.DeleteAttachmentsByIssue(4, false) assert.NoError(t, err) assert.Equal(t, 2, count) - count, err = DeleteAttachmentsByComment(2, false) + count, err = repo_model.DeleteAttachmentsByComment(2, false) assert.NoError(t, err) assert.Equal(t, 2, count) - err = DeleteAttachment(&Attachment{ID: 8}, false) + err = repo_model.DeleteAttachment(&repo_model.Attachment{ID: 8}, false) assert.NoError(t, err) - attachment, err := GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18") + attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18") assert.Error(t, err) - assert.True(t, IsErrAttachmentNotExist(err)) + assert.True(t, repo_model.IsErrAttachmentNotExist(err)) assert.Nil(t, attachment) } func TestGetAttachmentByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attach, err := GetAttachmentByID(db.DefaultContext, 1) + attach, err := repo_model.GetAttachmentByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID) } func TestAttachment_DownloadURL(t *testing.T) { - attach := &Attachment{ + attach := &repo_model.Attachment{ UUID: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", ID: 1, } @@ -81,20 +82,20 @@ func TestAttachment_DownloadURL(t *testing.T) { func TestUpdateAttachment(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attach, err := GetAttachmentByID(db.DefaultContext, 1) + attach, err := repo_model.GetAttachmentByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID) attach.Name = "new_name" - assert.NoError(t, UpdateAttachment(db.DefaultContext, attach)) + assert.NoError(t, repo_model.UpdateAttachment(db.DefaultContext, attach)) - unittest.AssertExistsAndLoadBean(t, &Attachment{Name: "new_name"}) + unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{Name: "new_name"}) } func TestGetAttachmentsByUUIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attachList, err := GetAttachmentsByUUIDs(db.DefaultContext, []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"}) + attachList, err := repo_model.GetAttachmentsByUUIDs(db.DefaultContext, []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"}) assert.NoError(t, err) assert.Len(t, attachList, 2) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attachList[0].UUID) diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go index a7d04498e9..8cb7980a75 100644 --- a/models/repo/collaboration_test.go +++ b/models/repo/collaboration_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,10 +17,10 @@ import ( func TestRepository_GetCollaborators(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) - collaborators, err := GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) + collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) assert.NoError(t, err) - expectedLen, err := db.GetEngine(db.DefaultContext).Count(&Collaboration{RepoID: repoID}) + expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID}) assert.NoError(t, err) assert.Len(t, collaborators, int(expectedLen)) for _, collaborator := range collaborators { @@ -37,8 +38,8 @@ func TestRepository_IsCollaborator(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID, userID int64, expected bool) { - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) - actual, err := IsCollaborator(db.DefaultContext, repo.ID, userID) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) + actual, err := repo_model.IsCollaborator(db.DefaultContext, repo.ID, userID) assert.NoError(t, err) assert.Equal(t, expected, actual) } diff --git a/models/repo/fork.go b/models/repo/fork.go index b48126253c..938bbae17e 100644 --- a/models/repo/fork.go +++ b/models/repo/fork.go @@ -8,6 +8,8 @@ import ( "context" "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "xorm.io/builder" ) // GetRepositoriesByForkID returns all repositories with given fork ID. @@ -63,3 +65,51 @@ func GetForks(repo *Repository, listOptions db.ListOptions) ([]*Repository, erro forks := make([]*Repository, 0, listOptions.PageSize) return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) } + +// IncrementRepoForkNum increment repository fork number +func IncrementRepoForkNum(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID) + return err +} + +// DecrementRepoForkNum decrement repository fork number +func DecrementRepoForkNum(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repoID) + return err +} + +// FindUserOrgForks returns the forked repositories for one user from a repository +func FindUserOrgForks(ctx context.Context, repoID, userID int64) ([]*Repository, error) { + cond := builder.And( + builder.Eq{"fork_id": repoID}, + builder.In("owner_id", + builder.Select("org_id"). + From("org_user"). + Where(builder.Eq{"uid": userID}), + ), + ) + + var repos []*Repository + return repos, db.GetEngine(ctx).Table("repository").Where(cond).Find(&repos) +} + +// GetForksByUserAndOrgs return forked repos of the user and owned orgs +func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *Repository) ([]*Repository, error) { + var repoList []*Repository + if user == nil { + return repoList, nil + } + forkedRepo, err := GetUserFork(ctx, repo.ID, user.ID) + if err != nil { + return repoList, err + } + if forkedRepo != nil { + repoList = append(repoList, forkedRepo) + } + orgForks, err := FindUserOrgForks(ctx, repo.ID, user.ID) + if err != nil { + return nil, err + } + repoList = append(repoList, orgForks...) + return repoList, nil +} diff --git a/models/repo/fork_test.go b/models/repo/fork_test.go index 263aec4e3a..9e08d8136e 100644 --- a/models/repo/fork_test.go +++ b/models/repo/fork_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -17,17 +18,17 @@ func TestGetUserFork(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // User13 has repo 11 forked from repo10 - repo, err := GetRepositoryByID(10) + repo, err := repo_model.GetRepositoryByID(10) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetUserFork(db.DefaultContext, repo.ID, 13) + repo, err = repo_model.GetUserFork(db.DefaultContext, repo.ID, 13) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetRepositoryByID(9) + repo, err = repo_model.GetRepositoryByID(9) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetUserFork(db.DefaultContext, repo.ID, 13) + repo, err = repo_model.GetUserFork(db.DefaultContext, 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 375d0e0df1..eb04aa8227 100644 --- a/models/repo/main_test.go +++ b/models/repo/main_test.go @@ -2,31 +2,22 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "path/filepath" "testing" + _ "code.gitea.io/gitea/models" // register table model + _ "code.gitea.io/gitea/models/perm/access" // register table model + _ "code.gitea.io/gitea/models/repo" // register table model + _ "code.gitea.io/gitea/models/user" // register table model + "code.gitea.io/gitea/models/unittest" ) func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), - FixtureFiles: []string{ - "attachment.yml", - "repo_archiver.yml", - "repository.yml", - "repo_unit.yml", - "repo_indexer_status.yml", - "repo_redirect.yml", - "watch.yml", - "star.yml", - "topic.yml", - "repo_topic.yml", - "user.yml", - "collaboration.yml", - }, }) } diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 5d20b7f833..bd83d24424 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -123,8 +123,8 @@ func MirrorsIterate(limit int, f func(idx int, bean interface{}) error) error { } // InsertMirror inserts a mirror to database -func InsertMirror(mirror *Mirror) error { - _, err := db.GetEngine(db.DefaultContext).Insert(mirror) +func InsertMirror(ctx context.Context, mirror *Mirror) error { + _, err := db.GetEngine(ctx).Insert(mirror) return err } @@ -168,3 +168,12 @@ func (repos MirrorRepositoryList) loadAttributes(ctx context.Context) error { func (repos MirrorRepositoryList) LoadAttributes() error { return repos.loadAttributes(db.DefaultContext) } + +// GetUserMirrorRepositories returns a list of mirror repositories of given user. +func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { + repos := make([]*Repository, 0, 10) + return repos, db.GetEngine(db.DefaultContext). + Where("owner_id = ?", userID). + And("is_mirror = ?", true). + Find(&repos) +} diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go index 83cf86131f..d36a48547e 100644 --- a/models/repo/pushmirror_test.go +++ b/models/repo/pushmirror_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "time" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/timeutil" @@ -19,20 +20,20 @@ func TestPushMirrorsIterate(t *testing.T) { now := timeutil.TimeStampNow() - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-1", LastUpdateUnix: now, Interval: 1, }) long, _ := time.ParseDuration("24h") - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-2", LastUpdateUnix: now, Interval: long, }) - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-3", LastUpdateUnix: now, Interval: 0, @@ -40,8 +41,8 @@ func TestPushMirrorsIterate(t *testing.T) { time.Sleep(1 * time.Millisecond) - PushMirrorsIterate(1, func(idx int, bean interface{}) error { - m, ok := bean.(*PushMirror) + repo_model.PushMirrorsIterate(1, func(idx int, bean interface{}) error { + m, ok := bean.(*repo_model.PushMirror) assert.True(t, ok) assert.Equal(t, "test-1", m.RemoteName) assert.Equal(t, m.RemoteName, m.GetRemoteName()) diff --git a/models/repo/redirect_test.go b/models/repo/redirect_test.go index 2dca2cbbfd..05b105cf63 100644 --- a/models/repo/redirect_test.go +++ b/models/repo/redirect_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,27 +17,27 @@ import ( func TestLookupRedirect(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repoID, err := LookupRedirect(2, "oldrepo1") + repoID, err := repo_model.LookupRedirect(2, "oldrepo1") assert.NoError(t, err) assert.EqualValues(t, 1, repoID) - _, err = LookupRedirect(unittest.NonexistentID, "doesnotexist") - assert.True(t, IsErrRedirectNotExist(err)) + _, err = repo_model.LookupRedirect(unittest.NonexistentID, "doesnotexist") + assert.True(t, repo_model.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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, }) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: "oldrepo1", RedirectRepoID: repo.ID, @@ -47,15 +48,15 @@ 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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "oldrepo1")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, }) - unittest.AssertNotExistsBean(t, &Redirect{ + unittest.AssertNotExistsBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: "oldrepo1", RedirectRepoID: repo.ID, @@ -66,10 +67,10 @@ 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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 23cdd6cad6..1bec35d571 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -5,18 +5,21 @@ package repo import ( + "context" + "fmt" + "strings" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/setting" -) + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" -// GetUserMirrorRepositories returns a list of mirror repositories of given user. -func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { - repos := make([]*Repository, 0, 10) - return repos, db.GetEngine(db.DefaultContext). - Where("owner_id = ?", userID). - And("is_mirror = ?", true). - Find(&repos) -} + "xorm.io/builder" +) // IterateRepository iterate repositories func IterateRepository(f func(repo *Repository) error) error { @@ -45,3 +48,643 @@ func IterateRepository(f func(repo *Repository) error) error { func FindReposMapByIDs(repoIDs []int64, res map[int64]*Repository) error { return db.GetEngine(db.DefaultContext).In("id", repoIDs).Find(&res) } + +// RepositoryListDefaultPageSize is the default number of repositories +// to load in memory when running administrative tasks on all (or almost +// all) of them. +// The number should be low enough to avoid filling up all RAM with +// repository data... +const RepositoryListDefaultPageSize = 64 + +// RepositoryList contains a list of repositories +type RepositoryList []*Repository + +func (repos RepositoryList) Len() int { + return len(repos) +} + +func (repos RepositoryList) Less(i, j int) bool { + return repos[i].FullName() < repos[j].FullName() +} + +func (repos RepositoryList) Swap(i, j int) { + repos[i], repos[j] = repos[j], repos[i] +} + +// ValuesRepository converts a repository map to a list +// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18 +func ValuesRepository(m map[int64]*Repository) []*Repository { + values := make([]*Repository, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values +} + +// RepositoryListOfMap make list from values of map +func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList { + return RepositoryList(ValuesRepository(repoMap)) +} + +func (repos RepositoryList) loadAttributes(ctx context.Context) error { + if len(repos) == 0 { + return nil + } + + set := make(map[int64]struct{}) + repoIDs := make([]int64, len(repos)) + for i := range repos { + set[repos[i].OwnerID] = struct{}{} + repoIDs[i] = repos[i].ID + } + + // Load owners. + users := make(map[int64]*user_model.User, len(set)) + if err := db.GetEngine(ctx). + Where("id > 0"). + In("id", container.KeysInt64(set)). + Find(&users); err != nil { + return fmt.Errorf("find users: %v", err) + } + for i := range repos { + repos[i].Owner = users[repos[i].OwnerID] + } + + // Load primary language. + stats := make(LanguageStatList, 0, len(repos)) + if err := db.GetEngine(ctx). + Where("`is_primary` = ? AND `language` != ?", true, "other"). + In("`repo_id`", repoIDs). + Find(&stats); err != nil { + return fmt.Errorf("find primary languages: %v", err) + } + stats.LoadAttributes() + for i := range repos { + for _, st := range stats { + if st.RepoID == repos[i].ID { + repos[i].PrimaryLanguage = st + break + } + } + } + + return nil +} + +// LoadAttributes loads the attributes for the given RepositoryList +func (repos RepositoryList) LoadAttributes() error { + return repos.loadAttributes(db.DefaultContext) +} + +// SearchRepoOptions holds the search options +type SearchRepoOptions struct { + db.ListOptions + Actor *user_model.User + Keyword string + OwnerID int64 + PriorityOwnerID int64 + TeamID int64 + OrderBy db.SearchOrderBy + Private bool // Include private repositories in results + StarredByID int64 + WatchedByID int64 + AllPublic bool // Include also all public repositories of users and public organisations + AllLimited bool // Include also all public repositories of limited organisations + // None -> include public and private + // True -> include just private + // False -> include just public + IsPrivate util.OptionalBool + // None -> include collaborative AND non-collaborative + // True -> include just collaborative + // False -> include just non-collaborative + Collaborate util.OptionalBool + // None -> include forks AND non-forks + // True -> include just forks + // False -> include just non-forks + Fork util.OptionalBool + // None -> include templates AND non-templates + // True -> include just templates + // False -> include just non-templates + Template util.OptionalBool + // None -> include mirrors AND non-mirrors + // True -> include just mirrors + // False -> include just non-mirrors + Mirror util.OptionalBool + // None -> include archived AND non-archived + // True -> include just archived + // False -> include just non-archived + Archived util.OptionalBool + // only search topic name + TopicOnly bool + // only search repositories with specified primary language + Language string + // include description in keyword search + IncludeDescription bool + // None -> include has milestones AND has no milestone + // True -> include just has milestones + // False -> include just has no milestone + HasMilestones util.OptionalBool + // LowerNames represents valid lower names to restrict to + LowerNames []string +} + +// SearchOrderBy is used to sort the result +type SearchOrderBy string + +func (s SearchOrderBy) String() string { + return string(s) +} + +// Strings for sorting result +const ( + SearchOrderByAlphabetically SearchOrderBy = "name ASC" + SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC" + SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC" + SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" + SearchOrderByOldest SearchOrderBy = "created_unix ASC" + SearchOrderByNewest SearchOrderBy = "created_unix DESC" + SearchOrderBySize SearchOrderBy = "size ASC" + SearchOrderBySizeReverse SearchOrderBy = "size DESC" + SearchOrderByID SearchOrderBy = "id ASC" + SearchOrderByIDReverse SearchOrderBy = "id DESC" + SearchOrderByStars SearchOrderBy = "num_stars ASC" + SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" + SearchOrderByForks SearchOrderBy = "num_forks ASC" + SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" +) + +// UserOwnedRepoCond returns user ownered repositories +func UserOwnedRepoCond(userID int64) builder.Cond { + return builder.Eq{ + "repository.owner_id": userID, + } +} + +// UserAssignedRepoCond return user as assignee repositories list +func UserAssignedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_assignees"). + InnerJoin("issue", "issue.id = issue_assignees.issue_id"). + Where(builder.Eq{ + "issue_assignees.assignee_id": userID, + }), + ), + ) +} + +// UserCreateIssueRepoCond return user created issues repositories list +func UserCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue"). + Where(builder.Eq{ + "issue.poster_id": userID, + "issue.is_pull": isPull, + }), + ), + ) +} + +// UserMentionedRepoCond return user metinoed repositories list +func UserMentionedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_user"). + InnerJoin("issue", "issue.id = issue_user.issue_id"). + Where(builder.Eq{ + "issue_user.is_mentioned": true, + "issue_user.uid": userID, + }), + ), + ) +} + +// UserCollaborationRepoCond returns user as collabrators repositories list +func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, builder.Select("repo_id"). + From("`access`"). + Where(builder.And( + builder.Eq{"`access`.user_id": userID}, + builder.Gt{"`access`.mode": int(perm.AccessModeNone)}, + )), + ) +} + +// userOrgTeamRepoCond selects repos that the given user has access to through team membership +func userOrgTeamRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, userOrgTeamRepoBuilder(userID)) +} + +// userOrgTeamRepoBuilder returns repo ids where user's teams can access. +func userOrgTeamRepoBuilder(userID int64) *builder.Builder { + return builder.Select("`team_repo`.repo_id"). + From("team_repo"). + Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_user`.uid": userID}) +} + +// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. +func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { + return userOrgTeamRepoBuilder(userID). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_unit`.`type`": unitType}) +} + +// UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit +func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { + return builder.In(idStr, + userOrgTeamUnitRepoBuilder(userID, unitType). + And(builder.Eq{"`team_unit`.org_id": orgID}), + ) +} + +// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations +func userOrgPublicRepoCond(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Where(builder.Eq{"`org_user`.uid": userID}), + ), + ) +} + +// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations +func userOrgPublicRepoCondPrivate(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). + Where(builder.Eq{ + "`org_user`.uid": userID, + "`user`.`type`": user_model.UserTypeOrganization, + "`user`.visibility": structs.VisibleTypePrivate, + }), + ), + ) +} + +// UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization +func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { + return userOrgPublicRepoCond(userID). + And(builder.Eq{"`repository`.owner_id": orgID}) +} + +// SearchRepositoryCondition creates a query condition according search repository options +func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { + cond := builder.NewCond() + + if opts.Private { + if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID { + // OK we're in the context of a User + cond = cond.And(AccessibleRepositoryCondition(opts.Actor)) + } + } else { + // Not looking at private organisations and users + // We should be able to see all non-private repositories that + // isn't in a private or limited organisation. + cond = cond.And( + builder.Eq{"is_private": false}, + builder.NotIn("owner_id", builder.Select("id").From("`user`").Where( + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + ))) + } + + if opts.IsPrivate != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) + } + + if opts.Template != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) + } + + // Restrict to starred repositories + if opts.StarredByID > 0 { + cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID}))) + } + + // Restrict to watched repositories + if opts.WatchedByID > 0 { + cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID}))) + } + + // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate + if opts.OwnerID > 0 { + accessCond := builder.NewCond() + if opts.Collaborate != util.OptionalBoolTrue { + accessCond = builder.Eq{"owner_id": opts.OwnerID} + } + + if opts.Collaborate != util.OptionalBoolFalse { + // A Collaboration is: + collaborateCond := builder.And( + // 1. Repository we don't own + builder.Neq{"owner_id": opts.OwnerID}, + // 2. But we can see because of: + builder.Or( + // A. We have access + UserCollaborationRepoCond("`repository`.id", opts.OwnerID), + // B. We are in a team for + userOrgTeamRepoCond("`repository`.id", opts.OwnerID), + // C. Public repositories in organizations that we are member of + userOrgPublicRepoCondPrivate(opts.OwnerID), + ), + ) + if !opts.Private { + collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) + } + + accessCond = accessCond.Or(collaborateCond) + } + + if opts.AllPublic { + accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})))) + } + + if opts.AllLimited { + accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})))) + } + + cond = cond.And(accessCond) + } + + if opts.TeamID > 0 { + cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID}))) + } + + if opts.Keyword != "" { + // separate keyword + subQueryCond := builder.NewCond() + for _, v := range strings.Split(opts.Keyword, ",") { + if opts.TopicOnly { + subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) + } else { + subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)}) + } + } + subQuery := builder.Select("repo_topic.repo_id").From("repo_topic"). + Join("INNER", "topic", "topic.id = repo_topic.topic_id"). + Where(subQueryCond). + GroupBy("repo_topic.repo_id") + + keywordCond := builder.In("id", subQuery) + if !opts.TopicOnly { + likes := builder.NewCond() + for _, v := range strings.Split(opts.Keyword, ",") { + likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) + + // If the string looks like "org/repo", match against that pattern too + if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 { + pieces := strings.Split(opts.Keyword, "/") + ownerName := pieces[0] + repoName := pieces[1] + likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)})) + } + + if opts.IncludeDescription { + likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)}) + } + } + keywordCond = keywordCond.Or(likes) + } + cond = cond.And(keywordCond) + } + + if opts.Language != "" { + cond = cond.And(builder.In("id", builder. + Select("repo_id"). + From("language_stat"). + Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) + } + + if opts.Fork != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue}) + } + + if opts.Mirror != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) + } + + if opts.Actor != nil && opts.Actor.IsRestricted { + cond = cond.And(AccessibleRepositoryCondition(opts.Actor)) + } + + if opts.Archived != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) + } + + switch opts.HasMilestones { + case util.OptionalBoolTrue: + cond = cond.And(builder.Gt{"num_milestones": 0}) + case util.OptionalBoolFalse: + cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + } + + return cond +} + +// SearchRepository returns repositories based on search options, +// it returns results in given range and number of total results. +func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { + cond := SearchRepositoryCondition(opts) + return SearchRepositoryByCondition(opts, cond, true) +} + +// SearchRepositoryByCondition search repositories by condition +func SearchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { + ctx := db.DefaultContext + sess, count, err := searchRepositoryByCondition(ctx, opts, cond) + if err != nil { + return nil, 0, err + } + + defaultSize := 50 + if opts.PageSize > 0 { + defaultSize = opts.PageSize + } + repos := make(RepositoryList, 0, defaultSize) + if err := sess.Find(&repos); err != nil { + return nil, 0, fmt.Errorf("Repo: %v", err) + } + + if opts.PageSize <= 0 { + count = int64(len(repos)) + } + + if loadAttributes { + if err := repos.loadAttributes(ctx); err != nil { + return nil, 0, fmt.Errorf("LoadAttributes: %v", err) + } + } + + return repos, count, nil +} + +func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { + if opts.Page <= 0 { + opts.Page = 1 + } + + if len(opts.OrderBy) == 0 { + opts.OrderBy = db.SearchOrderByAlphabetically + } + + args := make([]interface{}, 0) + if opts.PriorityOwnerID > 0 { + opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy)) + args = append(args, opts.PriorityOwnerID) + } else if strings.Count(opts.Keyword, "/") == 1 { + // With "owner/repo" search times, prioritise results which match the owner field + orgName := strings.Split(opts.Keyword, "/")[0] + opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy)) + args = append(args, orgName) + } + + sess := db.GetEngine(ctx) + + var count int64 + if opts.PageSize > 0 { + var err error + count, err = sess. + Where(cond). + Count(new(Repository)) + if err != nil { + return nil, 0, fmt.Errorf("Count: %v", err) + } + } + + sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...) + if opts.PageSize > 0 { + sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + return sess, count, nil +} + +// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible +func AccessibleRepositoryCondition(user *user_model.User) builder.Cond { + cond := builder.NewCond() + + if user == nil || !user.IsRestricted || user.ID <= 0 { + orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate} + if user == nil || user.ID <= 0 { + orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) + } + // 1. Be able to see all non-private repositories that either: + cond = cond.Or(builder.And( + builder.Eq{"`repository`.is_private": false}, + // 2. Aren't in an private organisation or limited organisation if we're not logged in + builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": user_model.UserTypeOrganization}, + builder.In("visibility", orgVisibilityLimit)), + )))) + } + + if user != nil { + cond = cond.Or( + // 2. Be able to see all repositories that we have access to + UserCollaborationRepoCond("`repository`.id", user.ID), + // 3. Repositories that we directly own + builder.Eq{"`repository`.owner_id": user.ID}, + // 4. Be able to see all repositories that we are in a team + userOrgTeamRepoCond("`repository`.id", user.ID), + // 5. Be able to see all public repos in private organizations that we are an org_user of + userOrgPublicRepoCond(user.ID), + ) + } + + return cond +} + +// SearchRepositoryByName takes keyword and part of repository name to search, +// it returns results in given range and number of total results. +func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) { + opts.IncludeDescription = false + return SearchRepository(opts) +} + +// SearchRepositoryIDs takes keyword and part of repository name to search, +// it returns results in given range and number of total results. +func SearchRepositoryIDs(opts *SearchRepoOptions) ([]int64, int64, error) { + opts.IncludeDescription = false + + cond := SearchRepositoryCondition(opts) + + sess, count, err := searchRepositoryByCondition(db.DefaultContext, opts, cond) + if err != nil { + return nil, 0, err + } + + defaultSize := 50 + if opts.PageSize > 0 { + defaultSize = opts.PageSize + } + + ids := make([]int64, 0, defaultSize) + err = sess.Select("id").Table("repository").Find(&ids) + if opts.PageSize <= 0 { + count = int64(len(ids)) + } + + return ids, count, err +} + +// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered. +func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder { + // NB: Please note this code needs to still work if user is nil + return builder.Select("id").From("repository").Where(AccessibleRepositoryCondition(user)) +} + +// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id +func FindUserAccessibleRepoIDs(user *user_model.User) ([]int64, error) { + repoIDs := make([]int64, 0, 10) + if err := db.GetEngine(db.DefaultContext). + Table("repository"). + Cols("id"). + Where(AccessibleRepositoryCondition(user)). + Find(&repoIDs); err != nil { + return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err) + } + return repoIDs, nil +} + +// GetUserRepositories returns a list of repositories of given user. +func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error) { + if len(opts.OrderBy) == 0 { + opts.OrderBy = "updated_unix DESC" + } + + cond := builder.NewCond() + cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID}) + if !opts.Private { + cond = cond.And(builder.Eq{"is_private": false}) + } + + if opts.LowerNames != nil && len(opts.LowerNames) > 0 { + cond = cond.And(builder.In("lower_name", opts.LowerNames)) + } + + sess := db.GetEngine(db.DefaultContext) + + count, err := sess.Where(cond).Count(new(Repository)) + if err != nil { + return nil, 0, fmt.Errorf("Count: %v", err) + } + + sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) + repos := make(RepositoryList, 0, opts.PageSize) + return repos, count, db.SetSessionPagination(sess, opts).Find(&repos) +} diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go new file mode 100644 index 0000000000..f9c84a0f3f --- /dev/null +++ b/models/repo/repo_list_test.go @@ -0,0 +1,385 @@ +// 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_test + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" +) + +func TestSearchRepository(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // test search public repository on explore page + repos, count, err := repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "repo_12", + Collaborate: util.OptionalBoolFalse, + }) + + assert.NoError(t, err) + if assert.Len(t, repos, 1) { + assert.Equal(t, "test_repo_12", repos[0].Name) + } + assert.Equal(t, int64(1), count) + + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "test_repo", + Collaborate: util.OptionalBoolFalse, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + assert.Len(t, repos, 2) + + // test search private repository on explore page + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "repo_13", + Private: true, + Collaborate: util.OptionalBoolFalse, + }) + + assert.NoError(t, err) + if assert.Len(t, repos, 1) { + assert.Equal(t, "test_repo_13", repos[0].Name) + } + assert.Equal(t, int64(1), count) + + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "test_repo", + Private: true, + Collaborate: util.OptionalBoolFalse, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(3), count) + assert.Len(t, repos, 3) + + // Test non existing owner + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) + + assert.NoError(t, err) + assert.Empty(t, repos) + assert.Equal(t, int64(0), count) + + // Test search within description + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "description_14", + Collaborate: util.OptionalBoolFalse, + IncludeDescription: true, + }) + + assert.NoError(t, err) + if assert.Len(t, repos, 1) { + assert.Equal(t, "test_repo_14", repos[0].Name) + } + assert.Equal(t, int64(1), count) + + // Test NOT search within description + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 10, + }, + Keyword: "description_14", + Collaborate: util.OptionalBoolFalse, + IncludeDescription: false, + }) + + assert.NoError(t, err) + assert.Empty(t, repos) + assert.Equal(t, int64(0), count) + + testCases := []struct { + name string + opts *repo_model.SearchRepoOptions + count int + }{ + { + name: "PublicRepositoriesByName", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse}, + count: 7, + }, + { + name: "PublicAndPrivateRepositoriesByName", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "PublicRepositoriesOfUser", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, + count: 2, + }, + { + name: "PublicRepositoriesOfUser2", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, + count: 0, + }, + { + name: "PublicRepositoriesOfUser3", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, + count: 2, + }, + { + name: "PublicAndPrivateRepositoriesOfUser", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 4, + }, + { + name: "PublicAndPrivateRepositoriesOfUser2", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 0, + }, + { + name: "PublicAndPrivateRepositoriesOfUser3", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 4, + }, + { + name: "PublicRepositoriesOfUserIncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, + count: 5, + }, + { + name: "PublicRepositoriesOfUser2IncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, + count: 1, + }, + { + name: "PublicRepositoriesOfUser3IncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, + count: 3, + }, + { + name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, + count: 9, + }, + { + name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, + count: 4, + }, + { + name: "PublicAndPrivateRepositoriesOfUser3IncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, + count: 7, + }, + { + name: "PublicRepositoriesOfOrganization", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, + count: 1, + }, + { + name: "PublicAndPrivateRepositoriesOfOrganization", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 2, + }, + { + name: "AllPublic/PublicRepositoriesByName", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + count: 7, + }, + { + name: "AllPublic/PublicAndPrivateRepositoriesByName", + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + count: 14, + }, + { + name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, + count: 28, + }, + { + name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, + count: 33, + }, + { + name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", + opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, + count: 15, + }, + { + name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", + opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, + count: 13, + }, + { + name: "AllPublic/PublicRepositoriesOfOrganization", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, + count: 28, + }, + { + name: "AllTemplates", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue}, + count: 2, + }, + { + name: "OwnerSlashRepoSearch", + opts: &repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + count: 3, + }, + { + name: "OwnerSlashSearch", + opts: &repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + count: 4, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + repos, count, err := repo_model.SearchRepositoryByName(testCase.opts) + + assert.NoError(t, err) + assert.Equal(t, int64(testCase.count), count) + + page := testCase.opts.Page + if page <= 0 { + page = 1 + } + expectedLen := testCase.opts.PageSize + if testCase.opts.PageSize*page > testCase.count+testCase.opts.PageSize { + expectedLen = 0 + } else if testCase.opts.PageSize*page > testCase.count { + expectedLen = testCase.count % testCase.opts.PageSize + } + if assert.Len(t, repos, expectedLen) { + for _, repo := range repos { + assert.NotEmpty(t, repo.Name) + + if len(testCase.opts.Keyword) > 0 { + // Keyword match condition is different for search terms of form "owner/repo" + if strings.Count(testCase.opts.Keyword, "/") == 1 { + // May still match as a whole... + wholeMatch := strings.Contains(repo.Name, testCase.opts.Keyword) + + pieces := strings.Split(testCase.opts.Keyword, "/") + ownerName := pieces[0] + repoName := pieces[1] + // ... or match in parts + splitMatch := strings.Contains(repo.OwnerName, ownerName) && strings.Contains(repo.Name, repoName) + + assert.True(t, wholeMatch || splitMatch, "Keyword '%s' does not match repo '%s/%s'", testCase.opts.Keyword, repo.Owner.Name, repo.Name) + } else { + assert.Contains(t, repo.Name, testCase.opts.Keyword) + } + } + + if !testCase.opts.Private { + assert.False(t, repo.IsPrivate) + } + + if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue { + assert.True(t, repo.IsFork || repo.IsMirror) + } else { + switch testCase.opts.Fork { + case util.OptionalBoolFalse: + assert.False(t, repo.IsFork) + case util.OptionalBoolTrue: + assert.True(t, repo.IsFork) + } + + switch testCase.opts.Mirror { + case util.OptionalBoolFalse: + assert.False(t, repo.IsMirror) + case util.OptionalBoolTrue: + assert.True(t, repo.IsMirror) + } + } + + if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic { + switch testCase.opts.Collaborate { + case util.OptionalBoolFalse: + assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) + case util.OptionalBoolTrue: + assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) + } + } + } + } + }) + } +} + +func TestSearchRepositoryByTopicName(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testCases := []struct { + name string + opts *repo_model.SearchRepoOptions + count int + }{ + { + name: "AllPublic/SearchPublicRepositoriesFromTopicAndName", + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, + count: 2, + }, + { + name: "AllPublic/OnlySearchPublicRepositoriesFromTopic", + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, + count: 1, + }, + { + name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic", + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, + count: 2, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, count, err := repo_model.SearchRepositoryByName(testCase.opts) + assert.NoError(t, err) + assert.Equal(t, int64(testCase.count), count) + }) + } +} diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index cf6ee8b67a..8ae84eab52 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/util" @@ -15,18 +16,18 @@ import ( ) var ( - countRepospts = CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} - countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} + countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} + countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} + countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} ) func TestGetRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx := db.DefaultContext - count, err1 := CountRepositories(ctx, countRepospts) - privateCount, err2 := CountRepositories(ctx, countReposptsPrivate) - publicCount, err3 := CountRepositories(ctx, countReposptsPublic) + count, err1 := repo_model.CountRepositories(ctx, countRepospts) + privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate) + publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic) assert.NoError(t, err1) assert.NoError(t, err2) assert.NoError(t, err3) @@ -37,7 +38,7 @@ func TestGetRepositoryCount(t *testing.T) { func TestGetPublicRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountRepositories(db.DefaultContext, countReposptsPublic) + count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic) assert.NoError(t, err) assert.Equal(t, int64(1), count) } @@ -45,14 +46,14 @@ func TestGetPublicRepositoryCount(t *testing.T) { func TestGetPrivateRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountRepositories(db.DefaultContext, countReposptsPrivate) + count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate) 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) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL()) } diff --git a/models/repo/star_test.go b/models/repo/star_test.go index 2dde09c745..aa72b1dac8 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -17,26 +18,26 @@ 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}) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) } func TestIsStaring(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, IsStaring(db.DefaultContext, 2, 4)) - assert.False(t, IsStaring(db.DefaultContext, 3, 4)) + assert.True(t, repo_model.IsStaring(db.DefaultContext, 2, 4)) + assert.False(t, repo_model.IsStaring(db.DefaultContext, 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}).(*repo_model.Repository) + gazers, err := repo_model.GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) if assert.Len(t, gazers, 1) { assert.Equal(t, int64(2), gazers[0].ID) @@ -46,8 +47,8 @@ func TestRepository_GetStargazers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + gazers, err := repo_model.GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) assert.Len(t, gazers, 0) } diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go index 353d96ef3e..8187addb81 100644 --- a/models/repo/topic_test.go +++ b/models/repo/topic_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -19,47 +20,47 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - topics, _, err := FindTopics(&FindTopicOptions{}) + topics, _, err := repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, total, err := FindTopics(&FindTopicOptions{ + topics, total, err := repo_model.FindTopics(&repo_model.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{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 1, }) assert.NoError(t, err) assert.Len(t, topics, repo1NrOfTopics) - assert.NoError(t, SaveTopics(2, "golang")) + assert.NoError(t, repo_model.SaveTopics(2, "golang")) repo2NrOfTopics := 1 - topics, _, err = FindTopics(&FindTopicOptions{}) + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = FindTopics(&FindTopicOptions{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) assert.Len(t, topics, repo2NrOfTopics) - assert.NoError(t, SaveTopics(2, "golang", "gitea")) + assert.NoError(t, repo_model.SaveTopics(2, "golang", "gitea")) repo2NrOfTopics = 2 totalNrOfTopics++ - topic, err := GetTopicByName("gitea") + topic, err := repo_model.GetTopicByName("gitea") assert.NoError(t, err) assert.EqualValues(t, 1, topic.RepoCount) - topics, _, err = FindTopics(&FindTopicOptions{}) + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = FindTopics(&FindTopicOptions{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) @@ -67,14 +68,14 @@ func TestAddTopic(t *testing.T) { } 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")) + assert.True(t, repo_model.ValidateTopic("12345")) + assert.True(t, repo_model.ValidateTopic("2-test")) + assert.True(t, repo_model.ValidateTopic("test-3")) + assert.True(t, repo_model.ValidateTopic("first")) + assert.True(t, repo_model.ValidateTopic("second-test-topic")) + assert.True(t, repo_model.ValidateTopic("third-project-topic-with-max-length")) + + assert.False(t, repo_model.ValidateTopic("$fourth-test,topic")) + assert.False(t, repo_model.ValidateTopic("-fifth-test-topic")) + assert.False(t, repo_model.ValidateTopic("sixth-go-project-topic-with-excess-length")) } diff --git a/models/repo/update.go b/models/repo/update.go index 7fb51c9593..07776ebc01 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -172,3 +172,11 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s return committer.Commit() } + +// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize +func UpdateRepoSize(ctx context.Context, repoID, size int64) error { + _, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{ + Size: size, + }) + return err +} diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index fe96771796..e697505b81 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -5,7 +5,14 @@ package repo import ( + "context" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" ) // GetStarredRepos returns the repos starred by a particular user @@ -48,3 +55,118 @@ func GetWatchedRepos(userID int64, private bool, listOptions db.ListOptions) ([] total, err := sess.FindAndCount(&repos) return repos, total, err } + +// GetRepoAssignees returns all users that have write access and can be assigned to issues +// of the repository, +func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { + if err = repo.GetOwner(ctx); err != nil { + return nil, err + } + + e := db.GetEngine(ctx) + userIDs := make([]int64, 0, 10) + if err = e.Table("access"). + Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite). + Select("user_id"). + Find(&userIDs); err != nil { + return nil, err + } + + additionalUserIDs := make([]int64, 0, 10) + if err = e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite). + Distinct("`team_user`.uid"). + Select("`team_user`.uid"). + Find(&additionalUserIDs); err != nil { + return nil, err + } + + uidMap := map[int64]bool{} + i := 0 + for _, uid := range userIDs { + if uidMap[uid] { + continue + } + uidMap[uid] = true + userIDs[i] = uid + i++ + } + userIDs = userIDs[:i] + userIDs = append(userIDs, additionalUserIDs...) + + for _, uid := range additionalUserIDs { + if uidMap[uid] { + continue + } + userIDs[i] = uid + i++ + } + userIDs = userIDs[:i] + + // Leave a seat for owner itself to append later, but if owner is an organization + // and just waste 1 unit is cheaper than re-allocate memory once. + users := make([]*user_model.User, 0, len(userIDs)+1) + if len(userIDs) > 0 { + if err = e.In("id", userIDs).Find(&users); err != nil { + return nil, err + } + } + if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] { + users = append(users, repo.Owner) + } + + return users, nil +} + +// GetReviewers get all users can be requested to review: +// * for private repositories this returns all users that have read access or higher to the repository. +// * for public repositories this returns all users that have read access or higher to the repository, +// all repo watchers and all organization members. +// TODO: may be we should have a busy choice for users to block review request to them. +func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) { + // Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries + if err := repo.GetOwner(ctx); err != nil { + return nil, err + } + + cond := builder.And(builder.Neq{"`user`.id": posterID}) + + if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { + // This a private repository: + // Anyone who can read the repository is a requestable reviewer + + cond = cond.And(builder.In("`user`.id", + builder.Select("user_id").From("access").Where( + builder.Eq{"repo_id": repo.ID}. + And(builder.Gte{"mode": perm.AccessModeRead}), + ), + )) + + if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID { + // as private *user* repos don't generate an entry in the `access` table, + // the owner of a private repo needs to be explicitly added. + cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) + } + + } else { + // This is a "public" repository: + // Any user that has read access, is a watcher or organization member can be requested to review + cond = cond.And(builder.And(builder.In("`user`.id", + builder.Select("user_id").From("access"). + Where(builder.Eq{"repo_id": repo.ID}. + And(builder.Gte{"mode": perm.AccessModeRead})), + ).Or(builder.In("`user`.id", + builder.Select("user_id").From("watch"). + Where(builder.Eq{"repo_id": repo.ID}. + And(builder.In("mode", WatchModeNormal, WatchModeAuto))), + ).Or(builder.In("`user`.id", + builder.Select("uid").From("org_user"). + Where(builder.Eq{"org_id": repo.OwnerID}), + ))))) + } + + users := make([]*user_model.User, 0, 8) + return users, db.GetEngine(ctx).Where(cond).OrderBy("name").Find(&users) +} diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go new file mode 100644 index 0000000000..d024729b9c --- /dev/null +++ b/models/repo/user_repo_test.go @@ -0,0 +1,74 @@ +// 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_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRepoAssignees(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + users, err := repo_model.GetRepoAssignees(db.DefaultContext, repo2) + assert.NoError(t, err) + assert.Len(t, users, 1) + assert.Equal(t, users[0].ID, int64(2)) + + repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}).(*repo_model.Repository) + users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) + assert.NoError(t, err) + assert.Len(t, users, 3) + assert.Equal(t, users[0].ID, int64(15)) + assert.Equal(t, users[1].ID, int64(18)) + assert.Equal(t, users[2].ID, int64(16)) +} + +func TestRepoGetReviewers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // test public repo + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + + ctx := db.DefaultContext + reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 4) + + // should include doer if doer is not PR poster. + reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 4) + + // should not include PR poster, if PR poster would be otherwise eligible + reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 3) + + // test private user repo + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + + reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) + assert.EqualValues(t, reviewers[0].ID, 2) + + // test private org repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + + reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1) + assert.NoError(t, err) + assert.Len(t, reviewers, 2) + + reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) +} diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index 2f4e04ab17..3875e63fd8 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -17,20 +18,20 @@ import ( 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.True(t, repo_model.IsWatching(1, 1)) + assert.True(t, repo_model.IsWatching(4, 1)) + assert.True(t, repo_model.IsWatching(11, 1)) - assert.False(t, IsWatching(1, 5)) - assert.False(t, IsWatching(8, 1)) - assert.False(t, IsWatching(unittest.NonexistentID, unittest.NonexistentID)) + assert.False(t, repo_model.IsWatching(1, 5)) + assert.False(t, repo_model.IsWatching(8, 1)) + assert.False(t, repo_model.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) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watches, err := repo_model.GetWatchers(db.DefaultContext, repo.ID) assert.NoError(t, err) // One watchers are inactive, thus minus 1 assert.Len(t, watches, repo.NumWatches-1) @@ -38,7 +39,7 @@ func TestGetWatchers(t *testing.T) { assert.EqualValues(t, repo.ID, watch.RepoID) } - watches, err = GetWatchers(db.DefaultContext, unittest.NonexistentID) + watches, err = repo_model.GetWatchers(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) assert.Len(t, watches, 0) } @@ -46,16 +47,16 @@ func TestGetWatchers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watchers, err := repo_model.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}) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID}) } - repo = unittest.AssertExistsAndLoadBean(t, &Repository{ID: 9}).(*Repository) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9}).(*repo_model.Repository) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, 0) } @@ -63,8 +64,8 @@ func TestRepository_GetWatchers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watchers, err := repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, repo.NumWatches) @@ -73,46 +74,46 @@ func TestWatchIfAuto(t *testing.T) { prevCount := repo.NumWatches // Must not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 8, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 10, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 10, 1, true)) + watchers, err = repo_model.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(db.DefaultContext, 8, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, false)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, false)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + watchers, err = repo_model.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(db.DefaultContext, 12, 1, false)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Must not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) } @@ -120,20 +121,20 @@ func TestWatchIfAuto(t *testing.T) { func TestWatchRepoMode(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + unittest.AssertCount(t, &repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeAuto)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNormal)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeDont)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1) - assert.NoError(t, WatchRepoMode(12, 1, WatchModeNone)) - unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) } diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index f5e61e5ae3..339289e05d 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "path/filepath" "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -17,7 +18,7 @@ import ( func TestRepository_WikiCloneLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) cloneLink := repo.WikiCloneLink() assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH) assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) @@ -26,20 +27,20 @@ func TestRepository_WikiCloneLink(t *testing.T) { func TestWikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, WikiPath("user2", "repo1")) + assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1")) } func TestRepository_WikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") assert.Equal(t, expected, repo.WikiPath()) } func TestRepository_HasWiki(t *testing.T) { unittest.PrepareTestEnv(t) - repo1 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) assert.True(t, repo1.HasWiki()) - repo2 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) assert.False(t, repo2.HasWiki()) } |