Browse Source

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
Morlinest 1 year ago
parent
commit
ddb7f59ef4

+ 51 - 6
integrations/api_repo_test.go

@@ -51,6 +51,7 @@ func TestAPISearchRepo(t *testing.T) {
51 51
 	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 15}).(*models.User)
52 52
 	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 16}).(*models.User)
53 53
 	user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 18}).(*models.User)
54
+	user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 20}).(*models.User)
54 55
 	orgUser := models.AssertExistsAndLoadBean(t, &models.User{ID: 17}).(*models.User)
55 56
 
56 57
 	// Map of expected results, where key is user for login
@@ -66,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) {
66 67
 		expectedResults
67 68
 	}{
68 69
 		{name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{
69
-			nil:   {count: 12},
70
-			user:  {count: 12},
71
-			user2: {count: 12}},
70
+			nil:   {count: 15},
71
+			user:  {count: 15},
72
+			user2: {count: 15}},
72 73
 		},
73 74
 		{name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{
74 75
 			nil:   {count: 10},
@@ -81,9 +82,9 @@ func TestAPISearchRepo(t *testing.T) {
81 82
 			user2: {count: 10}},
82 83
 		},
83 84
 		{name: "RepositoriesByName", requestURL: fmt.Sprintf("/api/v1/repos/search?q=%s", "big_test_"), expectedResults: expectedResults{
84
-			nil:   {count: 4, repoName: "big_test_"},
85
-			user:  {count: 4, repoName: "big_test_"},
86
-			user2: {count: 4, repoName: "big_test_"}},
85
+			nil:   {count: 7, repoName: "big_test_"},
86
+			user:  {count: 7, repoName: "big_test_"},
87
+			user2: {count: 7, repoName: "big_test_"}},
87 88
 		},
88 89
 		{name: "RepositoriesAccessibleAndRelatedToUser", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user.ID), expectedResults: expectedResults{
89 90
 			nil:   {count: 4},
@@ -106,6 +107,34 @@ func TestAPISearchRepo(t *testing.T) {
106 107
 			user:  {count: 2, repoOwnerID: orgUser.ID, includesPrivate: true},
107 108
 			user2: {count: 1, repoOwnerID: orgUser.ID}},
108 109
 		},
110
+		{name: "RepositoriesAccessibleAndRelatedToUser4", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user4.ID), expectedResults: expectedResults{
111
+			nil:   {count: 3},
112
+			user:  {count: 3},
113
+			user4: {count: 6, includesPrivate: true}}},
114
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeSource", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "source"), expectedResults: expectedResults{
115
+			nil:   {count: 0},
116
+			user:  {count: 0},
117
+			user4: {count: 0, includesPrivate: true}}},
118
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "fork"), expectedResults: expectedResults{
119
+			nil:   {count: 1},
120
+			user:  {count: 1},
121
+			user4: {count: 2, includesPrivate: true}}},
122
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeFork/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "fork"), expectedResults: expectedResults{
123
+			nil:   {count: 1},
124
+			user:  {count: 1},
125
+			user4: {count: 2, includesPrivate: true}}},
126
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "mirror"), expectedResults: expectedResults{
127
+			nil:   {count: 2},
128
+			user:  {count: 2},
129
+			user4: {count: 4, includesPrivate: true}}},
130
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeMirror/Exclusive", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s&exclusive=1", user4.ID, "mirror"), expectedResults: expectedResults{
131
+			nil:   {count: 1},
132
+			user:  {count: 1},
133
+			user4: {count: 2, includesPrivate: true}}},
134
+		{name: "RepositoriesAccessibleAndRelatedToUser4/SearchModeCollaborative", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d&mode=%s", user4.ID, "collaborative"), expectedResults: expectedResults{
135
+			nil:   {count: 0},
136
+			user:  {count: 0},
137
+			user4: {count: 0, includesPrivate: true}}},
109 138
 	}
110 139
 
