]> source.dussan.org Git - gitea.git/commitdiff
Add search mode option to /api/repo/search (#2756)
authorMorlinest <morlinest@gmail.com>
Thu, 26 Oct 2017 21:16:13 +0000 (23:16 +0200)
committerLauris BH <lauris@nix.lv>
Thu, 26 Oct 2017 21:16:13 +0000 (00:16 +0300)
* 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

16 files changed:
integrations/api_repo_test.go
models/fixtures/access.yml
models/fixtures/org_user.yml
models/fixtures/repository.yml
models/fixtures/team.yml
models/fixtures/team_repo.yml
models/fixtures/team_user.yml
models/fixtures/user.yml
models/issue_indexer.go
models/repo_list.go
models/repo_list_test.go
models/user_test.go
public/swagger.v1.json
routers/api/v1/repo/repo.go
routers/home.go
routers/user/profile.go

index f517ee42cd44ecde35650a10ff0bc5287810e065..b766dd584615a5bfb0aede8f94caf74bd8867d6f 100644 (file)
@@ -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)
 
index 9c149b78d037e15a76e259df889174b9147fc621..af2c8a52939cb266266fcad727f104914ad61e32 100644 (file)
   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
index 50d8ef5e683abcef06ad02783a40b17a3bbd5ea4..709a1997b9cb1a4ca8f079382389f660ad141914 100644 (file)
   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
index eb83dfcff794695e1020a1350631b143467c6a6a..91342d076e9e24226843bf4fba9c7f822a416104 100644 (file)
   num_closed_pulls: 0
   num_watches: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 18
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 19
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 20
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 21
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 22
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 23
   num_pulls: 0
   num_closed_pulls: 0
   is_mirror: false
+  is_fork: false
 
 -
   id: 24
   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
index 2b2186deaec711f23ec7a3002488f7801fa18546..1d242cb5bb005f826a03af758d0a474bd2843835 100644 (file)
@@ -37,6 +37,7 @@
   num_repos: 0
   num_members: 1
   unit_types: '[1,2,3,4,5,6,7]'
+
 -
   id: 5
   org_id: 17
   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
index 5154453f7b4dd3a96865d7b8aafd90b08ed36398..9e6d745539d225dc8fb4e4f1de38c67fd7156d47 100644 (file)
   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
index 56025bb0b767e096b621c6cbfc86aec3b89c3041..b1dfcdfdef8aa4c31efd03d31d8ce15c8b0664fc 100644 (file)
   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
index 1e0625598892c65819e6c30f7e42a152aa0da54f..60f5e8405ac17335011948207ea09f377634b63e 100644 (file)
   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
index 18c6f281b207d014a22b5039f20cca850426908e..e43dfc9fae19e3d08aef69bc531ba9f874067864 100644 (file)
@@ -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)
index 2c4c66ac3e9c3c0a265b1285f70bfbcc606741ce..883e3b98d5b717a1f1aefa869c16102fabc27bd6 100644 (file)
@@ -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
        }
index 4d125633a51c7f1bd0355984e06a79a0daba0925..3bccb1aebe72209e4a97459cb369191967474b54 100644 (file)
@@ -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)
+                                               }
+                                       }
                                }
                        }
                })
index 7ac9ebb0f5fb4601c3f8b172a09ad548fe40f578..03ab54aaf7b3a57afcc53f1c8ff16284d4d5a5a5 100644 (file)
@@ -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})
index e640b4e83160df8d3d8ba0179f312cf077bda8f2..c269fcb8640122cfb88a8f96189be141439d2c68 100644 (file)
             "type": "integer",
             "format": "int64",
             "x-go-name": "OwnerID",
-            "description": "Owner in we search search",
+            "description": "Repository owner to search",
             "name": "uid",
             "in": "query"
           },
             "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"
           }
index 30e1186c0a26771e8243e5ed01950dd8539f86ce..34f4c5fa161a217a1a9de4fda1298cb49cdd1ca4 100644 (file)
@@ -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.
index d653d1e8432e273a682200da9e103e7c7542c7c7..ce4e0be98d5fb23b9c176c9dbe43d78dc96d4b7b 100644 (file)
@@ -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)
index b0eab093337126adaf294c00930066713d4a03eb..86819de251e093601ea46cb4a7edbb9e0b565bb2 100644 (file)
@@ -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)