##// END OF EJS Templates
api: get_repos allows now to filter by root locations, and optionally traverse the returned data....
marcink -
r1267:b965be9c default
parent child Browse files
Show More
@@ -1,58 +1,129 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24 from rhodecode.model.repo import RepoModel
25 from rhodecode.api.tests.utils import build_data, api_call, assert_ok, jsonify
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_ok, assert_error, jsonify)
26 27 from rhodecode.model.db import User
27 28
28 29
29 30 @pytest.mark.usefixtures("testuser_api", "app")
30 31 class TestGetRepos(object):
31 32 def test_api_get_repos(self):
32 33 id_, params = build_data(self.apikey, 'get_repos')
33 34 response = api_call(self.app, params)
34 35
35 36 result = []
36 37 for repo in RepoModel().get_all():
37 38 result.append(repo.get_api_data(include_secrets=True))
38 39 ret = jsonify(result)
39 40
40 41 expected = ret
41 42 assert_ok(id_, expected, given=response.body)
42 43
44 def test_api_get_repos_only_toplevel(self, user_util):
45 repo_group = user_util.create_repo_group(auto_cleanup=True)
46 user_util.create_repo(parent=repo_group)
47
48 id_, params = build_data(self.apikey, 'get_repos', traverse=0)
49 response = api_call(self.app, params)
50
51 result = []
52 for repo in RepoModel().get_repos_for_root(root=None):
53 result.append(repo.get_api_data(include_secrets=True))
54 expected = jsonify(result)
55
56 assert_ok(id_, expected, given=response.body)
57
58 def test_api_get_repos_with_wrong_root(self):
59 id_, params = build_data(self.apikey, 'get_repos', root='abracadabra')
60 response = api_call(self.app, params)
61
62 expected = 'Root repository group `abracadabra` does not exist'
63 assert_error(id_, expected, given=response.body)
64
65 def test_api_get_repos_with_root(self, user_util):
66 repo_group = user_util.create_repo_group(auto_cleanup=True)
67 repo_group_name = repo_group.group_name
68
69 user_util.create_repo(parent=repo_group)
70 user_util.create_repo(parent=repo_group)
71
72 # nested, should not show up
73 user_util._test_name = '{}/'.format(repo_group_name)
74 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
75 user_util.create_repo(parent=sub_repo_group)
76
77 id_, params = build_data(self.apikey, 'get_repos',
78 root=repo_group_name, traverse=0)
79 response = api_call(self.app, params)
80
81 result = []
82 for repo in RepoModel().get_repos_for_root(repo_group):
83 result.append(repo.get_api_data(include_secrets=True))
84
85 assert len(result) == 2
86 expected = jsonify(result)
87 assert_ok(id_, expected, given=response.body)
88
89 def test_api_get_repos_with_root_and_traverse(self, user_util):
90 repo_group = user_util.create_repo_group(auto_cleanup=True)
91 repo_group_name = repo_group.group_name
92
93 user_util.create_repo(parent=repo_group)
94 user_util.create_repo(parent=repo_group)
95
96 # nested, should not show up
97 user_util._test_name = '{}/'.format(repo_group_name)
98 sub_repo_group = user_util.create_repo_group(auto_cleanup=True)
99 user_util.create_repo(parent=sub_repo_group)
100
101 id_, params = build_data(self.apikey, 'get_repos',
102 root=repo_group_name, traverse=1)
103 response = api_call(self.app, params)
104
105 result = []
106 for repo in RepoModel().get_repos_for_root(
107 repo_group_name, traverse=True):
108 result.append(repo.get_api_data(include_secrets=True))
109
110 assert len(result) == 3
111 expected = jsonify(result)
112 assert_ok(id_, expected, given=response.body)
113
43 114 def test_api_get_repos_non_admin(self):
44 115 id_, params = build_data(self.apikey_regular, 'get_repos')
45 116 response = api_call(self.app, params)
46 117
47 118 user = User.get_by_username(self.TEST_USER_LOGIN)
48 119 allowed_repos = user.AuthUser.permissions['repositories']
49 120
50 121 result = []
51 122 for repo in RepoModel().get_all():
52 123 perm = allowed_repos[repo.repo_name]
53 124 if perm in ['repository.read', 'repository.write', 'repository.admin']:
54 125 result.append(repo.get_api_data())
55 126 ret = jsonify(result)
56 127
57 128 expected = ret
58 129 assert_ok(id_, expected, given=response.body)
@@ -1,1932 +1,1960 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
33 33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
34 34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.model.changeset_status import ChangesetStatusModel
37 37 from rhodecode.model.comment import ChangesetCommentsModel
38 38 from rhodecode.model.db import (
39 Session, ChangesetStatus, RepositoryField, Repository)
39 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup)
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.scm import ScmModel, RepoList
42 42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
43 43 from rhodecode.model import validation_schema
44 44 from rhodecode.model.validation_schema.schemas import repo_schema
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 @jsonrpc_method()
50 50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
51 51 """
52 52 Gets an existing repository by its name or repository_id.
53 53
54 54 The members section so the output returns users groups or users
55 55 associated with that repository.
56 56
57 57 This command can only be run using an |authtoken| with admin rights,
58 58 or users with at least read rights to the |repo|.
59 59
60 60 :param apiuser: This is filled automatically from the |authtoken|.
61 61 :type apiuser: AuthUser
62 62 :param repoid: The repository name or repository id.
63 63 :type repoid: str or int
64 64 :param cache: use the cached value for last changeset
65 65 :type: cache: Optional(bool)
66 66
67 67 Example output:
68 68
69 69 .. code-block:: bash
70 70
71 71 {
72 72 "error": null,
73 73 "id": <repo_id>,
74 74 "result": {
75 75 "clone_uri": null,
76 76 "created_on": "timestamp",
77 77 "description": "repo description",
78 78 "enable_downloads": false,
79 79 "enable_locking": false,
80 80 "enable_statistics": false,
81 81 "followers": [
82 82 {
83 83 "active": true,
84 84 "admin": false,
85 85 "api_key": "****************************************",
86 86 "api_keys": [
87 87 "****************************************"
88 88 ],
89 89 "email": "user@example.com",
90 90 "emails": [
91 91 "user@example.com"
92 92 ],
93 93 "extern_name": "rhodecode",
94 94 "extern_type": "rhodecode",
95 95 "firstname": "username",
96 96 "ip_addresses": [],
97 97 "language": null,
98 98 "last_login": "2015-09-16T17:16:35.854",
99 99 "lastname": "surname",
100 100 "user_id": <user_id>,
101 101 "username": "name"
102 102 }
103 103 ],
104 104 "fork_of": "parent-repo",
105 105 "landing_rev": [
106 106 "rev",
107 107 "tip"
108 108 ],
109 109 "last_changeset": {
110 110 "author": "User <user@example.com>",
111 111 "branch": "default",
112 112 "date": "timestamp",
113 113 "message": "last commit message",
114 114 "parents": [
115 115 {
116 116 "raw_id": "commit-id"
117 117 }
118 118 ],
119 119 "raw_id": "commit-id",
120 120 "revision": <revision number>,
121 121 "short_id": "short id"
122 122 },
123 123 "lock_reason": null,
124 124 "locked_by": null,
125 125 "locked_date": null,
126 126 "members": [
127 127 {
128 128 "name": "super-admin-name",
129 129 "origin": "super-admin",
130 130 "permission": "repository.admin",
131 131 "type": "user"
132 132 },
133 133 {
134 134 "name": "owner-name",
135 135 "origin": "owner",
136 136 "permission": "repository.admin",
137 137 "type": "user"
138 138 },
139 139 {
140 140 "name": "user-group-name",
141 141 "origin": "permission",
142 142 "permission": "repository.write",
143 143 "type": "user_group"
144 144 }
145 145 ],
146 146 "owner": "owner-name",
147 147 "permissions": [
148 148 {
149 149 "name": "super-admin-name",
150 150 "origin": "super-admin",
151 151 "permission": "repository.admin",
152 152 "type": "user"
153 153 },
154 154 {
155 155 "name": "owner-name",
156 156 "origin": "owner",
157 157 "permission": "repository.admin",
158 158 "type": "user"
159 159 },
160 160 {
161 161 "name": "user-group-name",
162 162 "origin": "permission",
163 163 "permission": "repository.write",
164 164 "type": "user_group"
165 165 }
166 166 ],
167 167 "private": true,
168 168 "repo_id": 676,
169 169 "repo_name": "user-group/repo-name",
170 170 "repo_type": "hg"
171 171 }
172 172 }
173 173 """
174 174
175 175 repo = get_repo_or_error(repoid)
176 176 cache = Optional.extract(cache)
177 177
178 178 include_secrets = False
179 179 if has_superadmin_permission(apiuser):
180 180 include_secrets = True
181 181 else:
182 182 # check if we have at least read permission for this repo !
183 183 _perms = (
184 184 'repository.admin', 'repository.write', 'repository.read',)
185 185 validate_repo_permissions(apiuser, repoid, repo, _perms)
186 186
187 187 permissions = []
188 188 for _user in repo.permissions():
189 189 user_data = {
190 190 'name': _user.username,
191 191 'permission': _user.permission,
192 192 'origin': get_origin(_user),
193 193 'type': "user",
194 194 }
195 195 permissions.append(user_data)
196 196
197 197 for _user_group in repo.permission_user_groups():
198 198 user_group_data = {
199 199 'name': _user_group.users_group_name,
200 200 'permission': _user_group.permission,
201 201 'origin': get_origin(_user_group),
202 202 'type': "user_group",
203 203 }
204 204 permissions.append(user_group_data)
205 205
206 206 following_users = [
207 207 user.user.get_api_data(include_secrets=include_secrets)
208 208 for user in repo.followers]
209 209
210 210 if not cache:
211 211 repo.update_commit_cache()
212 212 data = repo.get_api_data(include_secrets=include_secrets)
213 213 data['members'] = permissions # TODO: this should be deprecated soon
214 214 data['permissions'] = permissions
215 215 data['followers'] = following_users
216 216 return data
217 217
218 218
219 219 @jsonrpc_method()
220 def get_repos(request, apiuser):
220 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
221 221 """
222 222 Lists all existing repositories.
223 223
224 224 This command can only be run using an |authtoken| with admin rights,
225 225 or users with at least read rights to |repos|.
226 226
227 227 :param apiuser: This is filled automatically from the |authtoken|.
228 228 :type apiuser: AuthUser
229 :param root: specify root repository group to fetch repositories.
230 filters the returned repositories to be members of given root group.
231 :type root: Optional(None)
232 :param traverse: traverse given root into subrepositories. With this flag
233 set to False, it will only return top-level repositories from `root`.
234 if root is empty it will return just top-level repositories.
235 :type traverse: Optional(True)
236
229 237
230 238 Example output:
231 239
232 240 .. code-block:: bash
233 241
234 242 id : <id_given_in_input>
235 243 result: [
236 244 {
237 245 "repo_id" : "<repo_id>",
238 246 "repo_name" : "<reponame>"
239 247 "repo_type" : "<repo_type>",
240 248 "clone_uri" : "<clone_uri>",
241 249 "private": : "<bool>",
242 250 "created_on" : "<datetimecreated>",
243 251 "description" : "<description>",
244 252 "landing_rev": "<landing_rev>",
245 253 "owner": "<repo_owner>",
246 254 "fork_of": "<name_of_fork_parent>",
247 255 "enable_downloads": "<bool>",
248 256 "enable_locking": "<bool>",
249 257 "enable_statistics": "<bool>",
250 258 },
251 259 ...
252 260 ]
253 261 error: null
254 262 """
255 263
256 264 include_secrets = has_superadmin_permission(apiuser)
257 265 _perms = ('repository.read', 'repository.write', 'repository.admin',)
258 266 extras = {'user': apiuser}
259 267
260 repo_list = RepoList(
261 RepoModel().get_all(), perm_set=_perms, extra_kwargs=extras)
268 root = Optional.extract(root)
269 traverse = Optional.extract(traverse, binary=True)
270
271 if root:
272 # verify parent existance, if it's empty return an error
273 parent = RepoGroup.get_by_group_name(root)
274 if not parent:
275 raise JSONRPCError(
276 'Root repository group `{}` does not exist'.format(root))
277
278 if traverse:
279 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
280 else:
281 repos = RepoModel().get_repos_for_root(root=parent)
282 else:
283 if traverse:
284 repos = RepoModel().get_all()
285 else:
286 # return just top-level
287 repos = RepoModel().get_repos_for_root(root=None)
288
289 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
262 290 return [repo.get_api_data(include_secrets=include_secrets)
263 291 for repo in repo_list]
264 292
265 293
266 294 @jsonrpc_method()
267 295 def get_repo_changeset(request, apiuser, repoid, revision,
268 296 details=Optional('basic')):
269 297 """
270 298 Returns information about a changeset.
271 299
272 300 Additionally parameters define the amount of details returned by
273 301 this function.
274 302
275 303 This command can only be run using an |authtoken| with admin rights,
276 304 or users with at least read rights to the |repo|.
277 305
278 306 :param apiuser: This is filled automatically from the |authtoken|.
279 307 :type apiuser: AuthUser
280 308 :param repoid: The repository name or repository id
281 309 :type repoid: str or int
282 310 :param revision: revision for which listing should be done
283 311 :type revision: str
284 312 :param details: details can be 'basic|extended|full' full gives diff
285 313 info details like the diff itself, and number of changed files etc.
286 314 :type details: Optional(str)
287 315
288 316 """
289 317 repo = get_repo_or_error(repoid)
290 318 if not has_superadmin_permission(apiuser):
291 319 _perms = (
292 320 'repository.admin', 'repository.write', 'repository.read',)
293 321 validate_repo_permissions(apiuser, repoid, repo, _perms)
294 322
295 323 changes_details = Optional.extract(details)
296 324 _changes_details_types = ['basic', 'extended', 'full']
297 325 if changes_details not in _changes_details_types:
298 326 raise JSONRPCError(
299 327 'ret_type must be one of %s' % (
300 328 ','.join(_changes_details_types)))
301 329
302 330 pre_load = ['author', 'branch', 'date', 'message', 'parents',
303 331 'status', '_commit', '_file_paths']
304 332
305 333 try:
306 334 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
307 335 except TypeError as e:
308 336 raise JSONRPCError(e.message)
309 337 _cs_json = cs.__json__()
310 338 _cs_json['diff'] = build_commit_data(cs, changes_details)
311 339 if changes_details == 'full':
312 340 _cs_json['refs'] = {
313 341 'branches': [cs.branch],
314 342 'bookmarks': getattr(cs, 'bookmarks', []),
315 343 'tags': cs.tags
316 344 }
317 345 return _cs_json
318 346
319 347
320 348 @jsonrpc_method()
321 349 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
322 350 details=Optional('basic')):
323 351 """
324 352 Returns a set of commits limited by the number starting
325 353 from the `start_rev` option.
326 354
327 355 Additional parameters define the amount of details returned by this
328 356 function.
329 357
330 358 This command can only be run using an |authtoken| with admin rights,
331 359 or users with at least read rights to |repos|.
332 360
333 361 :param apiuser: This is filled automatically from the |authtoken|.
334 362 :type apiuser: AuthUser
335 363 :param repoid: The repository name or repository ID.
336 364 :type repoid: str or int
337 365 :param start_rev: The starting revision from where to get changesets.
338 366 :type start_rev: str
339 367 :param limit: Limit the number of commits to this amount
340 368 :type limit: str or int
341 369 :param details: Set the level of detail returned. Valid option are:
342 370 ``basic``, ``extended`` and ``full``.
343 371 :type details: Optional(str)
344 372
345 373 .. note::
346 374
347 375 Setting the parameter `details` to the value ``full`` is extensive
348 376 and returns details like the diff itself, and the number
349 377 of changed files.
350 378
351 379 """
352 380 repo = get_repo_or_error(repoid)
353 381 if not has_superadmin_permission(apiuser):
354 382 _perms = (
355 383 'repository.admin', 'repository.write', 'repository.read',)
356 384 validate_repo_permissions(apiuser, repoid, repo, _perms)
357 385
358 386 changes_details = Optional.extract(details)
359 387 _changes_details_types = ['basic', 'extended', 'full']
360 388 if changes_details not in _changes_details_types:
361 389 raise JSONRPCError(
362 390 'ret_type must be one of %s' % (
363 391 ','.join(_changes_details_types)))
364 392
365 393 limit = int(limit)
366 394 pre_load = ['author', 'branch', 'date', 'message', 'parents',
367 395 'status', '_commit', '_file_paths']
368 396
369 397 vcs_repo = repo.scm_instance()
370 398 # SVN needs a special case to distinguish its index and commit id
371 399 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
372 400 start_rev = vcs_repo.commit_ids[0]
373 401
374 402 try:
375 403 commits = vcs_repo.get_commits(
376 404 start_id=start_rev, pre_load=pre_load)
377 405 except TypeError as e:
378 406 raise JSONRPCError(e.message)
379 407 except Exception:
380 408 log.exception('Fetching of commits failed')
381 409 raise JSONRPCError('Error occurred during commit fetching')
382 410
383 411 ret = []
384 412 for cnt, commit in enumerate(commits):
385 413 if cnt >= limit != -1:
386 414 break
387 415 _cs_json = commit.__json__()
388 416 _cs_json['diff'] = build_commit_data(commit, changes_details)
389 417 if changes_details == 'full':
390 418 _cs_json['refs'] = {
391 419 'branches': [commit.branch],
392 420 'bookmarks': getattr(commit, 'bookmarks', []),
393 421 'tags': commit.tags
394 422 }
395 423 ret.append(_cs_json)
396 424 return ret
397 425
398 426
399 427 @jsonrpc_method()
400 428 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
401 429 ret_type=Optional('all'), details=Optional('basic'),
402 430 max_file_bytes=Optional(None)):
403 431 """
404 432 Returns a list of nodes and children in a flat list for a given
405 433 path at given revision.
406 434
407 435 It's possible to specify ret_type to show only `files` or `dirs`.
408 436
409 437 This command can only be run using an |authtoken| with admin rights,
410 438 or users with at least read rights to |repos|.
411 439
412 440 :param apiuser: This is filled automatically from the |authtoken|.
413 441 :type apiuser: AuthUser
414 442 :param repoid: The repository name or repository ID.
415 443 :type repoid: str or int
416 444 :param revision: The revision for which listing should be done.
417 445 :type revision: str
418 446 :param root_path: The path from which to start displaying.
419 447 :type root_path: str
420 448 :param ret_type: Set the return type. Valid options are
421 449 ``all`` (default), ``files`` and ``dirs``.
422 450 :type ret_type: Optional(str)
423 451 :param details: Returns extended information about nodes, such as
424 452 md5, binary, and or content. The valid options are ``basic`` and
425 453 ``full``.
426 454 :type details: Optional(str)
427 455 :param max_file_bytes: Only return file content under this file size bytes
428 456 :type details: Optional(int)
429 457
430 458 Example output:
431 459
432 460 .. code-block:: bash
433 461
434 462 id : <id_given_in_input>
435 463 result: [
436 464 {
437 465 "name" : "<name>"
438 466 "type" : "<type>",
439 467 "binary": "<true|false>" (only in extended mode)
440 468 "md5" : "<md5 of file content>" (only in extended mode)
441 469 },
442 470 ...
443 471 ]
444 472 error: null
445 473 """
446 474
447 475 repo = get_repo_or_error(repoid)
448 476 if not has_superadmin_permission(apiuser):
449 477 _perms = (
450 478 'repository.admin', 'repository.write', 'repository.read',)
451 479 validate_repo_permissions(apiuser, repoid, repo, _perms)
452 480
453 481 ret_type = Optional.extract(ret_type)
454 482 details = Optional.extract(details)
455 483 _extended_types = ['basic', 'full']
456 484 if details not in _extended_types:
457 485 raise JSONRPCError(
458 486 'ret_type must be one of %s' % (','.join(_extended_types)))
459 487 extended_info = False
460 488 content = False
461 489 if details == 'basic':
462 490 extended_info = True
463 491
464 492 if details == 'full':
465 493 extended_info = content = True
466 494
467 495 _map = {}
468 496 try:
469 497 # check if repo is not empty by any chance, skip quicker if it is.
470 498 _scm = repo.scm_instance()
471 499 if _scm.is_empty():
472 500 return []
473 501
474 502 _d, _f = ScmModel().get_nodes(
475 503 repo, revision, root_path, flat=False,
476 504 extended_info=extended_info, content=content,
477 505 max_file_bytes=max_file_bytes)
478 506 _map = {
479 507 'all': _d + _f,
480 508 'files': _f,
481 509 'dirs': _d,
482 510 }
483 511 return _map[ret_type]
484 512 except KeyError:
485 513 raise JSONRPCError(
486 514 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
487 515 except Exception:
488 516 log.exception("Exception occurred while trying to get repo nodes")
489 517 raise JSONRPCError(
490 518 'failed to get repo: `%s` nodes' % repo.repo_name
491 519 )
492 520
493 521
494 522 @jsonrpc_method()
495 523 def get_repo_refs(request, apiuser, repoid):
496 524 """
497 525 Returns a dictionary of current references. It returns
498 526 bookmarks, branches, closed_branches, and tags for given repository
499 527
500 528 It's possible to specify ret_type to show only `files` or `dirs`.
501 529
502 530 This command can only be run using an |authtoken| with admin rights,
503 531 or users with at least read rights to |repos|.
504 532
505 533 :param apiuser: This is filled automatically from the |authtoken|.
506 534 :type apiuser: AuthUser
507 535 :param repoid: The repository name or repository ID.
508 536 :type repoid: str or int
509 537
510 538 Example output:
511 539
512 540 .. code-block:: bash
513 541
514 542 id : <id_given_in_input>
515 543 "result": {
516 544 "bookmarks": {
517 545 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
518 546 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
519 547 },
520 548 "branches": {
521 549 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
522 550 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
523 551 },
524 552 "branches_closed": {},
525 553 "tags": {
526 554 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
527 555 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
528 556 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
529 557 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
530 558 }
531 559 }
532 560 error: null
533 561 """
534 562
535 563 repo = get_repo_or_error(repoid)
536 564 if not has_superadmin_permission(apiuser):
537 565 _perms = ('repository.admin', 'repository.write', 'repository.read',)
538 566 validate_repo_permissions(apiuser, repoid, repo, _perms)
539 567
540 568 try:
541 569 # check if repo is not empty by any chance, skip quicker if it is.
542 570 vcs_instance = repo.scm_instance()
543 571 refs = vcs_instance.refs()
544 572 return refs
545 573 except Exception:
546 574 log.exception("Exception occurred while trying to get repo refs")
547 575 raise JSONRPCError(
548 576 'failed to get repo: `%s` references' % repo.repo_name
549 577 )
550 578
551 579
552 580 @jsonrpc_method()
553 581 def create_repo(
554 582 request, apiuser, repo_name, repo_type,
555 583 owner=Optional(OAttr('apiuser')),
556 584 description=Optional(''),
557 585 private=Optional(False),
558 586 clone_uri=Optional(None),
559 587 landing_rev=Optional('rev:tip'),
560 588 enable_statistics=Optional(False),
561 589 enable_locking=Optional(False),
562 590 enable_downloads=Optional(False),
563 591 copy_permissions=Optional(False)):
564 592 """
565 593 Creates a repository.
566 594
567 595 * If the repository name contains "/", repository will be created inside
568 596 a repository group or nested repository groups
569 597
570 598 For example "foo/bar/repo1" will create |repo| called "repo1" inside
571 599 group "foo/bar". You have to have permissions to access and write to
572 600 the last repository group ("bar" in this example)
573 601
574 602 This command can only be run using an |authtoken| with at least
575 603 permissions to create repositories, or write permissions to
576 604 parent repository groups.
577 605
578 606 :param apiuser: This is filled automatically from the |authtoken|.
579 607 :type apiuser: AuthUser
580 608 :param repo_name: Set the repository name.
581 609 :type repo_name: str
582 610 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
583 611 :type repo_type: str
584 612 :param owner: user_id or username
585 613 :type owner: Optional(str)
586 614 :param description: Set the repository description.
587 615 :type description: Optional(str)
588 616 :param private: set repository as private
589 617 :type private: bool
590 618 :param clone_uri: set clone_uri
591 619 :type clone_uri: str
592 620 :param landing_rev: <rev_type>:<rev>
593 621 :type landing_rev: str
594 622 :param enable_locking:
595 623 :type enable_locking: bool
596 624 :param enable_downloads:
597 625 :type enable_downloads: bool
598 626 :param enable_statistics:
599 627 :type enable_statistics: bool
600 628 :param copy_permissions: Copy permission from group in which the
601 629 repository is being created.
602 630 :type copy_permissions: bool
603 631
604 632
605 633 Example output:
606 634
607 635 .. code-block:: bash
608 636
609 637 id : <id_given_in_input>
610 638 result: {
611 639 "msg": "Created new repository `<reponame>`",
612 640 "success": true,
613 641 "task": "<celery task id or None if done sync>"
614 642 }
615 643 error: null
616 644
617 645
618 646 Example error output:
619 647
620 648 .. code-block:: bash
621 649
622 650 id : <id_given_in_input>
623 651 result : null
624 652 error : {
625 653 'failed to create repository `<repo_name>`'
626 654 }
627 655
628 656 """
629 657
630 658 owner = validate_set_owner_permissions(apiuser, owner)
631 659
632 660 description = Optional.extract(description)
633 661 copy_permissions = Optional.extract(copy_permissions)
634 662 clone_uri = Optional.extract(clone_uri)
635 663 landing_commit_ref = Optional.extract(landing_rev)
636 664
637 665 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
638 666 if isinstance(private, Optional):
639 667 private = defs.get('repo_private') or Optional.extract(private)
640 668 if isinstance(repo_type, Optional):
641 669 repo_type = defs.get('repo_type')
642 670 if isinstance(enable_statistics, Optional):
643 671 enable_statistics = defs.get('repo_enable_statistics')
644 672 if isinstance(enable_locking, Optional):
645 673 enable_locking = defs.get('repo_enable_locking')
646 674 if isinstance(enable_downloads, Optional):
647 675 enable_downloads = defs.get('repo_enable_downloads')
648 676
649 677 schema = repo_schema.RepoSchema().bind(
650 678 repo_type_options=rhodecode.BACKENDS.keys(),
651 679 # user caller
652 680 user=apiuser)
653 681
654 682 try:
655 683 schema_data = schema.deserialize(dict(
656 684 repo_name=repo_name,
657 685 repo_type=repo_type,
658 686 repo_owner=owner.username,
659 687 repo_description=description,
660 688 repo_landing_commit_ref=landing_commit_ref,
661 689 repo_clone_uri=clone_uri,
662 690 repo_private=private,
663 691 repo_copy_permissions=copy_permissions,
664 692 repo_enable_statistics=enable_statistics,
665 693 repo_enable_downloads=enable_downloads,
666 694 repo_enable_locking=enable_locking))
667 695 except validation_schema.Invalid as err:
668 696 raise JSONRPCValidationError(colander_exc=err)
669 697
670 698 try:
671 699 data = {
672 700 'owner': owner,
673 701 'repo_name': schema_data['repo_group']['repo_name_without_group'],
674 702 'repo_name_full': schema_data['repo_name'],
675 703 'repo_group': schema_data['repo_group']['repo_group_id'],
676 704 'repo_type': schema_data['repo_type'],
677 705 'repo_description': schema_data['repo_description'],
678 706 'repo_private': schema_data['repo_private'],
679 707 'clone_uri': schema_data['repo_clone_uri'],
680 708 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
681 709 'enable_statistics': schema_data['repo_enable_statistics'],
682 710 'enable_locking': schema_data['repo_enable_locking'],
683 711 'enable_downloads': schema_data['repo_enable_downloads'],
684 712 'repo_copy_permissions': schema_data['repo_copy_permissions'],
685 713 }
686 714
687 715 task = RepoModel().create(form_data=data, cur_user=owner)
688 716 from celery.result import BaseAsyncResult
689 717 task_id = None
690 718 if isinstance(task, BaseAsyncResult):
691 719 task_id = task.task_id
692 720 # no commit, it's done in RepoModel, or async via celery
693 721 return {
694 722 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
695 723 'success': True, # cannot return the repo data here since fork
696 724 # can be done async
697 725 'task': task_id
698 726 }
699 727 except Exception:
700 728 log.exception(
701 729 u"Exception while trying to create the repository %s",
702 730 schema_data['repo_name'])
703 731 raise JSONRPCError(
704 732 'failed to create repository `%s`' % (schema_data['repo_name'],))
705 733
706 734
707 735 @jsonrpc_method()
708 736 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
709 737 description=Optional('')):
710 738 """
711 739 Adds an extra field to a repository.
712 740
713 741 This command can only be run using an |authtoken| with at least
714 742 write permissions to the |repo|.
715 743
716 744 :param apiuser: This is filled automatically from the |authtoken|.
717 745 :type apiuser: AuthUser
718 746 :param repoid: Set the repository name or repository id.
719 747 :type repoid: str or int
720 748 :param key: Create a unique field key for this repository.
721 749 :type key: str
722 750 :param label:
723 751 :type label: Optional(str)
724 752 :param description:
725 753 :type description: Optional(str)
726 754 """
727 755 repo = get_repo_or_error(repoid)
728 756 if not has_superadmin_permission(apiuser):
729 757 _perms = ('repository.admin',)
730 758 validate_repo_permissions(apiuser, repoid, repo, _perms)
731 759
732 760 label = Optional.extract(label) or key
733 761 description = Optional.extract(description)
734 762
735 763 field = RepositoryField.get_by_key_name(key, repo)
736 764 if field:
737 765 raise JSONRPCError('Field with key '
738 766 '`%s` exists for repo `%s`' % (key, repoid))
739 767
740 768 try:
741 769 RepoModel().add_repo_field(repo, key, field_label=label,
742 770 field_desc=description)
743 771 Session().commit()
744 772 return {
745 773 'msg': "Added new repository field `%s`" % (key,),
746 774 'success': True,
747 775 }
748 776 except Exception:
749 777 log.exception("Exception occurred while trying to add field to repo")
750 778 raise JSONRPCError(
751 779 'failed to create new field for repository `%s`' % (repoid,))
752 780
753 781
754 782 @jsonrpc_method()
755 783 def remove_field_from_repo(request, apiuser, repoid, key):
756 784 """
757 785 Removes an extra field from a repository.
758 786
759 787 This command can only be run using an |authtoken| with at least
760 788 write permissions to the |repo|.
761 789
762 790 :param apiuser: This is filled automatically from the |authtoken|.
763 791 :type apiuser: AuthUser
764 792 :param repoid: Set the repository name or repository ID.
765 793 :type repoid: str or int
766 794 :param key: Set the unique field key for this repository.
767 795 :type key: str
768 796 """
769 797
770 798 repo = get_repo_or_error(repoid)
771 799 if not has_superadmin_permission(apiuser):
772 800 _perms = ('repository.admin',)
773 801 validate_repo_permissions(apiuser, repoid, repo, _perms)
774 802
775 803 field = RepositoryField.get_by_key_name(key, repo)
776 804 if not field:
777 805 raise JSONRPCError('Field with key `%s` does not '
778 806 'exists for repo `%s`' % (key, repoid))
779 807
780 808 try:
781 809 RepoModel().delete_repo_field(repo, field_key=key)
782 810 Session().commit()
783 811 return {
784 812 'msg': "Deleted repository field `%s`" % (key,),
785 813 'success': True,
786 814 }
787 815 except Exception:
788 816 log.exception(
789 817 "Exception occurred while trying to delete field from repo")
790 818 raise JSONRPCError(
791 819 'failed to delete field for repository `%s`' % (repoid,))
792 820
793 821
794 822 @jsonrpc_method()
795 823 def update_repo(
796 824 request, apiuser, repoid, repo_name=Optional(None),
797 825 owner=Optional(OAttr('apiuser')), description=Optional(''),
798 826 private=Optional(False), clone_uri=Optional(None),
799 827 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
800 828 enable_statistics=Optional(False),
801 829 enable_locking=Optional(False),
802 830 enable_downloads=Optional(False), fields=Optional('')):
803 831 """
804 832 Updates a repository with the given information.
805 833
806 834 This command can only be run using an |authtoken| with at least
807 835 admin permissions to the |repo|.
808 836
809 837 * If the repository name contains "/", repository will be updated
810 838 accordingly with a repository group or nested repository groups
811 839
812 840 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
813 841 called "repo-test" and place it inside group "foo/bar".
814 842 You have to have permissions to access and write to the last repository
815 843 group ("bar" in this example)
816 844
817 845 :param apiuser: This is filled automatically from the |authtoken|.
818 846 :type apiuser: AuthUser
819 847 :param repoid: repository name or repository ID.
820 848 :type repoid: str or int
821 849 :param repo_name: Update the |repo| name, including the
822 850 repository group it's in.
823 851 :type repo_name: str
824 852 :param owner: Set the |repo| owner.
825 853 :type owner: str
826 854 :param fork_of: Set the |repo| as fork of another |repo|.
827 855 :type fork_of: str
828 856 :param description: Update the |repo| description.
829 857 :type description: str
830 858 :param private: Set the |repo| as private. (True | False)
831 859 :type private: bool
832 860 :param clone_uri: Update the |repo| clone URI.
833 861 :type clone_uri: str
834 862 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
835 863 :type landing_rev: str
836 864 :param enable_statistics: Enable statistics on the |repo|, (True | False).
837 865 :type enable_statistics: bool
838 866 :param enable_locking: Enable |repo| locking.
839 867 :type enable_locking: bool
840 868 :param enable_downloads: Enable downloads from the |repo|, (True | False).
841 869 :type enable_downloads: bool
842 870 :param fields: Add extra fields to the |repo|. Use the following
843 871 example format: ``field_key=field_val,field_key2=fieldval2``.
844 872 Escape ', ' with \,
845 873 :type fields: str
846 874 """
847 875
848 876 repo = get_repo_or_error(repoid)
849 877
850 878 include_secrets = False
851 879 if not has_superadmin_permission(apiuser):
852 880 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
853 881 else:
854 882 include_secrets = True
855 883
856 884 updates = dict(
857 885 repo_name=repo_name
858 886 if not isinstance(repo_name, Optional) else repo.repo_name,
859 887
860 888 fork_id=fork_of
861 889 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
862 890
863 891 user=owner
864 892 if not isinstance(owner, Optional) else repo.user.username,
865 893
866 894 repo_description=description
867 895 if not isinstance(description, Optional) else repo.description,
868 896
869 897 repo_private=private
870 898 if not isinstance(private, Optional) else repo.private,
871 899
872 900 clone_uri=clone_uri
873 901 if not isinstance(clone_uri, Optional) else repo.clone_uri,
874 902
875 903 repo_landing_rev=landing_rev
876 904 if not isinstance(landing_rev, Optional) else repo._landing_revision,
877 905
878 906 repo_enable_statistics=enable_statistics
879 907 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
880 908
881 909 repo_enable_locking=enable_locking
882 910 if not isinstance(enable_locking, Optional) else repo.enable_locking,
883 911
884 912 repo_enable_downloads=enable_downloads
885 913 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
886 914
887 915 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
888 916
889 917 schema = repo_schema.RepoSchema().bind(
890 918 repo_type_options=rhodecode.BACKENDS.keys(),
891 919 repo_ref_options=ref_choices,
892 920 # user caller
893 921 user=apiuser,
894 922 old_values=repo.get_api_data())
895 923 try:
896 924 schema_data = schema.deserialize(dict(
897 925 # we save old value, users cannot change type
898 926 repo_type=repo.repo_type,
899 927
900 928 repo_name=updates['repo_name'],
901 929 repo_owner=updates['user'],
902 930 repo_description=updates['repo_description'],
903 931 repo_clone_uri=updates['clone_uri'],
904 932 repo_fork_of=updates['fork_id'],
905 933 repo_private=updates['repo_private'],
906 934 repo_landing_commit_ref=updates['repo_landing_rev'],
907 935 repo_enable_statistics=updates['repo_enable_statistics'],
908 936 repo_enable_downloads=updates['repo_enable_downloads'],
909 937 repo_enable_locking=updates['repo_enable_locking']))
910 938 except validation_schema.Invalid as err:
911 939 raise JSONRPCValidationError(colander_exc=err)
912 940
913 941 # save validated data back into the updates dict
914 942 validated_updates = dict(
915 943 repo_name=schema_data['repo_group']['repo_name_without_group'],
916 944 repo_group=schema_data['repo_group']['repo_group_id'],
917 945
918 946 user=schema_data['repo_owner'],
919 947 repo_description=schema_data['repo_description'],
920 948 repo_private=schema_data['repo_private'],
921 949 clone_uri=schema_data['repo_clone_uri'],
922 950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
923 951 repo_enable_statistics=schema_data['repo_enable_statistics'],
924 952 repo_enable_locking=schema_data['repo_enable_locking'],
925 953 repo_enable_downloads=schema_data['repo_enable_downloads'],
926 954 )
927 955
928 956 if schema_data['repo_fork_of']:
929 957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
930 958 validated_updates['fork_id'] = fork_repo.repo_id
931 959
932 960 # extra fields
933 961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
934 962 if fields:
935 963 validated_updates.update(fields)
936 964
937 965 try:
938 966 RepoModel().update(repo, **validated_updates)
939 967 Session().commit()
940 968 return {
941 969 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
942 970 'repository': repo.get_api_data(include_secrets=include_secrets)
943 971 }
944 972 except Exception:
945 973 log.exception(
946 974 u"Exception while trying to update the repository %s",
947 975 repoid)
948 976 raise JSONRPCError('failed to update repo `%s`' % repoid)
949 977
950 978
951 979 @jsonrpc_method()
952 980 def fork_repo(request, apiuser, repoid, fork_name,
953 981 owner=Optional(OAttr('apiuser')),
954 982 description=Optional(''),
955 983 private=Optional(False),
956 984 clone_uri=Optional(None),
957 985 landing_rev=Optional('rev:tip'),
958 986 copy_permissions=Optional(False)):
959 987 """
960 988 Creates a fork of the specified |repo|.
961 989
962 990 * If the fork_name contains "/", fork will be created inside
963 991 a repository group or nested repository groups
964 992
965 993 For example "foo/bar/fork-repo" will create fork called "fork-repo"
966 994 inside group "foo/bar". You have to have permissions to access and
967 995 write to the last repository group ("bar" in this example)
968 996
969 997 This command can only be run using an |authtoken| with minimum
970 998 read permissions of the forked repo, create fork permissions for an user.
971 999
972 1000 :param apiuser: This is filled automatically from the |authtoken|.
973 1001 :type apiuser: AuthUser
974 1002 :param repoid: Set repository name or repository ID.
975 1003 :type repoid: str or int
976 1004 :param fork_name: Set the fork name, including it's repository group membership.
977 1005 :type fork_name: str
978 1006 :param owner: Set the fork owner.
979 1007 :type owner: str
980 1008 :param description: Set the fork description.
981 1009 :type description: str
982 1010 :param copy_permissions: Copy permissions from parent |repo|. The
983 1011 default is False.
984 1012 :type copy_permissions: bool
985 1013 :param private: Make the fork private. The default is False.
986 1014 :type private: bool
987 1015 :param landing_rev: Set the landing revision. The default is tip.
988 1016
989 1017 Example output:
990 1018
991 1019 .. code-block:: bash
992 1020
993 1021 id : <id_for_response>
994 1022 api_key : "<api_key>"
995 1023 args: {
996 1024 "repoid" : "<reponame or repo_id>",
997 1025 "fork_name": "<forkname>",
998 1026 "owner": "<username or user_id = Optional(=apiuser)>",
999 1027 "description": "<description>",
1000 1028 "copy_permissions": "<bool>",
1001 1029 "private": "<bool>",
1002 1030 "landing_rev": "<landing_rev>"
1003 1031 }
1004 1032
1005 1033 Example error output:
1006 1034
1007 1035 .. code-block:: bash
1008 1036
1009 1037 id : <id_given_in_input>
1010 1038 result: {
1011 1039 "msg": "Created fork of `<reponame>` as `<forkname>`",
1012 1040 "success": true,
1013 1041 "task": "<celery task id or None if done sync>"
1014 1042 }
1015 1043 error: null
1016 1044
1017 1045 """
1018 1046
1019 1047 repo = get_repo_or_error(repoid)
1020 1048 repo_name = repo.repo_name
1021 1049
1022 1050 if not has_superadmin_permission(apiuser):
1023 1051 # check if we have at least read permission for
1024 1052 # this repo that we fork !
1025 1053 _perms = (
1026 1054 'repository.admin', 'repository.write', 'repository.read')
1027 1055 validate_repo_permissions(apiuser, repoid, repo, _perms)
1028 1056
1029 1057 # check if the regular user has at least fork permissions as well
1030 1058 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1031 1059 raise JSONRPCForbidden()
1032 1060
1033 1061 # check if user can set owner parameter
1034 1062 owner = validate_set_owner_permissions(apiuser, owner)
1035 1063
1036 1064 description = Optional.extract(description)
1037 1065 copy_permissions = Optional.extract(copy_permissions)
1038 1066 clone_uri = Optional.extract(clone_uri)
1039 1067 landing_commit_ref = Optional.extract(landing_rev)
1040 1068 private = Optional.extract(private)
1041 1069
1042 1070 schema = repo_schema.RepoSchema().bind(
1043 1071 repo_type_options=rhodecode.BACKENDS.keys(),
1044 1072 # user caller
1045 1073 user=apiuser)
1046 1074
1047 1075 try:
1048 1076 schema_data = schema.deserialize(dict(
1049 1077 repo_name=fork_name,
1050 1078 repo_type=repo.repo_type,
1051 1079 repo_owner=owner.username,
1052 1080 repo_description=description,
1053 1081 repo_landing_commit_ref=landing_commit_ref,
1054 1082 repo_clone_uri=clone_uri,
1055 1083 repo_private=private,
1056 1084 repo_copy_permissions=copy_permissions))
1057 1085 except validation_schema.Invalid as err:
1058 1086 raise JSONRPCValidationError(colander_exc=err)
1059 1087
1060 1088 try:
1061 1089 data = {
1062 1090 'fork_parent_id': repo.repo_id,
1063 1091
1064 1092 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1065 1093 'repo_name_full': schema_data['repo_name'],
1066 1094 'repo_group': schema_data['repo_group']['repo_group_id'],
1067 1095 'repo_type': schema_data['repo_type'],
1068 1096 'description': schema_data['repo_description'],
1069 1097 'private': schema_data['repo_private'],
1070 1098 'copy_permissions': schema_data['repo_copy_permissions'],
1071 1099 'landing_rev': schema_data['repo_landing_commit_ref'],
1072 1100 }
1073 1101
1074 1102 task = RepoModel().create_fork(data, cur_user=owner)
1075 1103 # no commit, it's done in RepoModel, or async via celery
1076 1104 from celery.result import BaseAsyncResult
1077 1105 task_id = None
1078 1106 if isinstance(task, BaseAsyncResult):
1079 1107 task_id = task.task_id
1080 1108 return {
1081 1109 'msg': 'Created fork of `%s` as `%s`' % (
1082 1110 repo.repo_name, schema_data['repo_name']),
1083 1111 'success': True, # cannot return the repo data here since fork
1084 1112 # can be done async
1085 1113 'task': task_id
1086 1114 }
1087 1115 except Exception:
1088 1116 log.exception(
1089 1117 u"Exception while trying to create fork %s",
1090 1118 schema_data['repo_name'])
1091 1119 raise JSONRPCError(
1092 1120 'failed to fork repository `%s` as `%s`' % (
1093 1121 repo_name, schema_data['repo_name']))
1094 1122
1095 1123
1096 1124 @jsonrpc_method()
1097 1125 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1098 1126 """
1099 1127 Deletes a repository.
1100 1128
1101 1129 * When the `forks` parameter is set it's possible to detach or delete
1102 1130 forks of deleted repository.
1103 1131
1104 1132 This command can only be run using an |authtoken| with admin
1105 1133 permissions on the |repo|.
1106 1134
1107 1135 :param apiuser: This is filled automatically from the |authtoken|.
1108 1136 :type apiuser: AuthUser
1109 1137 :param repoid: Set the repository name or repository ID.
1110 1138 :type repoid: str or int
1111 1139 :param forks: Set to `detach` or `delete` forks from the |repo|.
1112 1140 :type forks: Optional(str)
1113 1141
1114 1142 Example error output:
1115 1143
1116 1144 .. code-block:: bash
1117 1145
1118 1146 id : <id_given_in_input>
1119 1147 result: {
1120 1148 "msg": "Deleted repository `<reponame>`",
1121 1149 "success": true
1122 1150 }
1123 1151 error: null
1124 1152 """
1125 1153
1126 1154 repo = get_repo_or_error(repoid)
1127 1155 if not has_superadmin_permission(apiuser):
1128 1156 _perms = ('repository.admin',)
1129 1157 validate_repo_permissions(apiuser, repoid, repo, _perms)
1130 1158
1131 1159 try:
1132 1160 handle_forks = Optional.extract(forks)
1133 1161 _forks_msg = ''
1134 1162 _forks = [f for f in repo.forks]
1135 1163 if handle_forks == 'detach':
1136 1164 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1137 1165 elif handle_forks == 'delete':
1138 1166 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1139 1167 elif _forks:
1140 1168 raise JSONRPCError(
1141 1169 'Cannot delete `%s` it still contains attached forks' %
1142 1170 (repo.repo_name,)
1143 1171 )
1144 1172
1145 1173 RepoModel().delete(repo, forks=forks)
1146 1174 Session().commit()
1147 1175 return {
1148 1176 'msg': 'Deleted repository `%s`%s' % (
1149 1177 repo.repo_name, _forks_msg),
1150 1178 'success': True
1151 1179 }
1152 1180 except Exception:
1153 1181 log.exception("Exception occurred while trying to delete repo")
1154 1182 raise JSONRPCError(
1155 1183 'failed to delete repository `%s`' % (repo.repo_name,)
1156 1184 )
1157 1185
1158 1186
1159 1187 #TODO: marcink, change name ?
1160 1188 @jsonrpc_method()
1161 1189 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1162 1190 """
1163 1191 Invalidates the cache for the specified repository.
1164 1192
1165 1193 This command can only be run using an |authtoken| with admin rights to
1166 1194 the specified repository.
1167 1195
1168 1196 This command takes the following options:
1169 1197
1170 1198 :param apiuser: This is filled automatically from |authtoken|.
1171 1199 :type apiuser: AuthUser
1172 1200 :param repoid: Sets the repository name or repository ID.
1173 1201 :type repoid: str or int
1174 1202 :param delete_keys: This deletes the invalidated keys instead of
1175 1203 just flagging them.
1176 1204 :type delete_keys: Optional(``True`` | ``False``)
1177 1205
1178 1206 Example output:
1179 1207
1180 1208 .. code-block:: bash
1181 1209
1182 1210 id : <id_given_in_input>
1183 1211 result : {
1184 1212 'msg': Cache for repository `<repository name>` was invalidated,
1185 1213 'repository': <repository name>
1186 1214 }
1187 1215 error : null
1188 1216
1189 1217 Example error output:
1190 1218
1191 1219 .. code-block:: bash
1192 1220
1193 1221 id : <id_given_in_input>
1194 1222 result : null
1195 1223 error : {
1196 1224 'Error occurred during cache invalidation action'
1197 1225 }
1198 1226
1199 1227 """
1200 1228
1201 1229 repo = get_repo_or_error(repoid)
1202 1230 if not has_superadmin_permission(apiuser):
1203 1231 _perms = ('repository.admin', 'repository.write',)
1204 1232 validate_repo_permissions(apiuser, repoid, repo, _perms)
1205 1233
1206 1234 delete = Optional.extract(delete_keys)
1207 1235 try:
1208 1236 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1209 1237 return {
1210 1238 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1211 1239 'repository': repo.repo_name
1212 1240 }
1213 1241 except Exception:
1214 1242 log.exception(
1215 1243 "Exception occurred while trying to invalidate repo cache")
1216 1244 raise JSONRPCError(
1217 1245 'Error occurred during cache invalidation action'
1218 1246 )
1219 1247
1220 1248
1221 1249 #TODO: marcink, change name ?
1222 1250 @jsonrpc_method()
1223 1251 def lock(request, apiuser, repoid, locked=Optional(None),
1224 1252 userid=Optional(OAttr('apiuser'))):
1225 1253 """
1226 1254 Sets the lock state of the specified |repo| by the given user.
1227 1255 From more information, see :ref:`repo-locking`.
1228 1256
1229 1257 * If the ``userid`` option is not set, the repository is locked to the
1230 1258 user who called the method.
1231 1259 * If the ``locked`` parameter is not set, the current lock state of the
1232 1260 repository is displayed.
1233 1261
1234 1262 This command can only be run using an |authtoken| with admin rights to
1235 1263 the specified repository.
1236 1264
1237 1265 This command takes the following options:
1238 1266
1239 1267 :param apiuser: This is filled automatically from the |authtoken|.
1240 1268 :type apiuser: AuthUser
1241 1269 :param repoid: Sets the repository name or repository ID.
1242 1270 :type repoid: str or int
1243 1271 :param locked: Sets the lock state.
1244 1272 :type locked: Optional(``True`` | ``False``)
1245 1273 :param userid: Set the repository lock to this user.
1246 1274 :type userid: Optional(str or int)
1247 1275
1248 1276 Example error output:
1249 1277
1250 1278 .. code-block:: bash
1251 1279
1252 1280 id : <id_given_in_input>
1253 1281 result : {
1254 1282 'repo': '<reponame>',
1255 1283 'locked': <bool: lock state>,
1256 1284 'locked_since': <int: lock timestamp>,
1257 1285 'locked_by': <username of person who made the lock>,
1258 1286 'lock_reason': <str: reason for locking>,
1259 1287 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1260 1288 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1261 1289 or
1262 1290 'msg': 'Repo `<repository name>` not locked.'
1263 1291 or
1264 1292 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1265 1293 }
1266 1294 error : null
1267 1295
1268 1296 Example error output:
1269 1297
1270 1298 .. code-block:: bash
1271 1299
1272 1300 id : <id_given_in_input>
1273 1301 result : null
1274 1302 error : {
1275 1303 'Error occurred locking repository `<reponame>`'
1276 1304 }
1277 1305 """
1278 1306
1279 1307 repo = get_repo_or_error(repoid)
1280 1308 if not has_superadmin_permission(apiuser):
1281 1309 # check if we have at least write permission for this repo !
1282 1310 _perms = ('repository.admin', 'repository.write',)
1283 1311 validate_repo_permissions(apiuser, repoid, repo, _perms)
1284 1312
1285 1313 # make sure normal user does not pass someone else userid,
1286 1314 # he is not allowed to do that
1287 1315 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1288 1316 raise JSONRPCError('userid is not the same as your user')
1289 1317
1290 1318 if isinstance(userid, Optional):
1291 1319 userid = apiuser.user_id
1292 1320
1293 1321 user = get_user_or_error(userid)
1294 1322
1295 1323 if isinstance(locked, Optional):
1296 1324 lockobj = repo.locked
1297 1325
1298 1326 if lockobj[0] is None:
1299 1327 _d = {
1300 1328 'repo': repo.repo_name,
1301 1329 'locked': False,
1302 1330 'locked_since': None,
1303 1331 'locked_by': None,
1304 1332 'lock_reason': None,
1305 1333 'lock_state_changed': False,
1306 1334 'msg': 'Repo `%s` not locked.' % repo.repo_name
1307 1335 }
1308 1336 return _d
1309 1337 else:
1310 1338 _user_id, _time, _reason = lockobj
1311 1339 lock_user = get_user_or_error(userid)
1312 1340 _d = {
1313 1341 'repo': repo.repo_name,
1314 1342 'locked': True,
1315 1343 'locked_since': _time,
1316 1344 'locked_by': lock_user.username,
1317 1345 'lock_reason': _reason,
1318 1346 'lock_state_changed': False,
1319 1347 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1320 1348 % (repo.repo_name, lock_user.username,
1321 1349 json.dumps(time_to_datetime(_time))))
1322 1350 }
1323 1351 return _d
1324 1352
1325 1353 # force locked state through a flag
1326 1354 else:
1327 1355 locked = str2bool(locked)
1328 1356 lock_reason = Repository.LOCK_API
1329 1357 try:
1330 1358 if locked:
1331 1359 lock_time = time.time()
1332 1360 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1333 1361 else:
1334 1362 lock_time = None
1335 1363 Repository.unlock(repo)
1336 1364 _d = {
1337 1365 'repo': repo.repo_name,
1338 1366 'locked': locked,
1339 1367 'locked_since': lock_time,
1340 1368 'locked_by': user.username,
1341 1369 'lock_reason': lock_reason,
1342 1370 'lock_state_changed': True,
1343 1371 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1344 1372 % (user.username, repo.repo_name, locked))
1345 1373 }
1346 1374 return _d
1347 1375 except Exception:
1348 1376 log.exception(
1349 1377 "Exception occurred while trying to lock repository")
1350 1378 raise JSONRPCError(
1351 1379 'Error occurred locking repository `%s`' % repo.repo_name
1352 1380 )
1353 1381
1354 1382
1355 1383 @jsonrpc_method()
1356 1384 def comment_commit(
1357 1385 request, apiuser, repoid, commit_id, message,
1358 1386 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1359 1387 """
1360 1388 Set a commit comment, and optionally change the status of the commit.
1361 1389
1362 1390 :param apiuser: This is filled automatically from the |authtoken|.
1363 1391 :type apiuser: AuthUser
1364 1392 :param repoid: Set the repository name or repository ID.
1365 1393 :type repoid: str or int
1366 1394 :param commit_id: Specify the commit_id for which to set a comment.
1367 1395 :type commit_id: str
1368 1396 :param message: The comment text.
1369 1397 :type message: str
1370 1398 :param userid: Set the user name of the comment creator.
1371 1399 :type userid: Optional(str or int)
1372 1400 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1373 1401 'under_review'
1374 1402 :type status: str
1375 1403
1376 1404 Example error output:
1377 1405
1378 1406 .. code-block:: json
1379 1407
1380 1408 {
1381 1409 "id" : <id_given_in_input>,
1382 1410 "result" : {
1383 1411 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1384 1412 "status_change": null or <status>,
1385 1413 "success": true
1386 1414 },
1387 1415 "error" : null
1388 1416 }
1389 1417
1390 1418 """
1391 1419 repo = get_repo_or_error(repoid)
1392 1420 if not has_superadmin_permission(apiuser):
1393 1421 _perms = ('repository.read', 'repository.write', 'repository.admin')
1394 1422 validate_repo_permissions(apiuser, repoid, repo, _perms)
1395 1423
1396 1424 if isinstance(userid, Optional):
1397 1425 userid = apiuser.user_id
1398 1426
1399 1427 user = get_user_or_error(userid)
1400 1428 status = Optional.extract(status)
1401 1429
1402 1430 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1403 1431 if status and status not in allowed_statuses:
1404 1432 raise JSONRPCError('Bad status, must be on '
1405 1433 'of %s got %s' % (allowed_statuses, status,))
1406 1434
1407 1435 try:
1408 1436 rc_config = SettingsModel().get_all_settings()
1409 1437 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1410 1438 status_change_label = ChangesetStatus.get_status_lbl(status)
1411 1439 comm = ChangesetCommentsModel().create(
1412 1440 message, repo, user, revision=commit_id,
1413 1441 status_change=status_change_label,
1414 1442 status_change_type=status,
1415 1443 renderer=renderer)
1416 1444 if status:
1417 1445 # also do a status change
1418 1446 try:
1419 1447 ChangesetStatusModel().set_status(
1420 1448 repo, status, user, comm, revision=commit_id,
1421 1449 dont_allow_on_closed_pull_request=True
1422 1450 )
1423 1451 except StatusChangeOnClosedPullRequestError:
1424 1452 log.exception(
1425 1453 "Exception occurred while trying to change repo commit status")
1426 1454 msg = ('Changing status on a changeset associated with '
1427 1455 'a closed pull request is not allowed')
1428 1456 raise JSONRPCError(msg)
1429 1457
1430 1458 Session().commit()
1431 1459 return {
1432 1460 'msg': (
1433 1461 'Commented on commit `%s` for repository `%s`' % (
1434 1462 comm.revision, repo.repo_name)),
1435 1463 'status_change': status,
1436 1464 'success': True,
1437 1465 }
1438 1466 except JSONRPCError:
1439 1467 # catch any inside errors, and re-raise them to prevent from
1440 1468 # below global catch to silence them
1441 1469 raise
1442 1470 except Exception:
1443 1471 log.exception("Exception occurred while trying to comment on commit")
1444 1472 raise JSONRPCError(
1445 1473 'failed to set comment on repository `%s`' % (repo.repo_name,)
1446 1474 )
1447 1475
1448 1476
1449 1477 @jsonrpc_method()
1450 1478 def grant_user_permission(request, apiuser, repoid, userid, perm):
1451 1479 """
1452 1480 Grant permissions for the specified user on the given repository,
1453 1481 or update existing permissions if found.
1454 1482
1455 1483 This command can only be run using an |authtoken| with admin
1456 1484 permissions on the |repo|.
1457 1485
1458 1486 :param apiuser: This is filled automatically from the |authtoken|.
1459 1487 :type apiuser: AuthUser
1460 1488 :param repoid: Set the repository name or repository ID.
1461 1489 :type repoid: str or int
1462 1490 :param userid: Set the user name.
1463 1491 :type userid: str
1464 1492 :param perm: Set the user permissions, using the following format
1465 1493 ``(repository.(none|read|write|admin))``
1466 1494 :type perm: str
1467 1495
1468 1496 Example output:
1469 1497
1470 1498 .. code-block:: bash
1471 1499
1472 1500 id : <id_given_in_input>
1473 1501 result: {
1474 1502 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1475 1503 "success": true
1476 1504 }
1477 1505 error: null
1478 1506 """
1479 1507
1480 1508 repo = get_repo_or_error(repoid)
1481 1509 user = get_user_or_error(userid)
1482 1510 perm = get_perm_or_error(perm)
1483 1511 if not has_superadmin_permission(apiuser):
1484 1512 _perms = ('repository.admin',)
1485 1513 validate_repo_permissions(apiuser, repoid, repo, _perms)
1486 1514
1487 1515 try:
1488 1516
1489 1517 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1490 1518
1491 1519 Session().commit()
1492 1520 return {
1493 1521 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1494 1522 perm.permission_name, user.username, repo.repo_name
1495 1523 ),
1496 1524 'success': True
1497 1525 }
1498 1526 except Exception:
1499 1527 log.exception(
1500 1528 "Exception occurred while trying edit permissions for repo")
1501 1529 raise JSONRPCError(
1502 1530 'failed to edit permission for user: `%s` in repo: `%s`' % (
1503 1531 userid, repoid
1504 1532 )
1505 1533 )
1506 1534
1507 1535
1508 1536 @jsonrpc_method()
1509 1537 def revoke_user_permission(request, apiuser, repoid, userid):
1510 1538 """
1511 1539 Revoke permission for a user on the specified repository.
1512 1540
1513 1541 This command can only be run using an |authtoken| with admin
1514 1542 permissions on the |repo|.
1515 1543
1516 1544 :param apiuser: This is filled automatically from the |authtoken|.
1517 1545 :type apiuser: AuthUser
1518 1546 :param repoid: Set the repository name or repository ID.
1519 1547 :type repoid: str or int
1520 1548 :param userid: Set the user name of revoked user.
1521 1549 :type userid: str or int
1522 1550
1523 1551 Example error output:
1524 1552
1525 1553 .. code-block:: bash
1526 1554
1527 1555 id : <id_given_in_input>
1528 1556 result: {
1529 1557 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1530 1558 "success": true
1531 1559 }
1532 1560 error: null
1533 1561 """
1534 1562
1535 1563 repo = get_repo_or_error(repoid)
1536 1564 user = get_user_or_error(userid)
1537 1565 if not has_superadmin_permission(apiuser):
1538 1566 _perms = ('repository.admin',)
1539 1567 validate_repo_permissions(apiuser, repoid, repo, _perms)
1540 1568
1541 1569 try:
1542 1570 RepoModel().revoke_user_permission(repo=repo, user=user)
1543 1571 Session().commit()
1544 1572 return {
1545 1573 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1546 1574 user.username, repo.repo_name
1547 1575 ),
1548 1576 'success': True
1549 1577 }
1550 1578 except Exception:
1551 1579 log.exception(
1552 1580 "Exception occurred while trying revoke permissions to repo")
1553 1581 raise JSONRPCError(
1554 1582 'failed to edit permission for user: `%s` in repo: `%s`' % (
1555 1583 userid, repoid
1556 1584 )
1557 1585 )
1558 1586
1559 1587
1560 1588 @jsonrpc_method()
1561 1589 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1562 1590 """
1563 1591 Grant permission for a user group on the specified repository,
1564 1592 or update existing permissions.
1565 1593
1566 1594 This command can only be run using an |authtoken| with admin
1567 1595 permissions on the |repo|.
1568 1596
1569 1597 :param apiuser: This is filled automatically from the |authtoken|.
1570 1598 :type apiuser: AuthUser
1571 1599 :param repoid: Set the repository name or repository ID.
1572 1600 :type repoid: str or int
1573 1601 :param usergroupid: Specify the ID of the user group.
1574 1602 :type usergroupid: str or int
1575 1603 :param perm: Set the user group permissions using the following
1576 1604 format: (repository.(none|read|write|admin))
1577 1605 :type perm: str
1578 1606
1579 1607 Example output:
1580 1608
1581 1609 .. code-block:: bash
1582 1610
1583 1611 id : <id_given_in_input>
1584 1612 result : {
1585 1613 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1586 1614 "success": true
1587 1615
1588 1616 }
1589 1617 error : null
1590 1618
1591 1619 Example error output:
1592 1620
1593 1621 .. code-block:: bash
1594 1622
1595 1623 id : <id_given_in_input>
1596 1624 result : null
1597 1625 error : {
1598 1626 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1599 1627 }
1600 1628
1601 1629 """
1602 1630
1603 1631 repo = get_repo_or_error(repoid)
1604 1632 perm = get_perm_or_error(perm)
1605 1633 if not has_superadmin_permission(apiuser):
1606 1634 _perms = ('repository.admin',)
1607 1635 validate_repo_permissions(apiuser, repoid, repo, _perms)
1608 1636
1609 1637 user_group = get_user_group_or_error(usergroupid)
1610 1638 if not has_superadmin_permission(apiuser):
1611 1639 # check if we have at least read permission for this user group !
1612 1640 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1613 1641 if not HasUserGroupPermissionAnyApi(*_perms)(
1614 1642 user=apiuser, user_group_name=user_group.users_group_name):
1615 1643 raise JSONRPCError(
1616 1644 'user group `%s` does not exist' % (usergroupid,))
1617 1645
1618 1646 try:
1619 1647 RepoModel().grant_user_group_permission(
1620 1648 repo=repo, group_name=user_group, perm=perm)
1621 1649
1622 1650 Session().commit()
1623 1651 return {
1624 1652 'msg': 'Granted perm: `%s` for user group: `%s` in '
1625 1653 'repo: `%s`' % (
1626 1654 perm.permission_name, user_group.users_group_name,
1627 1655 repo.repo_name
1628 1656 ),
1629 1657 'success': True
1630 1658 }
1631 1659 except Exception:
1632 1660 log.exception(
1633 1661 "Exception occurred while trying change permission on repo")
1634 1662 raise JSONRPCError(
1635 1663 'failed to edit permission for user group: `%s` in '
1636 1664 'repo: `%s`' % (
1637 1665 usergroupid, repo.repo_name
1638 1666 )
1639 1667 )
1640 1668
1641 1669
1642 1670 @jsonrpc_method()
1643 1671 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1644 1672 """
1645 1673 Revoke the permissions of a user group on a given repository.
1646 1674
1647 1675 This command can only be run using an |authtoken| with admin
1648 1676 permissions on the |repo|.
1649 1677
1650 1678 :param apiuser: This is filled automatically from the |authtoken|.
1651 1679 :type apiuser: AuthUser
1652 1680 :param repoid: Set the repository name or repository ID.
1653 1681 :type repoid: str or int
1654 1682 :param usergroupid: Specify the user group ID.
1655 1683 :type usergroupid: str or int
1656 1684
1657 1685 Example output:
1658 1686
1659 1687 .. code-block:: bash
1660 1688
1661 1689 id : <id_given_in_input>
1662 1690 result: {
1663 1691 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1664 1692 "success": true
1665 1693 }
1666 1694 error: null
1667 1695 """
1668 1696
1669 1697 repo = get_repo_or_error(repoid)
1670 1698 if not has_superadmin_permission(apiuser):
1671 1699 _perms = ('repository.admin',)
1672 1700 validate_repo_permissions(apiuser, repoid, repo, _perms)
1673 1701
1674 1702 user_group = get_user_group_or_error(usergroupid)
1675 1703 if not has_superadmin_permission(apiuser):
1676 1704 # check if we have at least read permission for this user group !
1677 1705 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1678 1706 if not HasUserGroupPermissionAnyApi(*_perms)(
1679 1707 user=apiuser, user_group_name=user_group.users_group_name):
1680 1708 raise JSONRPCError(
1681 1709 'user group `%s` does not exist' % (usergroupid,))
1682 1710
1683 1711 try:
1684 1712 RepoModel().revoke_user_group_permission(
1685 1713 repo=repo, group_name=user_group)
1686 1714
1687 1715 Session().commit()
1688 1716 return {
1689 1717 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1690 1718 user_group.users_group_name, repo.repo_name
1691 1719 ),
1692 1720 'success': True
1693 1721 }
1694 1722 except Exception:
1695 1723 log.exception("Exception occurred while trying revoke "
1696 1724 "user group permission on repo")
1697 1725 raise JSONRPCError(
1698 1726 'failed to edit permission for user group: `%s` in '
1699 1727 'repo: `%s`' % (
1700 1728 user_group.users_group_name, repo.repo_name
1701 1729 )
1702 1730 )
1703 1731
1704 1732
1705 1733 @jsonrpc_method()
1706 1734 def pull(request, apiuser, repoid):
1707 1735 """
1708 1736 Triggers a pull on the given repository from a remote location. You
1709 1737 can use this to keep remote repositories up-to-date.
1710 1738
1711 1739 This command can only be run using an |authtoken| with admin
1712 1740 rights to the specified repository. For more information,
1713 1741 see :ref:`config-token-ref`.
1714 1742
1715 1743 This command takes the following options:
1716 1744
1717 1745 :param apiuser: This is filled automatically from the |authtoken|.
1718 1746 :type apiuser: AuthUser
1719 1747 :param repoid: The repository name or repository ID.
1720 1748 :type repoid: str or int
1721 1749
1722 1750 Example output:
1723 1751
1724 1752 .. code-block:: bash
1725 1753
1726 1754 id : <id_given_in_input>
1727 1755 result : {
1728 1756 "msg": "Pulled from `<repository name>`"
1729 1757 "repository": "<repository name>"
1730 1758 }
1731 1759 error : null
1732 1760
1733 1761 Example error output:
1734 1762
1735 1763 .. code-block:: bash
1736 1764
1737 1765 id : <id_given_in_input>
1738 1766 result : null
1739 1767 error : {
1740 1768 "Unable to pull changes from `<reponame>`"
1741 1769 }
1742 1770
1743 1771 """
1744 1772
1745 1773 repo = get_repo_or_error(repoid)
1746 1774 if not has_superadmin_permission(apiuser):
1747 1775 _perms = ('repository.admin',)
1748 1776 validate_repo_permissions(apiuser, repoid, repo, _perms)
1749 1777
1750 1778 try:
1751 1779 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1752 1780 return {
1753 1781 'msg': 'Pulled from `%s`' % repo.repo_name,
1754 1782 'repository': repo.repo_name
1755 1783 }
1756 1784 except Exception:
1757 1785 log.exception("Exception occurred while trying to "
1758 1786 "pull changes from remote location")
1759 1787 raise JSONRPCError(
1760 1788 'Unable to pull changes from `%s`' % repo.repo_name
1761 1789 )
1762 1790
1763 1791
1764 1792 @jsonrpc_method()
1765 1793 def strip(request, apiuser, repoid, revision, branch):
1766 1794 """
1767 1795 Strips the given revision from the specified repository.
1768 1796
1769 1797 * This will remove the revision and all of its decendants.
1770 1798
1771 1799 This command can only be run using an |authtoken| with admin rights to
1772 1800 the specified repository.
1773 1801
1774 1802 This command takes the following options:
1775 1803
1776 1804 :param apiuser: This is filled automatically from the |authtoken|.
1777 1805 :type apiuser: AuthUser
1778 1806 :param repoid: The repository name or repository ID.
1779 1807 :type repoid: str or int
1780 1808 :param revision: The revision you wish to strip.
1781 1809 :type revision: str
1782 1810 :param branch: The branch from which to strip the revision.
1783 1811 :type branch: str
1784 1812
1785 1813 Example output:
1786 1814
1787 1815 .. code-block:: bash
1788 1816
1789 1817 id : <id_given_in_input>
1790 1818 result : {
1791 1819 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1792 1820 "repository": "<repository name>"
1793 1821 }
1794 1822 error : null
1795 1823
1796 1824 Example error output:
1797 1825
1798 1826 .. code-block:: bash
1799 1827
1800 1828 id : <id_given_in_input>
1801 1829 result : null
1802 1830 error : {
1803 1831 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1804 1832 }
1805 1833
1806 1834 """
1807 1835
1808 1836 repo = get_repo_or_error(repoid)
1809 1837 if not has_superadmin_permission(apiuser):
1810 1838 _perms = ('repository.admin',)
1811 1839 validate_repo_permissions(apiuser, repoid, repo, _perms)
1812 1840
1813 1841 try:
1814 1842 ScmModel().strip(repo, revision, branch)
1815 1843 return {
1816 1844 'msg': 'Stripped commit %s from repo `%s`' % (
1817 1845 revision, repo.repo_name),
1818 1846 'repository': repo.repo_name
1819 1847 }
1820 1848 except Exception:
1821 1849 log.exception("Exception while trying to strip")
1822 1850 raise JSONRPCError(
1823 1851 'Unable to strip commit %s from repo `%s`' % (
1824 1852 revision, repo.repo_name)
1825 1853 )
1826 1854
1827 1855
1828 1856 @jsonrpc_method()
1829 1857 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1830 1858 """
1831 1859 Returns all settings for a repository. If key is given it only returns the
1832 1860 setting identified by the key or null.
1833 1861
1834 1862 :param apiuser: This is filled automatically from the |authtoken|.
1835 1863 :type apiuser: AuthUser
1836 1864 :param repoid: The repository name or repository id.
1837 1865 :type repoid: str or int
1838 1866 :param key: Key of the setting to return.
1839 1867 :type: key: Optional(str)
1840 1868
1841 1869 Example output:
1842 1870
1843 1871 .. code-block:: bash
1844 1872
1845 1873 {
1846 1874 "error": null,
1847 1875 "id": 237,
1848 1876 "result": {
1849 1877 "extensions_largefiles": true,
1850 1878 "hooks_changegroup_push_logger": true,
1851 1879 "hooks_changegroup_repo_size": false,
1852 1880 "hooks_outgoing_pull_logger": true,
1853 1881 "phases_publish": "True",
1854 1882 "rhodecode_hg_use_rebase_for_merging": true,
1855 1883 "rhodecode_pr_merge_enabled": true,
1856 1884 "rhodecode_use_outdated_comments": true
1857 1885 }
1858 1886 }
1859 1887 """
1860 1888
1861 1889 # Restrict access to this api method to admins only.
1862 1890 if not has_superadmin_permission(apiuser):
1863 1891 raise JSONRPCForbidden()
1864 1892
1865 1893 try:
1866 1894 repo = get_repo_or_error(repoid)
1867 1895 settings_model = VcsSettingsModel(repo=repo)
1868 1896 settings = settings_model.get_global_settings()
1869 1897 settings.update(settings_model.get_repo_settings())
1870 1898
1871 1899 # If only a single setting is requested fetch it from all settings.
1872 1900 key = Optional.extract(key)
1873 1901 if key is not None:
1874 1902 settings = settings.get(key, None)
1875 1903 except Exception:
1876 1904 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1877 1905 log.exception(msg)
1878 1906 raise JSONRPCError(msg)
1879 1907
1880 1908 return settings
1881 1909
1882 1910
1883 1911 @jsonrpc_method()
1884 1912 def set_repo_settings(request, apiuser, repoid, settings):
1885 1913 """
1886 1914 Update repository settings. Returns true on success.
1887 1915
1888 1916 :param apiuser: This is filled automatically from the |authtoken|.
1889 1917 :type apiuser: AuthUser
1890 1918 :param repoid: The repository name or repository id.
1891 1919 :type repoid: str or int
1892 1920 :param settings: The new settings for the repository.
1893 1921 :type: settings: dict
1894 1922
1895 1923 Example output:
1896 1924
1897 1925 .. code-block:: bash
1898 1926
1899 1927 {
1900 1928 "error": null,
1901 1929 "id": 237,
1902 1930 "result": true
1903 1931 }
1904 1932 """
1905 1933 # Restrict access to this api method to admins only.
1906 1934 if not has_superadmin_permission(apiuser):
1907 1935 raise JSONRPCForbidden()
1908 1936
1909 1937 if type(settings) is not dict:
1910 1938 raise JSONRPCError('Settings have to be a JSON Object.')
1911 1939
1912 1940 try:
1913 1941 settings_model = VcsSettingsModel(repo=repoid)
1914 1942
1915 1943 # Merge global, repo and incoming settings.
1916 1944 new_settings = settings_model.get_global_settings()
1917 1945 new_settings.update(settings_model.get_repo_settings())
1918 1946 new_settings.update(settings)
1919 1947
1920 1948 # Update the settings.
1921 1949 inherit_global_settings = new_settings.get(
1922 1950 'inherit_global_settings', False)
1923 1951 settings_model.create_or_update_repo_settings(
1924 1952 new_settings, inherit_global_settings=inherit_global_settings)
1925 1953 Session().commit()
1926 1954 except Exception:
1927 1955 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1928 1956 log.exception(msg)
1929 1957 raise JSONRPCError(msg)
1930 1958
1931 1959 # Indicate success.
1932 1960 return True
@@ -1,1057 +1,1070 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Repository model for rhodecode
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29 import time
30 30 import traceback
31 31 from datetime import datetime
32 32
33 33 from sqlalchemy.sql import func
34 34 from sqlalchemy.sql.expression import true, or_
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode import events
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import HasUserGroupPermissionAny
40 40 from rhodecode.lib.caching_query import FromCache
41 41 from rhodecode.lib.exceptions import AttachedForksError
42 42 from rhodecode.lib.hooks_base import log_delete_repository
43 43 from rhodecode.lib.markup_renderer import MarkupRenderer
44 44 from rhodecode.lib.utils import make_db_config
45 45 from rhodecode.lib.utils2 import (
46 46 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
47 47 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
48 48 from rhodecode.lib.vcs.backends import get_backend
49 49 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
50 50 from rhodecode.model import BaseModel
51 51 from rhodecode.model.db import (
52 52 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
53 53 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
54 54 RepoGroup, RepositoryField)
55 55 from rhodecode.model.scm import UserGroupList
56 56 from rhodecode.model.settings import VcsSettingsModel
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class RepoModel(BaseModel):
63 63
64 64 cls = Repository
65 65
66 66 def _get_user_group(self, users_group):
67 67 return self._get_instance(UserGroup, users_group,
68 68 callback=UserGroup.get_by_group_name)
69 69
70 70 def _get_repo_group(self, repo_group):
71 71 return self._get_instance(RepoGroup, repo_group,
72 72 callback=RepoGroup.get_by_group_name)
73 73
74 74 def _create_default_perms(self, repository, private):
75 75 # create default permission
76 76 default = 'repository.read'
77 77 def_user = User.get_default_user()
78 78 for p in def_user.user_perms:
79 79 if p.permission.permission_name.startswith('repository.'):
80 80 default = p.permission.permission_name
81 81 break
82 82
83 83 default_perm = 'repository.none' if private else default
84 84
85 85 repo_to_perm = UserRepoToPerm()
86 86 repo_to_perm.permission = Permission.get_by_key(default_perm)
87 87
88 88 repo_to_perm.repository = repository
89 89 repo_to_perm.user_id = def_user.user_id
90 90
91 91 return repo_to_perm
92 92
93 93 @LazyProperty
94 94 def repos_path(self):
95 95 """
96 96 Gets the repositories root path from database
97 97 """
98 98 settings_model = VcsSettingsModel(sa=self.sa)
99 99 return settings_model.get_repos_location()
100 100
101 101 def get(self, repo_id, cache=False):
102 102 repo = self.sa.query(Repository) \
103 103 .filter(Repository.repo_id == repo_id)
104 104
105 105 if cache:
106 106 repo = repo.options(FromCache("sql_cache_short",
107 107 "get_repo_%s" % repo_id))
108 108 return repo.scalar()
109 109
110 110 def get_repo(self, repository):
111 111 return self._get_repo(repository)
112 112
113 113 def get_by_repo_name(self, repo_name, cache=False):
114 114 repo = self.sa.query(Repository) \
115 115 .filter(Repository.repo_name == repo_name)
116 116
117 117 if cache:
118 118 repo = repo.options(FromCache("sql_cache_short",
119 119 "get_repo_%s" % repo_name))
120 120 return repo.scalar()
121 121
122 122 def _extract_id_from_repo_name(self, repo_name):
123 123 if repo_name.startswith('/'):
124 124 repo_name = repo_name.lstrip('/')
125 125 by_id_match = re.match(r'^_(\d{1,})', repo_name)
126 126 if by_id_match:
127 127 return by_id_match.groups()[0]
128 128
129 129 def get_repo_by_id(self, repo_name):
130 130 """
131 131 Extracts repo_name by id from special urls.
132 132 Example url is _11/repo_name
133 133
134 134 :param repo_name:
135 135 :return: repo object if matched else None
136 136 """
137 137 try:
138 138 _repo_id = self._extract_id_from_repo_name(repo_name)
139 139 if _repo_id:
140 140 return self.get(_repo_id)
141 141 except Exception:
142 142 log.exception('Failed to extract repo_name from URL')
143 143
144 144 return None
145 145
146 def get_repos_for_root(self, root, traverse=False):
147 if traverse:
148 like_expression = u'{}%'.format(safe_unicode(root))
149 repos = Repository.query().filter(
150 Repository.repo_name.like(like_expression)).all()
151 else:
152 if root and not isinstance(root, RepoGroup):
153 raise ValueError(
154 'Root must be an instance '
155 'of RepoGroup, got:{} instead'.format(type(root)))
156 repos = Repository.query().filter(Repository.group == root).all()
157 return repos
158
146 159 def get_url(self, repo):
147 160 return h.url('summary_home', repo_name=safe_str(repo.repo_name),
148 161 qualified=True)
149 162
150 163 def get_users(self, name_contains=None, limit=20, only_active=True):
151 164
152 165 # TODO: mikhail: move this method to the UserModel.
153 166 query = self.sa.query(User)
154 167 if only_active:
155 168 query = query.filter(User.active == true())
156 169
157 170 if name_contains:
158 171 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
159 172 query = query.filter(
160 173 or_(
161 174 User.name.ilike(ilike_expression),
162 175 User.lastname.ilike(ilike_expression),
163 176 User.username.ilike(ilike_expression)
164 177 )
165 178 )
166 179 query = query.limit(limit)
167 180 users = query.all()
168 181
169 182 _users = [
170 183 {
171 184 'id': user.user_id,
172 185 'first_name': user.name,
173 186 'last_name': user.lastname,
174 187 'username': user.username,
175 188 'email': user.email,
176 189 'icon_link': h.gravatar_url(user.email, 30),
177 190 'value_display': h.person(user),
178 191 'value': user.username,
179 192 'value_type': 'user',
180 193 'active': user.active,
181 194 }
182 195 for user in users
183 196 ]
184 197 return _users
185 198
186 199 def get_user_groups(self, name_contains=None, limit=20, only_active=True):
187 200 # TODO: mikhail: move this method to the UserGroupModel.
188 201 query = self.sa.query(UserGroup)
189 202 if only_active:
190 203 query = query.filter(UserGroup.users_group_active == true())
191 204
192 205 if name_contains:
193 206 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
194 207 query = query.filter(
195 208 UserGroup.users_group_name.ilike(ilike_expression))\
196 209 .order_by(func.length(UserGroup.users_group_name))\
197 210 .order_by(UserGroup.users_group_name)
198 211
199 212 query = query.limit(limit)
200 213 user_groups = query.all()
201 214 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
202 215 user_groups = UserGroupList(user_groups, perm_set=perm_set)
203 216
204 217 _groups = [
205 218 {
206 219 'id': group.users_group_id,
207 220 # TODO: marcink figure out a way to generate the url for the
208 221 # icon
209 222 'icon_link': '',
210 223 'value_display': 'Group: %s (%d members)' % (
211 224 group.users_group_name, len(group.members),),
212 225 'value': group.users_group_name,
213 226 'value_type': 'user_group',
214 227 'active': group.users_group_active,
215 228 }
216 229 for group in user_groups
217 230 ]
218 231 return _groups
219 232
220 233 @classmethod
221 234 def update_repoinfo(cls, repositories=None):
222 235 if not repositories:
223 236 repositories = Repository.getAll()
224 237 for repo in repositories:
225 238 repo.update_commit_cache()
226 239
227 240 def get_repos_as_dict(self, repo_list=None, admin=False,
228 241 super_user_actions=False):
229 242
230 243 from rhodecode.lib.utils import PartialRenderer
231 244 _render = PartialRenderer('data_table/_dt_elements.html')
232 245 c = _render.c
233 246
234 247 def quick_menu(repo_name):
235 248 return _render('quick_menu', repo_name)
236 249
237 250 def repo_lnk(name, rtype, rstate, private, fork_of):
238 251 return _render('repo_name', name, rtype, rstate, private, fork_of,
239 252 short_name=not admin, admin=False)
240 253
241 254 def last_change(last_change):
242 255 return _render("last_change", last_change)
243 256
244 257 def rss_lnk(repo_name):
245 258 return _render("rss", repo_name)
246 259
247 260 def atom_lnk(repo_name):
248 261 return _render("atom", repo_name)
249 262
250 263 def last_rev(repo_name, cs_cache):
251 264 return _render('revision', repo_name, cs_cache.get('revision'),
252 265 cs_cache.get('raw_id'), cs_cache.get('author'),
253 266 cs_cache.get('message'))
254 267
255 268 def desc(desc):
256 269 if c.visual.stylify_metatags:
257 270 desc = h.urlify_text(h.escaped_stylize(desc))
258 271 else:
259 272 desc = h.urlify_text(h.html_escape(desc))
260 273
261 274 return _render('repo_desc', desc)
262 275
263 276 def state(repo_state):
264 277 return _render("repo_state", repo_state)
265 278
266 279 def repo_actions(repo_name):
267 280 return _render('repo_actions', repo_name, super_user_actions)
268 281
269 282 def user_profile(username):
270 283 return _render('user_profile', username)
271 284
272 285 repos_data = []
273 286 for repo in repo_list:
274 287 cs_cache = repo.changeset_cache
275 288 row = {
276 289 "menu": quick_menu(repo.repo_name),
277 290
278 291 "name": repo_lnk(repo.repo_name, repo.repo_type,
279 292 repo.repo_state, repo.private, repo.fork),
280 293 "name_raw": repo.repo_name.lower(),
281 294
282 295 "last_change": last_change(repo.last_db_change),
283 296 "last_change_raw": datetime_to_time(repo.last_db_change),
284 297
285 298 "last_changeset": last_rev(repo.repo_name, cs_cache),
286 299 "last_changeset_raw": cs_cache.get('revision'),
287 300
288 301 "desc": desc(repo.description),
289 302 "owner": user_profile(repo.user.username),
290 303
291 304 "state": state(repo.repo_state),
292 305 "rss": rss_lnk(repo.repo_name),
293 306
294 307 "atom": atom_lnk(repo.repo_name),
295 308 }
296 309 if admin:
297 310 row.update({
298 311 "action": repo_actions(repo.repo_name),
299 312 })
300 313 repos_data.append(row)
301 314
302 315 return repos_data
303 316
304 317 def _get_defaults(self, repo_name):
305 318 """
306 319 Gets information about repository, and returns a dict for
307 320 usage in forms
308 321
309 322 :param repo_name:
310 323 """
311 324
312 325 repo_info = Repository.get_by_repo_name(repo_name)
313 326
314 327 if repo_info is None:
315 328 return None
316 329
317 330 defaults = repo_info.get_dict()
318 331 defaults['repo_name'] = repo_info.just_name
319 332
320 333 groups = repo_info.groups_with_parents
321 334 parent_group = groups[-1] if groups else None
322 335
323 336 # we use -1 as this is how in HTML, we mark an empty group
324 337 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
325 338
326 339 keys_to_process = (
327 340 {'k': 'repo_type', 'strip': False},
328 341 {'k': 'repo_enable_downloads', 'strip': True},
329 342 {'k': 'repo_description', 'strip': True},
330 343 {'k': 'repo_enable_locking', 'strip': True},
331 344 {'k': 'repo_landing_rev', 'strip': True},
332 345 {'k': 'clone_uri', 'strip': False},
333 346 {'k': 'repo_private', 'strip': True},
334 347 {'k': 'repo_enable_statistics', 'strip': True}
335 348 )
336 349
337 350 for item in keys_to_process:
338 351 attr = item['k']
339 352 if item['strip']:
340 353 attr = remove_prefix(item['k'], 'repo_')
341 354
342 355 val = defaults[attr]
343 356 if item['k'] == 'repo_landing_rev':
344 357 val = ':'.join(defaults[attr])
345 358 defaults[item['k']] = val
346 359 if item['k'] == 'clone_uri':
347 360 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
348 361
349 362 # fill owner
350 363 if repo_info.user:
351 364 defaults.update({'user': repo_info.user.username})
352 365 else:
353 366 replacement_user = User.get_first_super_admin().username
354 367 defaults.update({'user': replacement_user})
355 368
356 369 # fill repository users
357 370 for p in repo_info.repo_to_perm:
358 371 defaults.update({'u_perm_%s' % p.user.user_id:
359 372 p.permission.permission_name})
360 373
361 374 # fill repository groups
362 375 for p in repo_info.users_group_to_perm:
363 376 defaults.update({'g_perm_%s' % p.users_group.users_group_id:
364 377 p.permission.permission_name})
365 378
366 379 return defaults
367 380
368 381 def update(self, repo, **kwargs):
369 382 try:
370 383 cur_repo = self._get_repo(repo)
371 384 source_repo_name = cur_repo.repo_name
372 385 if 'user' in kwargs:
373 386 cur_repo.user = User.get_by_username(kwargs['user'])
374 387
375 388 if 'repo_group' in kwargs:
376 389 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
377 390 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
378 391
379 392 update_keys = [
380 393 (1, 'repo_description'),
381 394 (1, 'repo_landing_rev'),
382 395 (1, 'repo_private'),
383 396 (1, 'repo_enable_downloads'),
384 397 (1, 'repo_enable_locking'),
385 398 (1, 'repo_enable_statistics'),
386 399 (0, 'clone_uri'),
387 400 (0, 'fork_id')
388 401 ]
389 402 for strip, k in update_keys:
390 403 if k in kwargs:
391 404 val = kwargs[k]
392 405 if strip:
393 406 k = remove_prefix(k, 'repo_')
394 407 if k == 'clone_uri':
395 408 from rhodecode.model.validators import Missing
396 409 _change = kwargs.get('clone_uri_change')
397 410 if _change in [Missing, 'OLD']:
398 411 # we don't change the value, so use original one
399 412 val = cur_repo.clone_uri
400 413
401 414 setattr(cur_repo, k, val)
402 415
403 416 new_name = cur_repo.get_new_name(kwargs['repo_name'])
404 417 cur_repo.repo_name = new_name
405 418
406 419 # if private flag is set, reset default permission to NONE
407 420 if kwargs.get('repo_private'):
408 421 EMPTY_PERM = 'repository.none'
409 422 RepoModel().grant_user_permission(
410 423 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
411 424 )
412 425
413 426 # handle extra fields
414 427 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
415 428 kwargs):
416 429 k = RepositoryField.un_prefix_key(field)
417 430 ex_field = RepositoryField.get_by_key_name(
418 431 key=k, repo=cur_repo)
419 432 if ex_field:
420 433 ex_field.field_value = kwargs[field]
421 434 self.sa.add(ex_field)
422 435 self.sa.add(cur_repo)
423 436
424 437 if source_repo_name != new_name:
425 438 # rename repository
426 439 self._rename_filesystem_repo(
427 440 old=source_repo_name, new=new_name)
428 441
429 442 return cur_repo
430 443 except Exception:
431 444 log.error(traceback.format_exc())
432 445 raise
433 446
434 447 def _create_repo(self, repo_name, repo_type, description, owner,
435 448 private=False, clone_uri=None, repo_group=None,
436 449 landing_rev='rev:tip', fork_of=None,
437 450 copy_fork_permissions=False, enable_statistics=False,
438 451 enable_locking=False, enable_downloads=False,
439 452 copy_group_permissions=False,
440 453 state=Repository.STATE_PENDING):
441 454 """
442 455 Create repository inside database with PENDING state, this should be
443 456 only executed by create() repo. With exception of importing existing
444 457 repos
445 458 """
446 459 from rhodecode.model.scm import ScmModel
447 460
448 461 owner = self._get_user(owner)
449 462 fork_of = self._get_repo(fork_of)
450 463 repo_group = self._get_repo_group(safe_int(repo_group))
451 464
452 465 try:
453 466 repo_name = safe_unicode(repo_name)
454 467 description = safe_unicode(description)
455 468 # repo name is just a name of repository
456 469 # while repo_name_full is a full qualified name that is combined
457 470 # with name and path of group
458 471 repo_name_full = repo_name
459 472 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
460 473
461 474 new_repo = Repository()
462 475 new_repo.repo_state = state
463 476 new_repo.enable_statistics = False
464 477 new_repo.repo_name = repo_name_full
465 478 new_repo.repo_type = repo_type
466 479 new_repo.user = owner
467 480 new_repo.group = repo_group
468 481 new_repo.description = description or repo_name
469 482 new_repo.private = private
470 483 new_repo.clone_uri = clone_uri
471 484 new_repo.landing_rev = landing_rev
472 485
473 486 new_repo.enable_statistics = enable_statistics
474 487 new_repo.enable_locking = enable_locking
475 488 new_repo.enable_downloads = enable_downloads
476 489
477 490 if repo_group:
478 491 new_repo.enable_locking = repo_group.enable_locking
479 492
480 493 if fork_of:
481 494 parent_repo = fork_of
482 495 new_repo.fork = parent_repo
483 496
484 497 events.trigger(events.RepoPreCreateEvent(new_repo))
485 498
486 499 self.sa.add(new_repo)
487 500
488 501 EMPTY_PERM = 'repository.none'
489 502 if fork_of and copy_fork_permissions:
490 503 repo = fork_of
491 504 user_perms = UserRepoToPerm.query() \
492 505 .filter(UserRepoToPerm.repository == repo).all()
493 506 group_perms = UserGroupRepoToPerm.query() \
494 507 .filter(UserGroupRepoToPerm.repository == repo).all()
495 508
496 509 for perm in user_perms:
497 510 UserRepoToPerm.create(
498 511 perm.user, new_repo, perm.permission)
499 512
500 513 for perm in group_perms:
501 514 UserGroupRepoToPerm.create(
502 515 perm.users_group, new_repo, perm.permission)
503 516 # in case we copy permissions and also set this repo to private
504 517 # override the default user permission to make it a private
505 518 # repo
506 519 if private:
507 520 RepoModel(self.sa).grant_user_permission(
508 521 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
509 522
510 523 elif repo_group and copy_group_permissions:
511 524 user_perms = UserRepoGroupToPerm.query() \
512 525 .filter(UserRepoGroupToPerm.group == repo_group).all()
513 526
514 527 group_perms = UserGroupRepoGroupToPerm.query() \
515 528 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
516 529
517 530 for perm in user_perms:
518 531 perm_name = perm.permission.permission_name.replace(
519 532 'group.', 'repository.')
520 533 perm_obj = Permission.get_by_key(perm_name)
521 534 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
522 535
523 536 for perm in group_perms:
524 537 perm_name = perm.permission.permission_name.replace(
525 538 'group.', 'repository.')
526 539 perm_obj = Permission.get_by_key(perm_name)
527 540 UserGroupRepoToPerm.create(
528 541 perm.users_group, new_repo, perm_obj)
529 542
530 543 if private:
531 544 RepoModel(self.sa).grant_user_permission(
532 545 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
533 546
534 547 else:
535 548 perm_obj = self._create_default_perms(new_repo, private)
536 549 self.sa.add(perm_obj)
537 550
538 551 # now automatically start following this repository as owner
539 552 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
540 553 owner.user_id)
541 554
542 555 # we need to flush here, in order to check if database won't
543 556 # throw any exceptions, create filesystem dirs at the very end
544 557 self.sa.flush()
545 558 events.trigger(events.RepoCreateEvent(new_repo))
546 559 return new_repo
547 560
548 561 except Exception:
549 562 log.error(traceback.format_exc())
550 563 raise
551 564
552 565 def create(self, form_data, cur_user):
553 566 """
554 567 Create repository using celery tasks
555 568
556 569 :param form_data:
557 570 :param cur_user:
558 571 """
559 572 from rhodecode.lib.celerylib import tasks, run_task
560 573 return run_task(tasks.create_repo, form_data, cur_user)
561 574
562 575 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
563 576 perm_deletions=None, check_perms=True,
564 577 cur_user=None):
565 578 if not perm_additions:
566 579 perm_additions = []
567 580 if not perm_updates:
568 581 perm_updates = []
569 582 if not perm_deletions:
570 583 perm_deletions = []
571 584
572 585 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
573 586
574 587 # update permissions
575 588 for member_id, perm, member_type in perm_updates:
576 589 member_id = int(member_id)
577 590 if member_type == 'user':
578 591 # this updates also current one if found
579 592 self.grant_user_permission(
580 593 repo=repo, user=member_id, perm=perm)
581 594 else: # set for user group
582 595 # check if we have permissions to alter this usergroup
583 596 member_name = UserGroup.get(member_id).users_group_name
584 597 if not check_perms or HasUserGroupPermissionAny(
585 598 *req_perms)(member_name, user=cur_user):
586 599 self.grant_user_group_permission(
587 600 repo=repo, group_name=member_id, perm=perm)
588 601
589 602 # set new permissions
590 603 for member_id, perm, member_type in perm_additions:
591 604 member_id = int(member_id)
592 605 if member_type == 'user':
593 606 self.grant_user_permission(
594 607 repo=repo, user=member_id, perm=perm)
595 608 else: # set for user group
596 609 # check if we have permissions to alter this usergroup
597 610 member_name = UserGroup.get(member_id).users_group_name
598 611 if not check_perms or HasUserGroupPermissionAny(
599 612 *req_perms)(member_name, user=cur_user):
600 613 self.grant_user_group_permission(
601 614 repo=repo, group_name=member_id, perm=perm)
602 615
603 616 # delete permissions
604 617 for member_id, perm, member_type in perm_deletions:
605 618 member_id = int(member_id)
606 619 if member_type == 'user':
607 620 self.revoke_user_permission(repo=repo, user=member_id)
608 621 else: # set for user group
609 622 # check if we have permissions to alter this usergroup
610 623 member_name = UserGroup.get(member_id).users_group_name
611 624 if not check_perms or HasUserGroupPermissionAny(
612 625 *req_perms)(member_name, user=cur_user):
613 626 self.revoke_user_group_permission(
614 627 repo=repo, group_name=member_id)
615 628
616 629 def create_fork(self, form_data, cur_user):
617 630 """
618 631 Simple wrapper into executing celery task for fork creation
619 632
620 633 :param form_data:
621 634 :param cur_user:
622 635 """
623 636 from rhodecode.lib.celerylib import tasks, run_task
624 637 return run_task(tasks.create_repo_fork, form_data, cur_user)
625 638
626 639 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
627 640 """
628 641 Delete given repository, forks parameter defines what do do with
629 642 attached forks. Throws AttachedForksError if deleted repo has attached
630 643 forks
631 644
632 645 :param repo:
633 646 :param forks: str 'delete' or 'detach'
634 647 :param fs_remove: remove(archive) repo from filesystem
635 648 """
636 649 if not cur_user:
637 650 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
638 651 repo = self._get_repo(repo)
639 652 if repo:
640 653 if forks == 'detach':
641 654 for r in repo.forks:
642 655 r.fork = None
643 656 self.sa.add(r)
644 657 elif forks == 'delete':
645 658 for r in repo.forks:
646 659 self.delete(r, forks='delete')
647 660 elif [f for f in repo.forks]:
648 661 raise AttachedForksError()
649 662
650 663 old_repo_dict = repo.get_dict()
651 664 events.trigger(events.RepoPreDeleteEvent(repo))
652 665 try:
653 666 self.sa.delete(repo)
654 667 if fs_remove:
655 668 self._delete_filesystem_repo(repo)
656 669 else:
657 670 log.debug('skipping removal from filesystem')
658 671 old_repo_dict.update({
659 672 'deleted_by': cur_user,
660 673 'deleted_on': time.time(),
661 674 })
662 675 log_delete_repository(**old_repo_dict)
663 676 events.trigger(events.RepoDeleteEvent(repo))
664 677 except Exception:
665 678 log.error(traceback.format_exc())
666 679 raise
667 680
668 681 def grant_user_permission(self, repo, user, perm):
669 682 """
670 683 Grant permission for user on given repository, or update existing one
671 684 if found
672 685
673 686 :param repo: Instance of Repository, repository_id, or repository name
674 687 :param user: Instance of User, user_id or username
675 688 :param perm: Instance of Permission, or permission_name
676 689 """
677 690 user = self._get_user(user)
678 691 repo = self._get_repo(repo)
679 692 permission = self._get_perm(perm)
680 693
681 694 # check if we have that permission already
682 695 obj = self.sa.query(UserRepoToPerm) \
683 696 .filter(UserRepoToPerm.user == user) \
684 697 .filter(UserRepoToPerm.repository == repo) \
685 698 .scalar()
686 699 if obj is None:
687 700 # create new !
688 701 obj = UserRepoToPerm()
689 702 obj.repository = repo
690 703 obj.user = user
691 704 obj.permission = permission
692 705 self.sa.add(obj)
693 706 log.debug('Granted perm %s to %s on %s', perm, user, repo)
694 707 action_logger_generic(
695 708 'granted permission: {} to user: {} on repo: {}'.format(
696 709 perm, user, repo), namespace='security.repo')
697 710 return obj
698 711
699 712 def revoke_user_permission(self, repo, user):
700 713 """
701 714 Revoke permission for user on given repository
702 715
703 716 :param repo: Instance of Repository, repository_id, or repository name
704 717 :param user: Instance of User, user_id or username
705 718 """
706 719
707 720 user = self._get_user(user)
708 721 repo = self._get_repo(repo)
709 722
710 723 obj = self.sa.query(UserRepoToPerm) \
711 724 .filter(UserRepoToPerm.repository == repo) \
712 725 .filter(UserRepoToPerm.user == user) \
713 726 .scalar()
714 727 if obj:
715 728 self.sa.delete(obj)
716 729 log.debug('Revoked perm on %s on %s', repo, user)
717 730 action_logger_generic(
718 731 'revoked permission from user: {} on repo: {}'.format(
719 732 user, repo), namespace='security.repo')
720 733
721 734 def grant_user_group_permission(self, repo, group_name, perm):
722 735 """
723 736 Grant permission for user group on given repository, or update
724 737 existing one if found
725 738
726 739 :param repo: Instance of Repository, repository_id, or repository name
727 740 :param group_name: Instance of UserGroup, users_group_id,
728 741 or user group name
729 742 :param perm: Instance of Permission, or permission_name
730 743 """
731 744 repo = self._get_repo(repo)
732 745 group_name = self._get_user_group(group_name)
733 746 permission = self._get_perm(perm)
734 747
735 748 # check if we have that permission already
736 749 obj = self.sa.query(UserGroupRepoToPerm) \
737 750 .filter(UserGroupRepoToPerm.users_group == group_name) \
738 751 .filter(UserGroupRepoToPerm.repository == repo) \
739 752 .scalar()
740 753
741 754 if obj is None:
742 755 # create new
743 756 obj = UserGroupRepoToPerm()
744 757
745 758 obj.repository = repo
746 759 obj.users_group = group_name
747 760 obj.permission = permission
748 761 self.sa.add(obj)
749 762 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
750 763 action_logger_generic(
751 764 'granted permission: {} to usergroup: {} on repo: {}'.format(
752 765 perm, group_name, repo), namespace='security.repo')
753 766
754 767 return obj
755 768
756 769 def revoke_user_group_permission(self, repo, group_name):
757 770 """
758 771 Revoke permission for user group on given repository
759 772
760 773 :param repo: Instance of Repository, repository_id, or repository name
761 774 :param group_name: Instance of UserGroup, users_group_id,
762 775 or user group name
763 776 """
764 777 repo = self._get_repo(repo)
765 778 group_name = self._get_user_group(group_name)
766 779
767 780 obj = self.sa.query(UserGroupRepoToPerm) \
768 781 .filter(UserGroupRepoToPerm.repository == repo) \
769 782 .filter(UserGroupRepoToPerm.users_group == group_name) \
770 783 .scalar()
771 784 if obj:
772 785 self.sa.delete(obj)
773 786 log.debug('Revoked perm to %s on %s', repo, group_name)
774 787 action_logger_generic(
775 788 'revoked permission from usergroup: {} on repo: {}'.format(
776 789 group_name, repo), namespace='security.repo')
777 790
778 791 def delete_stats(self, repo_name):
779 792 """
780 793 removes stats for given repo
781 794
782 795 :param repo_name:
783 796 """
784 797 repo = self._get_repo(repo_name)
785 798 try:
786 799 obj = self.sa.query(Statistics) \
787 800 .filter(Statistics.repository == repo).scalar()
788 801 if obj:
789 802 self.sa.delete(obj)
790 803 except Exception:
791 804 log.error(traceback.format_exc())
792 805 raise
793 806
794 807 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
795 808 field_type='str', field_desc=''):
796 809
797 810 repo = self._get_repo(repo_name)
798 811
799 812 new_field = RepositoryField()
800 813 new_field.repository = repo
801 814 new_field.field_key = field_key
802 815 new_field.field_type = field_type # python type
803 816 new_field.field_value = field_value
804 817 new_field.field_desc = field_desc
805 818 new_field.field_label = field_label
806 819 self.sa.add(new_field)
807 820 return new_field
808 821
809 822 def delete_repo_field(self, repo_name, field_key):
810 823 repo = self._get_repo(repo_name)
811 824 field = RepositoryField.get_by_key_name(field_key, repo)
812 825 if field:
813 826 self.sa.delete(field)
814 827
815 828 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
816 829 clone_uri=None, repo_store_location=None,
817 830 use_global_config=False):
818 831 """
819 832 makes repository on filesystem. It's group aware means it'll create
820 833 a repository within a group, and alter the paths accordingly of
821 834 group location
822 835
823 836 :param repo_name:
824 837 :param alias:
825 838 :param parent:
826 839 :param clone_uri:
827 840 :param repo_store_location:
828 841 """
829 842 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
830 843 from rhodecode.model.scm import ScmModel
831 844
832 845 if Repository.NAME_SEP in repo_name:
833 846 raise ValueError(
834 847 'repo_name must not contain groups got `%s`' % repo_name)
835 848
836 849 if isinstance(repo_group, RepoGroup):
837 850 new_parent_path = os.sep.join(repo_group.full_path_splitted)
838 851 else:
839 852 new_parent_path = repo_group or ''
840 853
841 854 if repo_store_location:
842 855 _paths = [repo_store_location]
843 856 else:
844 857 _paths = [self.repos_path, new_parent_path, repo_name]
845 858 # we need to make it str for mercurial
846 859 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
847 860
848 861 # check if this path is not a repository
849 862 if is_valid_repo(repo_path, self.repos_path):
850 863 raise Exception('This path %s is a valid repository' % repo_path)
851 864
852 865 # check if this path is a group
853 866 if is_valid_repo_group(repo_path, self.repos_path):
854 867 raise Exception('This path %s is a valid group' % repo_path)
855 868
856 869 log.info('creating repo %s in %s from url: `%s`',
857 870 repo_name, safe_unicode(repo_path),
858 871 obfuscate_url_pw(clone_uri))
859 872
860 873 backend = get_backend(repo_type)
861 874
862 875 config_repo = None if use_global_config else repo_name
863 876 if config_repo and new_parent_path:
864 877 config_repo = Repository.NAME_SEP.join(
865 878 (new_parent_path, config_repo))
866 879 config = make_db_config(clear_session=False, repo=config_repo)
867 880 config.set('extensions', 'largefiles', '')
868 881
869 882 # patch and reset hooks section of UI config to not run any
870 883 # hooks on creating remote repo
871 884 config.clear_section('hooks')
872 885
873 886 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
874 887 if repo_type == 'git':
875 888 repo = backend(
876 889 repo_path, config=config, create=True, src_url=clone_uri,
877 890 bare=True)
878 891 else:
879 892 repo = backend(
880 893 repo_path, config=config, create=True, src_url=clone_uri)
881 894
882 895 ScmModel().install_hooks(repo, repo_type=repo_type)
883 896
884 897 log.debug('Created repo %s with %s backend',
885 898 safe_unicode(repo_name), safe_unicode(repo_type))
886 899 return repo
887 900
888 901 def _rename_filesystem_repo(self, old, new):
889 902 """
890 903 renames repository on filesystem
891 904
892 905 :param old: old name
893 906 :param new: new name
894 907 """
895 908 log.info('renaming repo from %s to %s', old, new)
896 909
897 910 old_path = os.path.join(self.repos_path, old)
898 911 new_path = os.path.join(self.repos_path, new)
899 912 if os.path.isdir(new_path):
900 913 raise Exception(
901 914 'Was trying to rename to already existing dir %s' % new_path
902 915 )
903 916 shutil.move(old_path, new_path)
904 917
905 918 def _delete_filesystem_repo(self, repo):
906 919 """
907 920 removes repo from filesystem, the removal is acctually made by
908 921 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
909 922 repository is no longer valid for rhodecode, can be undeleted later on
910 923 by reverting the renames on this repository
911 924
912 925 :param repo: repo object
913 926 """
914 927 rm_path = os.path.join(self.repos_path, repo.repo_name)
915 928 repo_group = repo.group
916 929 log.info("Removing repository %s", rm_path)
917 930 # disable hg/git internal that it doesn't get detected as repo
918 931 alias = repo.repo_type
919 932
920 933 config = make_db_config(clear_session=False)
921 934 config.set('extensions', 'largefiles', '')
922 935 bare = getattr(repo.scm_instance(config=config), 'bare', False)
923 936
924 937 # skip this for bare git repos
925 938 if not bare:
926 939 # disable VCS repo
927 940 vcs_path = os.path.join(rm_path, '.%s' % alias)
928 941 if os.path.exists(vcs_path):
929 942 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
930 943
931 944 _now = datetime.now()
932 945 _ms = str(_now.microsecond).rjust(6, '0')
933 946 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
934 947 repo.just_name)
935 948 if repo_group:
936 949 # if repository is in group, prefix the removal path with the group
937 950 args = repo_group.full_path_splitted + [_d]
938 951 _d = os.path.join(*args)
939 952
940 953 if os.path.isdir(rm_path):
941 954 shutil.move(rm_path, os.path.join(self.repos_path, _d))
942 955
943 956
944 957 class ReadmeFinder:
945 958 """
946 959 Utility which knows how to find a readme for a specific commit.
947 960
948 961 The main idea is that this is a configurable algorithm. When creating an
949 962 instance you can define parameters, currently only the `default_renderer`.
950 963 Based on this configuration the method :meth:`search` behaves slightly
951 964 different.
952 965 """
953 966
954 967 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
955 968 path_re = re.compile(r'^docs?', re.IGNORECASE)
956 969
957 970 default_priorities = {
958 971 None: 0,
959 972 '.text': 2,
960 973 '.txt': 3,
961 974 '.rst': 1,
962 975 '.rest': 2,
963 976 '.md': 1,
964 977 '.mkdn': 2,
965 978 '.mdown': 3,
966 979 '.markdown': 4,
967 980 }
968 981
969 982 path_priority = {
970 983 'doc': 0,
971 984 'docs': 1,
972 985 }
973 986
974 987 FALLBACK_PRIORITY = 99
975 988
976 989 RENDERER_TO_EXTENSION = {
977 990 'rst': ['.rst', '.rest'],
978 991 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
979 992 }
980 993
981 994 def __init__(self, default_renderer=None):
982 995 self._default_renderer = default_renderer
983 996 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
984 997 default_renderer, [])
985 998
986 999 def search(self, commit, path='/'):
987 1000 """
988 1001 Find a readme in the given `commit`.
989 1002 """
990 1003 nodes = commit.get_nodes(path)
991 1004 matches = self._match_readmes(nodes)
992 1005 matches = self._sort_according_to_priority(matches)
993 1006 if matches:
994 1007 return matches[0].node
995 1008
996 1009 paths = self._match_paths(nodes)
997 1010 paths = self._sort_paths_according_to_priority(paths)
998 1011 for path in paths:
999 1012 match = self.search(commit, path=path)
1000 1013 if match:
1001 1014 return match
1002 1015
1003 1016 return None
1004 1017
1005 1018 def _match_readmes(self, nodes):
1006 1019 for node in nodes:
1007 1020 if not node.is_file():
1008 1021 continue
1009 1022 path = node.path.rsplit('/', 1)[-1]
1010 1023 match = self.readme_re.match(path)
1011 1024 if match:
1012 1025 extension = match.group(1)
1013 1026 yield ReadmeMatch(node, match, self._priority(extension))
1014 1027
1015 1028 def _match_paths(self, nodes):
1016 1029 for node in nodes:
1017 1030 if not node.is_dir():
1018 1031 continue
1019 1032 match = self.path_re.match(node.path)
1020 1033 if match:
1021 1034 yield node.path
1022 1035
1023 1036 def _priority(self, extension):
1024 1037 renderer_priority = (
1025 1038 0 if extension in self._renderer_extensions else 1)
1026 1039 extension_priority = self.default_priorities.get(
1027 1040 extension, self.FALLBACK_PRIORITY)
1028 1041 return (renderer_priority, extension_priority)
1029 1042
1030 1043 def _sort_according_to_priority(self, matches):
1031 1044
1032 1045 def priority_and_path(match):
1033 1046 return (match.priority, match.path)
1034 1047
1035 1048 return sorted(matches, key=priority_and_path)
1036 1049
1037 1050 def _sort_paths_according_to_priority(self, paths):
1038 1051
1039 1052 def priority_and_path(path):
1040 1053 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1041 1054
1042 1055 return sorted(paths, key=priority_and_path)
1043 1056
1044 1057
1045 1058 class ReadmeMatch:
1046 1059
1047 1060 def __init__(self, node, match, priority):
1048 1061 self.node = node
1049 1062 self._match = match
1050 1063 self.priority = priority
1051 1064
1052 1065 @property
1053 1066 def path(self):
1054 1067 return self.node.path
1055 1068
1056 1069 def __repr__(self):
1057 1070 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
General Comments 0
You need to be logged in to leave comments. Login now