111 140
 	for _, testCase := range testCases {
@@ -113,9 +142,11 @@ func TestAPISearchRepo(t *testing.T) {
113 142
 			for userToLogin, expected := range testCase.expectedResults {
114 143
 				var session *TestSession
115 144
 				var testName string
145
+				var userID int64
116 146
 				if userToLogin != nil && userToLogin.ID > 0 {
117 147
 					testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
118 148
 					session = loginUser(t, userToLogin.Name)
149
+					userID = userToLogin.ID
119 150
 				} else {
120 151
 					testName = "AnonymousUser"
121 152
 					session = emptyTestSession(t)
@@ -130,6 +161,11 @@ func TestAPISearchRepo(t *testing.T) {
130 161
 
131 162
 					assert.Len(t, body.Data, expected.count)
132 163
 					for _, repo := range body.Data {
164
+						r := getRepo(t, repo.ID)
165
+						hasAccess, err := models.HasAccess(userID, r, models.AccessModeRead)
166
+						assert.NoError(t, err)
167
+						assert.True(t, hasAccess)
168
+
133 169
 						assert.NotEmpty(t, repo.Name)
134 170
 
135 171
 						if len(expected.repoName) > 0 {
@@ -150,6 +186,15 @@ func TestAPISearchRepo(t *testing.T) {
150 186
 	}
151 187
 }
152 188
 
189
+var repoCache = make(map[int64]*models.Repository)
190
+
191
+func getRepo(t *testing.T, repoID int64) *models.Repository {
192
+	if _, ok := repoCache[repoID]; !ok {
193
+		repoCache[repoID] = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repoID}).(*models.Repository)
194
+	}
195
+	return repoCache[repoID]
196
+}
197
+
153 198
 func TestAPIViewRepo(t *testing.T) {
154 199
 	prepareTestEnv(t)
155 200
 

+ 13 - 1
models/fixtures/access.yml

@@ -62,4 +62,16 @@
62 62
   id: 11
63 63
   user_id: 18
64 64
   repo_id: 21
65
-  mode: 2 # write
65
+  mode: 2 # write
66
+
67
+-
68
+  id: 12
69
+  user_id: 20
70
+  repo_id: 27
71
+  mode: 4 # owner
72
+  
73
+-
74
+  id: 13
75
+  user_id: 20
76
+  repo_id: 28
77
+  mode: 4 # owner

+ 8 - 0
models/fixtures/org_user.yml

@@ -44,4 +44,12 @@
44 44
   org_id: 17
45 45
   is_public: false
46 46
   is_owner: true
47
+  num_teams: 1
48
+
49
+-
50
+  id: 7
51
+  uid: 20
52
+  org_id: 19
53
+  is_public: true
54
+  is_owner: true
47 55
   num_teams: 1

+ 94 - 0
models/fixtures/repository.yml

@@ -201,6 +201,7 @@
201 201
   num_closed_pulls: 0
202 202
   num_watches: 0
203 203
   is_mirror: false
204
+  is_fork: false
204 205
 
205 206
 -
206 207
   id: 18
@@ -213,6 +214,7 @@
213 214
   num_pulls: 0
214 215
   num_closed_pulls: 0
215 216
   is_mirror: false
217
+  is_fork: false
216 218
 
217 219
 -
218 220
   id: 19
@@ -225,6 +227,7 @@
225 227
   num_pulls: 0
226 228
   num_closed_pulls: 0
227 229
   is_mirror: false
230
+  is_fork: false
228 231
 
229 232
 -
230 233
   id: 20
@@ -237,6 +240,7 @@
237 240
   num_pulls: 0
238 241
   num_closed_pulls: 0
239 242
   is_mirror: false
243
+  is_fork: false
240 244
 
241 245
 -
242 246
   id: 21
@@ -249,6 +253,7 @@
249 253
   num_pulls: 0
250 254
   num_closed_pulls: 0
251 255
   is_mirror: false
256
+  is_fork: false
252 257
 
253 258
 -
254 259
   id: 22
@@ -261,6 +266,7 @@
261 266
   num_pulls: 0
262 267
   num_closed_pulls: 0
263 268
   is_mirror: false
269
+  is_fork: false
264 270
 
265 271
 -
266 272
   id: 23
@@ -273,6 +279,7 @@
273 279
   num_pulls: 0
274 280
   num_closed_pulls: 0
275 281
   is_mirror: false
282
+  is_fork: false
276 283
 
277 284
 -
278 285
   id: 24
@@ -285,3 +292,90 @@
285 292
   num_pulls: 0
286 293
   num_closed_pulls: 0
287 294
   is_mirror: false
295
+  is_fork: false
296
+
297
+-
298
+  id: 25
299
+  owner_id: 20
300
+  lower_name: big_test_public_mirror_5
301
+  name: big_test_public_mirror_5
302
+  is_private: false
303
+  num_issues: 0
304
+  num_closed_issues: 0
305
+  num_pulls: 0
306
+  num_closed_pulls: 0
307
+  num_watches: 0
308
+  is_mirror: true
309
+  is_fork: false
310
+
311
+-
312
+  id: 26
313
+  owner_id: 20
314
+  lower_name: big_test_private_mirror_5
315
+  name: big_test_private_mirror_5
316
+  is_private: true
317
+  num_issues: 0
318
+  num_closed_issues: 0
319
+  num_pulls: 0
320
+  num_closed_pulls: 0
321
+  num_watches: 0
322
+  is_mirror: true
323
+  is_fork: false
324
+
325
+-
326
+  id: 27
327
+  owner_id: 19
328
+  lower_name: big_test_public_mirror_6
329
+  name: big_test_public_mirror_6
330
+  is_private: false
331
+  num_issues: 0
332
+  num_closed_issues: 0
333
+  num_pulls: 0
334
+  num_closed_pulls: 0
335
+  num_watches: 0
336
+  is_mirror: true
337
+  num_forks: 1
338
+  is_fork: false
339
+
340
+-
341
+  id: 28
342
+  owner_id: 19
343
+  lower_name: big_test_private_mirror_6
344
+  name: big_test_private_mirror_6
345
+  is_private: true
346
+  num_issues: 0
347
+  num_closed_issues: 0
348
+  num_pulls: 0
349
+  num_closed_pulls: 0
350
+  num_watches: 0
351
+  is_mirror: true
352
+  num_forks: 1
353
+  is_fork: false
354
+  
355
+-
356
+  id: 29
357
+  fork_id: 27
358
+  owner_id: 20
359
+  lower_name: big_test_public_fork_7
360
+  name: big_test_public_fork_7
361
+  is_private: false
362
+  num_issues: 0
363
+  num_closed_issues: 0
364
+  num_pulls: 0
365
+  num_closed_pulls: 0
366
+  is_mirror: false
367
+  is_fork: true
368
+  
369
+-
370
+  id: 30
371
+  fork_id: 28
372
+  owner_id: 20
373
+  lower_name: big_test_private_fork_7
374
+  name: big_test_private_fork_7
375
+  is_private: true
376
+  num_issues: 0
377
+  num_closed_issues: 0
378
+  num_pulls: 0
379
+  num_closed_pulls: 0
380
+  is_mirror: false
381
+  is_fork: true

+ 11 - 0
models/fixtures/team.yml

@@ -37,6 +37,7 @@
37 37
   num_repos: 0
38 38
   num_members: 1
39 39
   unit_types: '[1,2,3,4,5,6,7]'
40
+
40 41
 -
41 42
   id: 5
42 43
   org_id: 17
@@ -45,4 +46,14 @@
45 46
   authorize: 4 # owner
46 47
   num_repos: 2
47 48
   num_members: 2
49
+  unit_types: '[1,2,3,4,5,6,7]'
50
+
51
+-
52
+  id: 6
53
+  org_id: 19
54
+  lower_name: owners
55
+  name: Owners
56
+  authorize: 4 # owner
57
+  num_repos: 2
58
+  num_members: 1
48 59
   unit_types: '[1,2,3,4,5,6,7]'

+ 13 - 1
models/fixtures/team_repo.yml

@@ -26,4 +26,16 @@
26 26
   id: 5
27 27
   org_id: 17
28 28
   team_id: 5
29
-  repo_id: 24
29
+  repo_id: 24
30
+
31
+-
32
+  id: 6
33
+  org_id: 19
34
+  team_id: 6
35
+  repo_id: 27
36
+  
37
+-
38
+  id: 7
39
+  org_id: 19
40
+  team_id: 6
41
+  repo_id: 28

+ 7 - 1
models/fixtures/team_user.yml

@@ -38,4 +38,10 @@
38 38
   id: 7
39 39
   org_id: 17
40 40
   team_id: 5
41
-  uid: 18
41
+  uid: 18
42
+
43
+-
44
+  id: 8
45
+  org_id: 19
46
+  team_id: 6
47
+  uid: 20

+ 32 - 0
models/fixtures/user.yml

@@ -281,4 +281,36 @@
281 281
   avatar: avatar18
282 282
   avatar_email: user18@example.com
283 283
   num_repos: 0
284
+  is_active: true
285
+
286
+-
287
+  id: 19
288
+  lower_name: user19
289
+  name: user19
290
+  full_name: User 19
291
+  email: user19@example.com
292
+  passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
293
+  type: 1 # organization
294
+  salt: ZogKvWdyEx
295
+  is_admin: false
296
+  avatar: avatar19
297
+  avatar_email: user19@example.com
298
+  num_repos: 2
299
+  is_active: true
300
+  num_members: 1
301
+  num_teams: 1
302
+
303
+-
304
+  id: 20
305
+  lower_name: user20
306
+  name: user20
307
+  full_name: User 20
308
+  email: user20@example.com
309
+  passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
310
+  type: 0 # individual
311
+  salt: ZogKvWdyEx
312
+  is_admin: false
313
+  avatar: avatar20
314
+  avatar_email: user20@example.com
315
+  num_repos: 4
284 316
   is_active: true

+ 5 - 4
models/issue_indexer.go

@@ -28,10 +28,11 @@ func populateIssueIndexer() error {
28 28
 	batch := indexer.IssueIndexerBatch()
29 29
 	for page := 1; ; page++ {
30 30
 		repos, _, err := SearchRepositoryByName(&SearchRepoOptions{
31
-			Page:     page,
32
-			PageSize: 10,
33
-			OrderBy:  SearchOrderByID,
34
-			Private:  true,
31
+			Page:        page,
32
+			PageSize:    10,
33
+			OrderBy:     SearchOrderByID,
34
+			Private:     true,
35
+			Collaborate: util.OptionalBoolFalse,
35 36
 		})
36 37
 		if err != nil {
37 38
 			return fmt.Errorf("Repositories: %v", err)

+ 42 - 31
models/repo_list.go

@@ -8,6 +8,8 @@ import (
8 8
 	"fmt"
9 9
 	"strings"
10 10
 
11
+	"code.gitea.io/gitea/modules/util"
12
+
11 13
 	"github.com/go-xorm/builder"
12 14
 )
13 15
 
@@ -88,28 +90,28 @@ func (repos MirrorRepositoryList) LoadAttributes() error {
88 90
 }
89 91
 
90 92
 // SearchRepoOptions holds the search options
91
-// swagger:parameters repoSearch
92 93
 type SearchRepoOptions struct {
93
-	// Keyword to search
94
-	//
95
-	// in: query
96
-	Keyword string `json:"q"`
97
-	// Owner in we search search
98
-	//
99
-	// in: query
100
-	OwnerID     int64         `json:"uid"`
101
-	OrderBy     SearchOrderBy `json:"-"`
102
-	Private     bool          `json:"-"` // Include private repositories in results
103
-	Collaborate bool          `json:"-"` // Include collaborative repositories
104
-	Starred     bool          `json:"-"`
105
-	Page        int           `json:"-"`
106
-	IsProfile   bool          `json:"-"`
107
-	AllPublic   bool          `json:"-"` // Include also all public repositories
108
-	// Limit of result
109
-	//
110
-	// maximum: setting.ExplorePagingNum
111
-	// in: query
112
-	PageSize int `json:"limit"` // Can be smaller than or equal to setting.ExplorePagingNum
94
+	Keyword   string
95
+	OwnerID   int64
96
+	OrderBy   SearchOrderBy
97
+	Private   bool // Include private repositories in results
98
+	Starred   bool
99
+	Page      int
100
+	IsProfile bool
101
+	AllPublic bool // Include also all public repositories
102
+	PageSize  int  // Can be smaller than or equal to setting.ExplorePagingNum
103
+	// None -> include collaborative AND non-collaborative
104
+	// True -> include just collaborative
105
+	// False -> incude just non-collaborative
106
+	Collaborate util.OptionalBool
107
+	// None -> include forks AND non-forks
108
+	// True -> include just forks
109
+	// False -> include just non-forks
110
+	Fork util.OptionalBool
111
+	// None -> include mirrors AND non-mirrors
112
+	// True -> include just mirrors
113
+	// False -> include just non-mirrors
114
+	Mirror util.OptionalBool
113 115
 }
114 116
 
115 117
 //SearchOrderBy is used to sort the result
@@ -146,17 +148,18 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
146 148
 		cond = cond.And(builder.Eq{"is_private": false})
147 149
 	}
148 150
 
149
-	starred := false
151
+	var starred bool
150 152
 	if opts.OwnerID > 0 {
151 153
 		if opts.Starred {
152 154
 			starred = true
153
-			cond = builder.Eq{
154
-				"star.uid": opts.OwnerID,
155
-			}
155
+			cond = builder.Eq{"star.uid": opts.OwnerID}
156 156
 		} else {
157
-			var accessCond builder.Cond = builder.Eq{"owner_id": opts.OwnerID}
157
+			var accessCond = builder.NewCond()
158
+			if opts.Collaborate != util.OptionalBoolTrue {
159
+				accessCond = builder.Eq{"owner_id": opts.OwnerID}
160
+			}
158 161
 
159
-			if opts.Collaborate {
162
+			if opts.Collaborate != util.OptionalBoolFalse {
160 163
 				collaborateCond := builder.And(
161 164
 					builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID),
162 165
 					builder.Neq{"owner_id": opts.OwnerID})
@@ -167,18 +170,26 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
167 170
 				accessCond = accessCond.Or(collaborateCond)
168 171
 			}
169 172
 
173
+			if opts.AllPublic {
174
+				accessCond = accessCond.Or(builder.Eq{"is_private": false})
175
+			}
176
+
170 177
 			cond = cond.And(accessCond)
171 178
 		}
172 179
 	}
173 180
 
174
-	if opts.OwnerID > 0 && opts.AllPublic {
175
-		cond = cond.Or(builder.Eq{"is_private": false})
176
-	}
177
-
178 181
 	if opts.Keyword != "" {
179 182
 		cond = cond.And(builder.Like{"lower_name", strings.ToLower(opts.Keyword)})
180 183
 	}
181 184
 
185
+	if opts.Fork != util.OptionalBoolNone {
186
+		cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue})
187
+	}
188
+
189
+	if opts.Mirror != util.OptionalBoolNone {
190
+		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
191
+	}
192
+
182 193
 	if len(opts.OrderBy) == 0 {
183 194
 		opts.OrderBy = SearchOrderByAlphabetically
184 195
 	}

+ 123 - 65
models/repo_list_test.go

@@ -7,6 +7,8 @@ package models
7 7
 import (
8 8
 	"testing"
9 9
 
10
+	"code.gitea.io/gitea/modules/util"
11
+
10 12
 	"github.com/stretchr/testify/assert"
11 13
 )
12 14
 
@@ -15,9 +17,10 @@ func TestSearchRepositoryByName(t *testing.T) {
15 17
 
16 18
 	// test search public repository on explore page
17 19
 	repos, count, err := SearchRepositoryByName(&SearchRepoOptions{
18
-		Keyword:  "repo_12",
19
-		Page:     1,
20
-		PageSize: 10,
20
+		Keyword:     "repo_12",
21
+		Page:        1,
22
+		PageSize:    10,
23
+		Collaborate: util.OptionalBoolFalse,
21 24
 	})
22 25
 
23 26
 	assert.NoError(t, err)
@@ -27,9 +30,10 @@ func TestSearchRepositoryByName(t *testing.T) {
27 30
 	assert.Equal(t, int64(1), count)
28 31
 
29 32
 	repos, count, err = SearchRepositoryByName(&SearchRepoOptions{
30
-		Keyword:  "test_repo",
31
-		Page:     1,
32
-		PageSize: 10,
33
+		Keyword:     "test_repo",
34
+		Page:        1,
35
+		PageSize:    10,
36
+		Collaborate: util.OptionalBoolFalse,
33 37
 	})
34 38
 
35 39
 	assert.NoError(t, err)
@@ -38,10 +42,11 @@ func TestSearchRepositoryByName(t *testing.T) {
38 42
 
39 43
 	// test search private repository on explore page
40 44
 	repos, count, err = SearchRepositoryByName(&SearchRepoOptions{
41
-		Keyword:  "repo_13",
42
-		Page:     1,
43
-		PageSize: 10,
44
-		Private:  true,
45
+		Keyword:     "repo_13",
46
+		Page:        1,
47
+		PageSize:    10,
48
+		Private:     true,
49
+		Collaborate: util.OptionalBoolFalse,
45 50
 	})
46 51
 
47 52
 	assert.NoError(t, err)
@@ -51,84 +56,110 @@ func TestSearchRepositoryByName(t *testing.T) {
51 56
 	assert.Equal(t, int64(1), count)
52 57
 
53 58
 	repos, count, err = SearchRepositoryByName(&SearchRepoOptions{
54
-		Keyword:  "test_repo",
55
-		Page:     1,
56
-		PageSize: 10,
57
-		Private:  true,
59
+		Keyword:     "test_repo",
60
+		Page:        1,
61
+		PageSize:    10,
62
+		Private:     true,
63
+		Collaborate: util.OptionalBoolFalse,
58 64
 	})
59 65
 
60 66
 	assert.NoError(t, err)
61 67
 	assert.Equal(t, int64(3), count)
62 68
 	assert.Len(t, repos, 3)
63 69
 
70
+	// Test non existing owner
71
+	repos, count, err = SearchRepositoryByName(&SearchRepoOptions{OwnerID: NonexistentID})
72
+
73
+	assert.NoError(t, err)
74
+	assert.Empty(t, repos)
75
+	assert.Equal(t, int64(0), count)
76
+
64 77
 	testCases := []struct {
65 78
 		name  string
66 79
 		opts  *SearchRepoOptions
67 80
 		count int
68 81
 	}{
69 82
 		{name: "PublicRepositoriesByName",
70
-			opts:  &SearchRepoOptions{Keyword: "big_test_", PageSize: 10},
71
-			count: 4},
83
+			opts:  &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, Collaborate: util.OptionalBoolFalse},
84
+			count: 7},
72 85
 		{name: "PublicAndPrivateRepositoriesByName",
73
-			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true},
74
-			count: 8},
86
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, Collaborate: util.OptionalBoolFalse},
87
+			count: 14},
75 88
 		{name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage",
76
-			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 5, Private: true},
77
-			count: 8},
89
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse},
90
+			count: 14},
78 91
 		{name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage",
79
-			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 2, PageSize: 5, Private: true},
80
-			count: 8},
92
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 2, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse},
93
+			count: 14},
94
+		{name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage",
95
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 3, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse},
96
+			count: 14},
97
+		{name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage",
98
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 3, PageSize: 5, Private: true, Collaborate: util.OptionalBoolFalse},
99
+			count: 14},
81 100
 		{name: "PublicRepositoriesOfUser",
82
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15},
101
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: util.OptionalBoolFalse},
83 102
 			count: 2},
