From ddb7f59ef414ffad8dc2485055c246aaea0507d7 Mon Sep 17 00:00:00 2001 From: Morlinest Date: Thu, 26 Oct 2017 23:16:13 +0200 Subject: [PATCH] Add search mode option to /api/repo/search (#2756) * Add repo type option to /api/repo/search * Add tests and fix result of collaborative filter in specific condition * Fix/optimize search & tests * Improve integration tests * Fix lint errors * Fix unit tests * Change and improve internal implementation of repo search * Use NonexistentID * Make search api more general * Change mirror and fork search behaviour * Fix tests & typo in comment --- integrations/api_repo_test.go | 57 ++++++++-- models/fixtures/access.yml | 14 ++- models/fixtures/org_user.yml | 8 ++ models/fixtures/repository.yml | 94 +++++++++++++++++ models/fixtures/team.yml | 11 ++ models/fixtures/team_repo.yml | 14 ++- models/fixtures/team_user.yml | 8 +- models/fixtures/user.yml | 32 ++++++ models/issue_indexer.go | 9 +- models/repo_list.go | 73 +++++++------ models/repo_list_test.go | 188 +++++++++++++++++++++------------ models/user_test.go | 9 +- public/swagger.v1.json | 19 +++- routers/api/v1/repo/repo.go | 65 ++++++++++-- routers/home.go | 15 ++- routers/user/profile.go | 31 +++--- 16 files changed, 505 insertions(+), 142 deletions(-) diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index f517ee42c..b766dd584 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -51,6 +51,7 @@ func TestAPISearchRepo(t *testing.T) { user := models.AssertExistsAndLoadBean(t, &models.User{ID: 15}).(*models.User) user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 16}).(*models.User) user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 18}).(*models.User) + user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 20}).(*models.User) orgUser := models.AssertExistsAndLoadBean(t, &models.User{ID: 17}).(*models.User) // Map of expected results, where key is user for login @@ -66,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) { expectedResults }{ {name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{ - nil: {count: 12}, - user: {count: 12}, - user2: {count: 12}}, + nil: {count: 15}, + user: {count: 15}, + user2: {count: 15}}, }, {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ nil: {count: 10}, @@ -81,9 +82,9 @@ func TestAPISearchRepo(t *testing.T) { user2: {count: 10}}, }, {name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s", "big_test_"), expectedResults: expectedResults{ - nil: {count: 4, repoName: "big_test_"}, - user: {count: 4, repoName: "big_test_"}, - user2: {count: 4, repoName: "big_test_"}}, + nil: {count: 7, repoName: "big_test_"}, + user: {count: 7, repoName: "big_test_"}, + user2: {count: 7, repoName: "big_test_"}}, }, {name: "RepositoriesAccessibleAndRelatedToUser", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user.ID), expectedResults: expectedResults{ nil: {count: 4}, @@ -106,6 +107,34 @@ func TestAPISearchRepo(t *testing.T) { user: {count: 2, repoOwnerID: orgUser.ID, includesPrivate: true}, user2: {count: 1, repoOwnerID: orgUser.ID}}, }, + {name: "RepositoriesAccessibleAndRelatedToUser4", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user4.ID), expectedResults: expectedResults{ + nil: {count: 3}, + user: {count: 3}, + user4: {count: 6, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeSource", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "source"), expectedResults: expectedResults{ + nil: {count: 0}, + user: {count: 0}, + user4: {count: 0, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "fork"), expectedResults: expectedResults{ + nil: {count: 1}, + user: {count: 1}, + user4: {count: 2, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "fork"), expectedResults: expectedResults{ + nil: {count: 1}, + user: {count: 1}, + user4: {count: 2, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "mirror"), expectedResults: expectedResults{ + nil: {count: 2}, + user: {count: 2}, + user4: {count: 4, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "mirror"), expectedResults: expectedResults{ + nil: {count: 1}, + user: {count: 1}, + user4: {count: 2, includesPrivate: true}}}, + {name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeCollaborative", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "collaborative"), expectedResults: expectedResults{ + nil: {count: 0}, + user: {count: 0}, + user4: {count: 0, includesPrivate: true}}}, } for _, testCase := range testCases { @@ -113,9 +142,11 @@ func TestAPISearchRepo(t *testing.T) { for userToLogin, expected := range testCase.expectedResults { var session *TestSession var testName string + var userID int64 if userToLogin != nil && userToLogin.ID > 0 { testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID) session = loginUser(t, userToLogin.Name) + userID = userToLogin.ID } else { testName = "AnonymousUser" session = emptyTestSession(t) @@ -130,6 +161,11 @@ func TestAPISearchRepo(t *testing.T) { assert.Len(t, body.Data, expected.count) for _, repo := range body.Data { + r := getRepo(t, repo.ID) + hasAccess, err := models.HasAccess(userID, r, models.AccessModeRead) + assert.NoError(t, err) + assert.True(t, hasAccess) + assert.NotEmpty(t, repo.Name) if len(expected.repoName) > 0 { @@ -150,6 +186,15 @@ func TestAPISearchRepo(t *testing.T) { } } +var repoCache = make(map[int64]*models.Repository) + +func getRepo(t *testing.T, repoID int64) *models.Repository { + if _, ok := repoCache[repoID]; !ok { + repoCache[repoID] = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repoID}).(*models.Repository) + } + return repoCache[repoID] +} + func TestAPIViewRepo(t *testing.T) { prepareTestEnv(t) diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 9c149b78d..af2c8a529 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -62,4 +62,16 @@ id: 11 user_id: 18 repo_id: 21 - mode: 2 # write \ No newline at end of file + mode: 2 # write + +- + id: 12 + user_id: 20 + repo_id: 27 + mode: 4 # owner + +- + id: 13 + user_id: 20 + repo_id: 28 + mode: 4 # owner \ No newline at end of file diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml index 50d8ef5e6..709a1997b 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -44,4 +44,12 @@ org_id: 17 is_public: false is_owner: true + num_teams: 1 + +- + id: 7 + uid: 20 + org_id: 19 + is_public: true + is_owner: true num_teams: 1 \ No newline at end of file diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index eb83dfcff..91342d076 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -201,6 +201,7 @@ num_closed_pulls: 0 num_watches: 0 is_mirror: false + is_fork: false - id: 18 @@ -213,6 +214,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 19 @@ -225,6 +227,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 20 @@ -237,6 +240,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 21 @@ -249,6 +253,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 22 @@ -261,6 +266,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 23 @@ -273,6 +279,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false - id: 24 @@ -285,3 +292,90 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + is_fork: false + +- + id: 25 + owner_id: 20 + lower_name: big_test_public_mirror_5 + name: big_test_public_mirror_5 + is_private: false + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_watches: 0 + is_mirror: true + is_fork: false + +- + id: 26 + owner_id: 20 + lower_name: big_test_private_mirror_5 + name: big_test_private_mirror_5 + is_private: true + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_watches: 0 + is_mirror: true + is_fork: false + +- + id: 27 + owner_id: 19 + lower_name: big_test_public_mirror_6 + name: big_test_public_mirror_6 + is_private: false + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_watches: 0 + is_mirror: true + num_forks: 1 + is_fork: false + +- + id: 28 + owner_id: 19 + lower_name: big_test_private_mirror_6 + name: big_test_private_mirror_6 + is_private: true + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_watches: 0 + is_mirror: true + num_forks: 1 + is_fork: false + +- + id: 29 + fork_id: 27 + owner_id: 20 + lower_name: big_test_public_fork_7 + name: big_test_public_fork_7 + is_private: false + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + is_mirror: false + is_fork: true + +- + id: 30 + fork_id: 28 + owner_id: 20 + lower_name: big_test_private_fork_7 + name: big_test_private_fork_7 + is_private: true + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + is_mirror: false + is_fork: true \ No newline at end of file diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 2b2186dea..1d242cb5b 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -37,6 +37,7 @@ num_repos: 0 num_members: 1 unit_types: '[1,2,3,4,5,6,7]' + - id: 5 org_id: 17 @@ -45,4 +46,14 @@ authorize: 4 # owner num_repos: 2 num_members: 2 + unit_types: '[1,2,3,4,5,6,7]' + +- + id: 6 + org_id: 19 + lower_name: owners + name: Owners + authorize: 4 # owner + num_repos: 2 + num_members: 1 unit_types: '[1,2,3,4,5,6,7]' \ No newline at end of file diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index 5154453f7..9e6d74553 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -26,4 +26,16 @@ id: 5 org_id: 17 team_id: 5 - repo_id: 24 \ No newline at end of file + repo_id: 24 + +- + id: 6 + org_id: 19 + team_id: 6 + repo_id: 27 + +- + id: 7 + org_id: 19 + team_id: 6 + repo_id: 28 \ No newline at end of file diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 56025bb0b..b1dfcdfde 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -38,4 +38,10 @@ id: 7 org_id: 17 team_id: 5 - uid: 18 \ No newline at end of file + uid: 18 + +- + id: 8 + org_id: 19 + team_id: 6 + uid: 20 \ No newline at end of file diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 1e0625598..60f5e8405 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -281,4 +281,36 @@ avatar: avatar18 avatar_email: user18@example.com num_repos: 0 + is_active: true + +- + id: 19 + lower_name: user19 + name: user19 + full_name: User 19 + email: user19@example.com + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password + type: 1 # organization + salt: ZogKvWdyEx + is_admin: false + avatar: avatar19 + avatar_email: user19@example.com + num_repos: 2 + is_active: true + num_members: 1 + num_teams: 1 + +- + id: 20 + lower_name: user20 + name: user20 + full_name: User 20 + email: user20@example.com + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password + type: 0 # individual + salt: ZogKvWdyEx + is_admin: false + avatar: avatar20 + avatar_email: user20@example.com + num_repos: 4 is_active: true \ No newline at end of file diff --git a/models/issue_indexer.go b/models/issue_indexer.go index 18c6f281b..e43dfc9fa 100644 --- a/models/issue_indexer.go +++ b/models/issue_indexer.go @@ -28,10 +28,11 @@ func populateIssueIndexer() error { batch := indexer.IssueIndexerBatch() for page := 1; ; page++ { repos, _, err := SearchRepositoryByName(&SearchRepoOptions{ - Page: page, - PageSize: 10, - OrderBy: SearchOrderByID, - Private: true, + Page: page, + PageSize: 10, + OrderBy: SearchOrderByID, + Private: true, + Collaborate: util.OptionalBoolFalse, }) if err != nil { return fmt.Errorf("Repositories: %v", err) diff --git a/models/repo_list.go b/models/repo_list.go index 2c4c66ac3..883e3b98d 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -8,6 +8,8 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/modules/util" + "github.com/go-xorm/builder" ) @@ -88,28 +90,28 @@ func (repos MirrorRepositoryList) LoadAttributes() error { } // SearchRepoOptions holds the search options -// swagger:parameters repoSearch type SearchRepoOptions struct { - // Keyword to search - // - // in: query - Keyword string `json:"q"` - // Owner in we search search - // - // in: query - OwnerID int64 `json:"uid"` - OrderBy SearchOrderBy `json:"-"` - Private bool `json:"-"` // Include private repositories in results - Collaborate bool `json:"-"` // Include collaborative repositories - Starred bool `json:"-"` - Page int `json:"-"` - IsProfile bool `json:"-"` - AllPublic bool `json:"-"` // Include also all public repositories - // Limit of result - // - // maximum: setting.ExplorePagingNum - // in: query - PageSize int `json:"limit"` // Can be smaller than or equal to setting.ExplorePagingNum + Keyword string + OwnerID int64 + OrderBy SearchOrderBy + Private bool // Include private repositories in results + Starred bool + Page int + IsProfile bool + AllPublic bool // Include also all public repositories + PageSize int // Can be smaller than or equal to setting.ExplorePagingNum + // None -> include collaborative AND non-collaborative + // True -> include just collaborative + // False -> incude 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 mirrors AND non-mirrors + // True -> include just mirrors + // False -> include just non-mirrors + Mirror util.OptionalBool } //SearchOrderBy is used to sort the result @@ -146,17 +148,18 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err cond = cond.And(builder.Eq{"is_private": false}) } - starred := false + var starred bool if opts.OwnerID > 0 { if opts.Starred { starred = true - cond = builder.Eq{ - "star.uid": opts.OwnerID, - } + cond = builder.Eq{"star.uid": opts.OwnerID} } else { - var accessCond builder.Cond = builder.Eq{"owner_id": opts.OwnerID} + var accessCond = builder.NewCond() + if opts.Collaborate != util.OptionalBoolTrue { + accessCond = builder.Eq{"owner_id": opts.OwnerID} + } - if opts.Collaborate { + if opts.Collaborate != util.OptionalBoolFalse { collaborateCond := builder.And( builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID), builder.Neq{"owner_id": opts.OwnerID}) @@ -167,18 +170,26 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err accessCond = accessCond.Or(collaborateCond) } + if opts.AllPublic { + accessCond = accessCond.Or(builder.Eq{"is_private": false}) + } + cond = cond.And(accessCond) } } - if opts.OwnerID > 0 && opts.AllPublic { - cond = cond.Or(builder.Eq{"is_private": false}) - } - if opts.Keyword != "" { cond = cond.And(builder.Like{"lower_name", strings.ToLower(opts.Keyword)}) } + 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 len(opts.OrderBy) == 0 { opts.OrderBy = SearchOrderByAlphabetically } diff --git a/models/repo_list_test.go b/models/repo_list_test.go index 4d125633a..3bccb1aeb 100644 --- a/models/repo_list_test.go +++ b/models/repo_list_test.go @@ -7,6 +7,8 @@ package models import ( "testing" + "code.gitea.io/gitea/modules/util" + "github.com/stretchr/testify/assert" ) @@ -15,9 +17,10 @@ func TestSearchRepositoryByName(t *testing.T) { // test search public repository on explore page repos, count, err := SearchRepositoryByName(&SearchRepoOptions{ - Keyword: "repo_12", - Page: 1, - PageSize: 10, + Keyword: "repo_12", + Page: 1, + PageSize: 10, + Collaborate: util.OptionalBoolFalse, }) assert.NoError(t, err) @@ -27,9 +30,10 @@ func TestSearchRepositoryByName(t *testing.T) { assert.Equal(t, int64(1), count) repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ - Keyword: "test_repo", - Page: 1, - PageSize: 10, + Keyword: "test_repo", + Page: 1, + PageSize: 10, + Collaborate: util.OptionalBoolFalse, }) assert.NoError(t, err) @@ -38,10 +42,11 @@ func TestSearchRepositoryByName(t *testing.T) { // test search private repository on explore page repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ - Keyword: "repo_13", - Page: 1, - PageSize: 10, - Private: true, + Keyword: "repo_13", + Page: 1, + PageSize: 10, + Private: true, + Collaborate: util.OptionalBoolFalse, }) assert.NoError(t, err) @@ -51,84 +56,110 @@ func TestSearchRepositoryByName(t *testing.T) { assert.Equal(t, int64(1), count) repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ - Keyword: "test_repo", - Page: 1, - PageSize: 10, - Private: true, + Keyword: "test_repo", + Page: 1, + PageSize: 10, + 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 = SearchRepositoryByName(&SearchRepoOptions{OwnerID: NonexistentID}) + + assert.NoError(t, err) + assert.Empty(t, repos) + assert.Equal(t, int64(0), count) + testCases := []struct { name string opts *SearchRepoOptions count int }{ {name: "PublicRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", PageSize: 10}, - count: 4}, + opts: &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, Collaborate: util.OptionalBoolFalse}, + count: 7}, {name: "PublicAndPrivateRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true}, - count: 8}, + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, {name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 5, Private: true}, - count: 8}, + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, {name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &SearchRepoOptions{Keyword: "big_test_", Page: 2, PageSize: 5, Private: true}, - count: 8}, + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 2, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, + {name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 3, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, + {name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 3, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, {name: "PublicRepositoriesOfUser", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, count: 2}, {name: "PublicRepositoriesOfUser2", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, count: 0}, + {name: "PublicRepositoriesOfUser3", + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, + count: 2}, {name: "PublicAndPrivateRepositoriesOfUser", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, count: 4}, {name: "PublicAndPrivateRepositoriesOfUser2", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, count: 0}, + {name: "PublicAndPrivateRepositoriesOfUser3", + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, + count: 4}, {name: "PublicRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15}, count: 4}, {name: "PublicRepositoriesOfUser2IncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Collaborate: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18}, count: 1}, + {name: "PublicRepositoriesOfUser3IncludingCollaborative", + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20}, + count: 3}, {name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true}, count: 8}, {name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true}, count: 4}, + {name: "PublicAndPrivateRepositoriesOfUser3IncludingCollaborative", + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Private: true}, + count: 6}, {name: "PublicRepositoriesOfOrganization", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, count: 1}, {name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Private: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, count: 2}, {name: "AllPublic/PublicRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, AllPublic: true}, - count: 4}, + opts: &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + count: 7}, {name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, AllPublic: true}, - count: 8}, + opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + count: 14}, {name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: true, AllPublic: true}, - count: 12}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, + count: 15}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true, AllPublic: true}, - count: 16}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, + count: 19}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", - opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true, AllPublic: true}, - count: 10}, + opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, + count: 13}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", - opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: true, AllPublic: true}, - count: 8}, + opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 18, Private: true, AllPublic: true}, + count: 11}, {name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true}, - count: 12}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + count: 15}, } for _, testCase := range testCases { @@ -138,27 +169,54 @@ func TestSearchRepositoryByName(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(testCase.count), count) - var expectedLen int - if testCase.opts.PageSize*testCase.opts.Page > testCase.count { + page := testCase.opts.Page + if page <= 0 { + page = 1 + } + var 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 - } else { - expectedLen = testCase.opts.PageSize } - assert.Len(t, repos, expectedLen) - - for _, repo := range repos { - assert.NotEmpty(t, repo.Name) - - if len(testCase.opts.Keyword) > 0 { - assert.Contains(t, repo.Name, testCase.opts.Keyword) - } - - if testCase.opts.OwnerID > 0 && !testCase.opts.Collaborate && !testCase.opts.AllPublic { - assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) - } - - if !testCase.opts.Private { - assert.False(t, repo.IsPrivate) + if assert.Len(t, repos, expectedLen) { + for _, repo := range repos { + assert.NotEmpty(t, repo.Name) + + if len(testCase.opts.Keyword) > 0 { + 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) + } + } } } }) diff --git a/models/user_test.go b/models/user_test.go index 7ac9ebb0f..03ab54aaf 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -63,7 +63,10 @@ func TestSearchUsers(t *testing.T) { testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 2, PageSize: 2}, []int64{7, 17}) - testOrgSuccess(&SearchUserOptions{Page: 3, PageSize: 2}, + testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2}, + []int64{19}) + + testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2}, []int64{}) // test users @@ -73,13 +76,13 @@ func TestSearchUsers(t *testing.T) { } testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, - []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18}) + []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20}) testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse}, []int64{9}) testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, - []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18}) + []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20}) testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) diff --git a/public/swagger.v1.json b/public/swagger.v1.json index e640b4e83..c269fcb86 100644 --- a/public/swagger.v1.json +++ b/public/swagger.v1.json @@ -1102,7 +1102,7 @@ "type": "integer", "format": "int64", "x-go-name": "OwnerID", - "description": "Owner in we search search", + "description": "Repository owner to search", "name": "uid", "in": "query" }, @@ -1113,12 +1113,29 @@ "description": "Limit of result\n\nmaximum: setting.ExplorePagingNum", "name": "limit", "in": "query" + }, + { + "type": "string", + "x-go-name": "SearchMode", + "description": "Type of repository to search, related to owner", + "name": "mode", + "in": "query" + }, + { + "type": "boolean", + "x-go-name": "OwnerExclusive", + "description": "Search only owners repositories\nHas effect only if owner is provided and mode is not \"collaborative\"", + "name": "exclusive", + "in": "query" } ], "responses": { "200": { "$ref": "#/responses/SearchResults" }, + "422": { + "$ref": "#/responses/validationError" + }, "500": { "$ref": "#/responses/SearchError" } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 30e1186c0..34f4c5fa1 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -6,6 +6,7 @@ package repo import ( "fmt" + "net/http" "strings" api "code.gitea.io/sdk/gitea" @@ -15,9 +16,37 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/convert" ) +// SearchRepoOption options when searching repositories +// swagger:parameters repoSearch +type SearchRepoOption struct { // TODO: Move SearchRepoOption to Gitea SDK + // Keyword to search + // + // in: query + Keyword string `json:"q"` + // Repository owner to search + // + // in: query + OwnerID int64 `json:"uid"` + // Limit of result + // + // maximum: setting.ExplorePagingNum + // in: query + PageSize int `json:"limit"` + // Type of repository to search, related to owner + // + // in: query + SearchMode string `json:"mode"` + // Search only owners repositories + // Has effect only if owner is provided and mode is not "collaborative" + // + // in: query + OwnerExclusive bool `json:"exclusive"` +} + // Search repositories via options func Search(ctx *context.APIContext) { // swagger:route GET /repos/search repository repoSearch @@ -27,20 +56,44 @@ func Search(ctx *context.APIContext) { // // Responses: // 200: SearchResults + // 422: validationError // 500: SearchError opts := &models.SearchRepoOptions{ - Keyword: strings.Trim(ctx.Query("q"), " "), - OwnerID: ctx.QueryInt64("uid"), - PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), + Keyword: strings.Trim(ctx.Query("q"), " "), + OwnerID: ctx.QueryInt64("uid"), + PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), + Collaborate: util.OptionalBoolNone, + } + + if ctx.QueryBool("exclusive") { + opts.Collaborate = util.OptionalBoolFalse + } + + var mode = ctx.Query("mode") + switch mode { + case "source": + opts.Fork = util.OptionalBoolFalse + opts.Mirror = util.OptionalBoolFalse + case "fork": + opts.Fork = util.OptionalBoolTrue + case "mirror": + opts.Mirror = util.OptionalBoolTrue + case "collaborative": + opts.Mirror = util.OptionalBoolFalse + opts.Collaborate = util.OptionalBoolTrue + case "": + default: + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode)) + return } + var err error if opts.OwnerID > 0 { var repoOwner *models.User if ctx.User != nil && ctx.User.ID == opts.OwnerID { repoOwner = ctx.User } else { - var err error repoOwner, err = models.GetUserByID(opts.OwnerID) if err != nil { ctx.JSON(500, api.SearchError{ @@ -51,8 +104,8 @@ func Search(ctx *context.APIContext) { } } - if !repoOwner.IsOrganization() { - opts.Collaborate = true + if repoOwner.IsOrganization() { + opts.Collaborate = util.OptionalBoolFalse } // Check visibility. diff --git a/routers/home.go b/routers/home.go index d653d1e84..ce4e0be98 100644 --- a/routers/home.go +++ b/routers/home.go @@ -108,14 +108,13 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { keyword := strings.Trim(ctx.Query("q"), " ") repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ - Page: page, - PageSize: opts.PageSize, - OrderBy: orderBy, - Private: opts.Private, - Keyword: keyword, - OwnerID: opts.OwnerID, - Collaborate: true, - AllPublic: true, + Page: page, + PageSize: opts.PageSize, + OrderBy: orderBy, + Private: opts.Private, + Keyword: keyword, + OwnerID: opts.OwnerID, + AllPublic: true, }) if err != nil { ctx.Handle(500, "SearchRepositoryByName", err) diff --git a/routers/user/profile.go b/routers/user/profile.go index b0eab0933..86819de25 100644 --- a/routers/user/profile.go +++ b/routers/user/profile.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/repo" ) @@ -157,13 +158,14 @@ func Profile(ctx *context.Context) { } } else { repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ - Keyword: keyword, - OwnerID: ctxUser.ID, - OrderBy: orderBy, - Private: showPrivate, - Page: page, - PageSize: setting.UI.User.RepoPagingNum, - Starred: true, + Keyword: keyword, + OwnerID: ctxUser.ID, + OrderBy: orderBy, + Private: showPrivate, + Page: page, + PageSize: setting.UI.User.RepoPagingNum, + Starred: true, + Collaborate: util.OptionalBoolFalse, }) if err != nil { ctx.Handle(500, "SearchRepositoryByName", err) @@ -199,14 +201,13 @@ func Profile(ctx *context.Context) { ctx.Data["Total"] = total } else { repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ - Keyword: keyword, - OwnerID: ctxUser.ID, - OrderBy: orderBy, - Private: showPrivate, - Page: page, - IsProfile: true, - PageSize: setting.UI.User.RepoPagingNum, - Collaborate: true, + Keyword: keyword, + OwnerID: ctxUser.ID, + OrderBy: orderBy, + Private: showPrivate, + Page: page, + IsProfile: true, + PageSize: setting.UI.User.RepoPagingNum, }) if err != nil { ctx.Handle(500, "SearchRepositoryByName", err)