84 103
 		{name: "PublicRepositoriesOfUser2",
85
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18},
104
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Collaborate: util.OptionalBoolFalse},
86 105
 			count: 0},
106
+		{name: "PublicRepositoriesOfUser3",
107
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Collaborate: util.OptionalBoolFalse},
108
+			count: 2},
87 109
 		{name: "PublicAndPrivateRepositoriesOfUser",
88
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true},
110
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse},
89 111
 			count: 4},
90 112
 		{name: "PublicAndPrivateRepositoriesOfUser2",
91
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true},
113
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse},
92 114
 			count: 0},
115
+		{name: "PublicAndPrivateRepositoriesOfUser3",
116
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse},
117
+			count: 4},
93 118
 		{name: "PublicRepositoriesOfUserIncludingCollaborative",
94
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: true},
119
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15},
95 120
 			count: 4},
96 121
 		{name: "PublicRepositoriesOfUser2IncludingCollaborative",
97
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Collaborate: true},
122
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18},
98 123
 			count: 1},
124
+		{name: "PublicRepositoriesOfUser3IncludingCollaborative",
125
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20},
126
+			count: 3},
99 127
 		{name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
100
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true},
128
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true},
101 129
 			count: 8},
102 130
 		{name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative",
103
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: true},
131
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 18, Private: true},
104 132
 			count: 4},
133
+		{name: "PublicAndPrivateRepositoriesOfUser3IncludingCollaborative",
134
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 20, Private: true},
135
+			count: 6},
105 136
 		{name: "PublicRepositoriesOfOrganization",
106
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17},
137
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Collaborate: util.OptionalBoolFalse},
107 138
 			count: 1},
108 139
 		{name: "PublicAndPrivateRepositoriesOfOrganization",
109
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Private: true},
140
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse},
110 141
 			count: 2},
111 142
 		{name: "AllPublic/PublicRepositoriesByName",
112
-			opts:  &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, AllPublic: true},
113
-			count: 4},
143
+			opts:  &SearchRepoOptions{Keyword: "big_test_", PageSize: 10, AllPublic: true, Collaborate: util.OptionalBoolFalse},
144
+			count: 7},
114 145
 		{name: "AllPublic/PublicAndPrivateRepositoriesByName",
115
-			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, AllPublic: true},
116
-			count: 8},
146
+			opts:  &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse},
147
+			count: 14},
117 148
 		{name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
118
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Collaborate: true, AllPublic: true},
119
-			count: 12},
149
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true},
150
+			count: 15},
120 151
 		{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
121
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true, AllPublic: true},
122
-			count: 16},
152
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
153
+			count: 19},
123 154
 		{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
124
-			opts:  &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, Collaborate: true, AllPublic: true},
125
-			count: 10},
155
+			opts:  &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
156
+			count: 13},
126 157
 		{name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName",
127
-			opts:  &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 18, Private: true, Collaborate: true, AllPublic: true},
128
-			count: 8},
158
+			opts:  &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 18, Private: true, AllPublic: true},
159
+			count: 11},
129 160
 		{name: "AllPublic/PublicRepositoriesOfOrganization",
130
-			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true},
131
-			count: 12},
161
+			opts:  &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse},
162
+			count: 15},
132 163
 	}
133 164
 
134 165
 	for _, testCase := range testCases {
@@ -138,27 +169,54 @@ func TestSearchRepositoryByName(t *testing.T) {
138 169
 			assert.NoError(t, err)
139 170
 			assert.Equal(t, int64(testCase.count), count)
140 171
 
141
-			var expectedLen int
142
-			if testCase.opts.PageSize*testCase.opts.Page > testCase.count {
172
+			page := testCase.opts.Page
173
+			if page <= 0 {
174
+				page = 1
175
+			}
176
+			var expectedLen = testCase.opts.PageSize
177
+			if testCase.opts.PageSize*page > testCase.count+testCase.opts.PageSize {
178
+				expectedLen = 0
179
+			} else if testCase.opts.PageSize*page > testCase.count {
143 180
 				expectedLen = testCase.count % testCase.opts.PageSize
144
-			} else {
145
-				expectedLen = testCase.opts.PageSize
146 181
 			}
147
-			assert.Len(t, repos, expectedLen)
148
-
149
-			for _, repo := range repos {
150
-				assert.NotEmpty(t, repo.Name)
151
-
152
-				if len(testCase.opts.Keyword) > 0 {
153
-					assert.Contains(t, repo.Name, testCase.opts.Keyword)
154
-				}
155
-
156
-				if testCase.opts.OwnerID > 0 && !testCase.opts.Collaborate && !testCase.opts.AllPublic {
157
-					assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
158
-				}
159
-
160
-				if !testCase.opts.Private {
161
-					assert.False(t, repo.IsPrivate)
182
+			if assert.Len(t, repos, expectedLen) {
183
+				for _, repo := range repos {
184
+					assert.NotEmpty(t, repo.Name)
185
+
186
+					if len(testCase.opts.Keyword) > 0 {
187
+						assert.Contains(t, repo.Name, testCase.opts.Keyword)
188
+					}
189
+
190
+					if !testCase.opts.Private {
191
+						assert.False(t, repo.IsPrivate)
192
+					}
193
+
194
+					if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue {
195
+						assert.True(t, repo.IsFork || repo.IsMirror)
196
+					} else {
197
+						switch testCase.opts.Fork {
198
+						case util.OptionalBoolFalse:
199
+							assert.False(t, repo.IsFork)
200
+						case util.OptionalBoolTrue:
201
+							assert.True(t, repo.IsFork)
202
+						}
203
+
204
+						switch testCase.opts.Mirror {
205
+						case util.OptionalBoolFalse:
206
+							assert.False(t, repo.IsMirror)
207
+						case util.OptionalBoolTrue:
208
+							assert.True(t, repo.IsMirror)
209
+						}
210
+					}
211
+
212
+					if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic {
213
+						switch testCase.opts.Collaborate {
214
+						case util.OptionalBoolFalse:
215
+							assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID)
216
+						case util.OptionalBoolTrue:
217
+							assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID)
218
+						}
219
+					}
162 220
 				}
163 221
 			}
164 222
 		})

+ 6 - 3
models/user_test.go

@@ -63,7 +63,10 @@ func TestSearchUsers(t *testing.T) {
63 63
 	testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 2, PageSize: 2},
64 64
 		[]int64{7, 17})
65 65
 
66
-	testOrgSuccess(&SearchUserOptions{Page: 3, PageSize: 2},
66
+	testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2},
67
+		[]int64{19})
68
+
69
+	testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2},
67 70
 		[]int64{})
68 71
 
69 72
 	// test users
@@ -73,13 +76,13 @@ func TestSearchUsers(t *testing.T) {
73 76
 	}
74 77
 
75 78
 	testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1},
76
-		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18})
79
+		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20})
77 80
 
78 81
 	testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
79 82
 		[]int64{9})
80 83
 
81 84
 	testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
82
-		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18})
85
+		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20})
83 86
 
84 87
 	testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
85 88
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})

+ 18 - 1
public/swagger.v1.json

@@ -1102,7 +1102,7 @@
1102 1102
             "type": "integer",
1103 1103
             "format": "int64",
1104 1104
             "x-go-name": "OwnerID",
1105
-            "description": "Owner in we search search",
1105
+            "description": "Repository owner to search",
1106 1106
             "name": "uid",
1107 1107
             "in": "query"
1108 1108
           },
@@ -1113,12 +1113,29 @@
1113 1113
             "description": "Limit of result\n\nmaximum: setting.ExplorePagingNum",
1114 1114
             "name": "limit",
1115 1115
             "in": "query"
1116
+          },
1117
+          {
1118
+            "type": "string",
1119
+            "x-go-name": "SearchMode",
1120
+            "description": "Type of repository to search, related to owner",
1121
+            "name": "mode",
1122
+            "in": "query"
1123
+          },
1124
+          {
1125
+            "type": "boolean",
1126
+            "x-go-name": "OwnerExclusive",
1127
+            "description": "Search only owners repositories\nHas effect only if owner is provided and mode is not \"collaborative\"",
1128
+            "name": "exclusive",
1129
+            "in": "query"
1116 1130
           }
1117 1131
         ],
1118 1132
         "responses": {
1119 1133
           "200": {
1120 1134
             "$ref": "#/responses/SearchResults"
1121 1135
           },
1136
+          "422": {
1137
+            "$ref": "#/responses/validationError"
1138
+          },
1122 1139
           "500": {
1123 1140
             "$ref": "#/responses/SearchError"
1124 1141
           }

+ 59 - 6
routers/api/v1/repo/repo.go

@@ -6,6 +6,7 @@ package repo
6 6
 
7 7
 import (
8 8
 	"fmt"
9
+	"net/http"
9 10
 	"strings"
10 11
 
11 12
 	api "code.gitea.io/sdk/gitea"
@@ -15,9 +16,37 @@ import (
15 16
 	"code.gitea.io/gitea/modules/context"
16 17
 	"code.gitea.io/gitea/modules/log"
17 18
 	"code.gitea.io/gitea/modules/setting"
19
+	"code.gitea.io/gitea/modules/util"
18 20
 	"code.gitea.io/gitea/routers/api/v1/convert"
19 21
 )
20 22
 
23
+// SearchRepoOption options when searching repositories
24
+// swagger:parameters repoSearch
25
+type SearchRepoOption struct { // TODO: Move SearchRepoOption to Gitea SDK
26
+	// Keyword to search
27
+	//
28
+	// in: query
29
+	Keyword string `json:"q"`
30
+	// Repository owner to search
31
+	//
32
+	// in: query
33
+	OwnerID int64 `json:"uid"`
34
+	// Limit of result
35
+	//
36
+	// maximum: setting.ExplorePagingNum
37
+	// in: query
38
+	PageSize int `json:"limit"`
39
+	// Type of repository to search, related to owner
40
+	//
41
+	// in: query
42
+	SearchMode string `json:"mode"`
43
+	// Search only owners repositories
44
+	// Has effect only if owner is provided and mode is not "collaborative"
45
+	//
46
+	// in: query
47
+	OwnerExclusive bool `json:"exclusive"`
48
+}
49
+
21 50
 // Search repositories via options
22 51
 func Search(ctx *context.APIContext) {
23 52
 	// swagger:route GET /repos/search repository repoSearch
@@ -27,20 +56,44 @@ func Search(ctx *context.APIContext) {
27 56
 	//
28 57
 	//     Responses:
29 58
 	//       200: SearchResults
59
+	//       422: validationError
30 60
 	//       500: SearchError
31 61
 
32 62
 	opts := &models.SearchRepoOptions{
33
-		Keyword:  strings.Trim(ctx.Query("q"), " "),
34
-		OwnerID:  ctx.QueryInt64("uid"),
35
-		PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")),
63
+		Keyword:     strings.Trim(ctx.Query("q"), " "),
64
+		OwnerID:     ctx.QueryInt64("uid"),
65
+		PageSize:    convert.ToCorrectPageSize(ctx.QueryInt("limit")),
66
+		Collaborate: util.OptionalBoolNone,
67
+	}
68
+
69
+	if ctx.QueryBool("exclusive") {
70
+		opts.Collaborate = util.OptionalBoolFalse
71
+	}
72
+
73
+	var mode = ctx.Query("mode")
74
+	switch mode {
75
+	case "source":
76
+		opts.Fork = util.OptionalBoolFalse
77
+		opts.Mirror = util.OptionalBoolFalse
78
+	case "fork":
79
+		opts.Fork = util.OptionalBoolTrue
80
+	case "mirror":
81
+		opts.Mirror = util.OptionalBoolTrue
82
+	case "collaborative":
83
+		opts.Mirror = util.OptionalBoolFalse
84
+		opts.Collaborate = util.OptionalBoolTrue
85
+	case "":
86
+	default:
87
+		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
88
+		return
36 89
 	}
37 90
 
91
+	var err error
38 92
 	if opts.OwnerID > 0 {
39 93
 		var repoOwner *models.User
40 94
 		if ctx.User != nil && ctx.User.ID == opts.OwnerID {
41 95
 			repoOwner = ctx.User
42 96
 		} else {
43
-			var err error
44 97
 			repoOwner, err = models.GetUserByID(opts.OwnerID)
45 98
 			if err != nil {
46 99
 				ctx.JSON(500, api.SearchError{
@@ -51,8 +104,8 @@ func Search(ctx *context.APIContext) {
51 104
 			}
52 105
 		}
53 106
 
54
-		if !repoOwner.IsOrganization() {
55
-			opts.Collaborate = true
107
+		if repoOwner.IsOrganization() {
108
+			opts.Collaborate = util.OptionalBoolFalse
56 109
 		}
57 110
 
58 111
 		// Check visibility.

+ 7 - 8
routers/home.go

@@ -108,14 +108,13 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
108 108
 	keyword := strings.Trim(ctx.Query("q"), " ")
109 109
 
110 110
 	repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
111
-		Page:        page,
112
-		PageSize:    opts.PageSize,
113
-		OrderBy:     orderBy,
114
-		Private:     opts.Private,
115
-		Keyword:     keyword,
116
-		OwnerID:     opts.OwnerID,
117
-		Collaborate: true,
118
-		AllPublic:   true,
111
+		Page:      page,
112
+		PageSize:  opts.PageSize,
113
+		OrderBy:   orderBy,
114
+		Private:   opts.Private,
115
+		Keyword:   keyword,
116
+		OwnerID:   opts.OwnerID,
117
+		AllPublic: true,
119 118
 	})
120 119
 	if err != nil {
121 120
 		ctx.Handle(500, "SearchRepositoryByName", err)

+ 16 - 15
routers/user/profile.go

@@ -15,6 +15,7 @@ import (
15 15
 	"code.gitea.io/gitea/modules/base"
16 16
 	"code.gitea.io/gitea/modules/context"
17 17
 	"code.gitea.io/gitea/modules/setting"
18
+	"code.gitea.io/gitea/modules/util"
18 19
 	"code.gitea.io/gitea/routers/repo"
19 20
 )
20 21
 
@@ -157,13 +158,14 @@ func Profile(ctx *context.Context) {
157 158
 			}
158 159
 		} else {
159 160
 			repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
160
-				Keyword:  keyword,
161
-				OwnerID:  ctxUser.ID,
162
-				OrderBy:  orderBy,
163
-				Private:  showPrivate,
164
-				Page:     page,
165
-				PageSize: setting.UI.User.RepoPagingNum,
166
-				Starred:  true,
161
+				Keyword:     keyword,
162
+				OwnerID:     ctxUser.ID,
163
+				OrderBy:     orderBy,
164
+				Private:     showPrivate,
165
+				Page:        page,
166
+				PageSize:    setting.UI.User.RepoPagingNum,
167
+				Starred:     true,
168
+				Collaborate: util.OptionalBoolFalse,
167 169
 			})
168 170
 			if err != nil {
169 171
 				ctx.Handle(500, "SearchRepositoryByName", err)
@@ -199,14 +201,13 @@ func Profile(ctx *context.Context) {
199 201
 			ctx.Data["Total"] = total
200 202
 		} else {
201 203
 			repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{
202
-				Keyword:     keyword,
203
-				OwnerID:     ctxUser.ID,
204
-				OrderBy:     orderBy,
205
-				Private:     showPrivate,
206
-				Page:        page,
207
-				IsProfile:   true,
208
-				PageSize:    setting.UI.User.RepoPagingNum,
209
-				Collaborate: true,
204
+				Keyword:   keyword,
205
+				OwnerID:   ctxUser.ID,
206
+				OrderBy:   orderBy,
207
+				Private:   showPrivate,
208
+				Page:      page,
209
+				IsProfile: true,
210
+				PageSize:  setting.UI.User.RepoPagingNum,
210 211
 			})
211 212
 			if err != nil {
212 213
 				ctx.Handle(500, "SearchRepositoryByName", err)