##// END OF EJS Templates
landing-rev: fixes #4102, use branches instead of landing tip refs by default....
marcink -
r3881:95b6f937 default
parent child Browse files
Show More
@@ -1,117 +1,122 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 random
23 import pytest
23 24
24 25 from rhodecode.api.utils import get_origin
25 26 from rhodecode.lib.ext_json import json
26 27
27 28
29 def jsonify(obj):
30 return json.loads(json.dumps(obj))
31
32
28 33 API_URL = '/_admin/api'
29 34
30 35
31 36 def assert_call_ok(id_, given):
32 37 expected = jsonify({
33 38 'id': id_,
34 39 'error': None,
35 40 'result': None
36 41 })
37 42 given = json.loads(given)
38 43
39 44 assert expected['id'] == given['id']
40 45 assert expected['error'] == given['error']
41 46 return given['result']
42 47
43 48
44 49 def assert_ok(id_, expected, given):
50 given = json.loads(given)
51 if given.get('error'):
52 pytest.fail("Unexpected ERROR in success response: {}".format(given['error']))
53
45 54 expected = jsonify({
46 55 'id': id_,
47 56 'error': None,
48 57 'result': expected
49 58 })
50 given = json.loads(given)
59
51 60 assert expected == given
52 61
53 62
54 63 def assert_error(id_, expected, given):
55 64 expected = jsonify({
56 65 'id': id_,
57 66 'error': expected,
58 67 'result': None
59 68 })
60 69 given = json.loads(given)
61 70 assert expected == given
62 71
63 72
64 def jsonify(obj):
65 return json.loads(json.dumps(obj))
66
67
68 73 def build_data(apikey, method, **kw):
69 74 """
70 75 Builds API data with given random ID
71 76 """
72 77 random_id = random.randrange(1, 9999)
73 78 return random_id, json.dumps({
74 79 "id": random_id,
75 80 "api_key": apikey,
76 81 "method": method,
77 82 "args": kw
78 83 })
79 84
80 85
81 86 def api_call(app, params, status=None):
82 87 response = app.post(
83 88 API_URL, content_type='application/json', params=params, status=status)
84 89 return response
85 90
86 91
87 92 def crash(*args, **kwargs):
88 93 raise Exception('Total Crash !')
89 94
90 95
91 96 def expected_permissions(object_with_permissions):
92 97 """
93 98 Returns the expected permissions structure for the given object.
94 99
95 100 The object is expected to be a `Repository`, `RepositoryGroup`,
96 101 or `UserGroup`. They all implement the same permission handling
97 102 API.
98 103 """
99 104 permissions = []
100 105 for _user in object_with_permissions.permissions():
101 106 user_data = {
102 107 'name': _user.username,
103 108 'permission': _user.permission,
104 109 'origin': get_origin(_user),
105 110 'type': "user",
106 111 }
107 112 permissions.append(user_data)
108 113
109 114 for _user_group in object_with_permissions.permission_user_groups():
110 115 user_group_data = {
111 116 'name': _user_group.users_group_name,
112 117 'permission': _user_group.permission,
113 118 'origin': get_origin(_user_group),
114 119 'type': "user_group",
115 120 }
116 121 permissions.append(user_group_data)
117 122 return permissions
@@ -1,2313 +1,2327 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.lib.vcs import RepositoryError
40 40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 45 ChangesetComment)
46 46 from rhodecode.model.repo import RepoModel
47 47 from rhodecode.model.scm import ScmModel, RepoList
48 48 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
49 49 from rhodecode.model import validation_schema
50 50 from rhodecode.model.validation_schema.schemas import repo_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 @jsonrpc_method()
56 56 def get_repo(request, apiuser, repoid, cache=Optional(True)):
57 57 """
58 58 Gets an existing repository by its name or repository_id.
59 59
60 60 The members section so the output returns users groups or users
61 61 associated with that repository.
62 62
63 63 This command can only be run using an |authtoken| with admin rights,
64 64 or users with at least read rights to the |repo|.
65 65
66 66 :param apiuser: This is filled automatically from the |authtoken|.
67 67 :type apiuser: AuthUser
68 68 :param repoid: The repository name or repository id.
69 69 :type repoid: str or int
70 70 :param cache: use the cached value for last changeset
71 71 :type: cache: Optional(bool)
72 72
73 73 Example output:
74 74
75 75 .. code-block:: bash
76 76
77 77 {
78 78 "error": null,
79 79 "id": <repo_id>,
80 80 "result": {
81 81 "clone_uri": null,
82 82 "created_on": "timestamp",
83 83 "description": "repo description",
84 84 "enable_downloads": false,
85 85 "enable_locking": false,
86 86 "enable_statistics": false,
87 87 "followers": [
88 88 {
89 89 "active": true,
90 90 "admin": false,
91 91 "api_key": "****************************************",
92 92 "api_keys": [
93 93 "****************************************"
94 94 ],
95 95 "email": "user@example.com",
96 96 "emails": [
97 97 "user@example.com"
98 98 ],
99 99 "extern_name": "rhodecode",
100 100 "extern_type": "rhodecode",
101 101 "firstname": "username",
102 102 "ip_addresses": [],
103 103 "language": null,
104 104 "last_login": "2015-09-16T17:16:35.854",
105 105 "lastname": "surname",
106 106 "user_id": <user_id>,
107 107 "username": "name"
108 108 }
109 109 ],
110 110 "fork_of": "parent-repo",
111 111 "landing_rev": [
112 112 "rev",
113 113 "tip"
114 114 ],
115 115 "last_changeset": {
116 116 "author": "User <user@example.com>",
117 117 "branch": "default",
118 118 "date": "timestamp",
119 119 "message": "last commit message",
120 120 "parents": [
121 121 {
122 122 "raw_id": "commit-id"
123 123 }
124 124 ],
125 125 "raw_id": "commit-id",
126 126 "revision": <revision number>,
127 127 "short_id": "short id"
128 128 },
129 129 "lock_reason": null,
130 130 "locked_by": null,
131 131 "locked_date": null,
132 132 "owner": "owner-name",
133 133 "permissions": [
134 134 {
135 135 "name": "super-admin-name",
136 136 "origin": "super-admin",
137 137 "permission": "repository.admin",
138 138 "type": "user"
139 139 },
140 140 {
141 141 "name": "owner-name",
142 142 "origin": "owner",
143 143 "permission": "repository.admin",
144 144 "type": "user"
145 145 },
146 146 {
147 147 "name": "user-group-name",
148 148 "origin": "permission",
149 149 "permission": "repository.write",
150 150 "type": "user_group"
151 151 }
152 152 ],
153 153 "private": true,
154 154 "repo_id": 676,
155 155 "repo_name": "user-group/repo-name",
156 156 "repo_type": "hg"
157 157 }
158 158 }
159 159 """
160 160
161 161 repo = get_repo_or_error(repoid)
162 162 cache = Optional.extract(cache)
163 163
164 164 include_secrets = False
165 165 if has_superadmin_permission(apiuser):
166 166 include_secrets = True
167 167 else:
168 168 # check if we have at least read permission for this repo !
169 169 _perms = (
170 170 'repository.admin', 'repository.write', 'repository.read',)
171 171 validate_repo_permissions(apiuser, repoid, repo, _perms)
172 172
173 173 permissions = []
174 174 for _user in repo.permissions():
175 175 user_data = {
176 176 'name': _user.username,
177 177 'permission': _user.permission,
178 178 'origin': get_origin(_user),
179 179 'type': "user",
180 180 }
181 181 permissions.append(user_data)
182 182
183 183 for _user_group in repo.permission_user_groups():
184 184 user_group_data = {
185 185 'name': _user_group.users_group_name,
186 186 'permission': _user_group.permission,
187 187 'origin': get_origin(_user_group),
188 188 'type': "user_group",
189 189 }
190 190 permissions.append(user_group_data)
191 191
192 192 following_users = [
193 193 user.user.get_api_data(include_secrets=include_secrets)
194 194 for user in repo.followers]
195 195
196 196 if not cache:
197 197 repo.update_commit_cache()
198 198 data = repo.get_api_data(include_secrets=include_secrets)
199 199 data['permissions'] = permissions
200 200 data['followers'] = following_users
201 201 return data
202 202
203 203
204 204 @jsonrpc_method()
205 205 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
206 206 """
207 207 Lists all existing repositories.
208 208
209 209 This command can only be run using an |authtoken| with admin rights,
210 210 or users with at least read rights to |repos|.
211 211
212 212 :param apiuser: This is filled automatically from the |authtoken|.
213 213 :type apiuser: AuthUser
214 214 :param root: specify root repository group to fetch repositories.
215 215 filters the returned repositories to be members of given root group.
216 216 :type root: Optional(None)
217 217 :param traverse: traverse given root into subrepositories. With this flag
218 218 set to False, it will only return top-level repositories from `root`.
219 219 if root is empty it will return just top-level repositories.
220 220 :type traverse: Optional(True)
221 221
222 222
223 223 Example output:
224 224
225 225 .. code-block:: bash
226 226
227 227 id : <id_given_in_input>
228 228 result: [
229 229 {
230 230 "repo_id" : "<repo_id>",
231 231 "repo_name" : "<reponame>"
232 232 "repo_type" : "<repo_type>",
233 233 "clone_uri" : "<clone_uri>",
234 234 "private": : "<bool>",
235 235 "created_on" : "<datetimecreated>",
236 236 "description" : "<description>",
237 237 "landing_rev": "<landing_rev>",
238 238 "owner": "<repo_owner>",
239 239 "fork_of": "<name_of_fork_parent>",
240 240 "enable_downloads": "<bool>",
241 241 "enable_locking": "<bool>",
242 242 "enable_statistics": "<bool>",
243 243 },
244 244 ...
245 245 ]
246 246 error: null
247 247 """
248 248
249 249 include_secrets = has_superadmin_permission(apiuser)
250 250 _perms = ('repository.read', 'repository.write', 'repository.admin',)
251 251 extras = {'user': apiuser}
252 252
253 253 root = Optional.extract(root)
254 254 traverse = Optional.extract(traverse, binary=True)
255 255
256 256 if root:
257 257 # verify parent existance, if it's empty return an error
258 258 parent = RepoGroup.get_by_group_name(root)
259 259 if not parent:
260 260 raise JSONRPCError(
261 261 'Root repository group `{}` does not exist'.format(root))
262 262
263 263 if traverse:
264 264 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
265 265 else:
266 266 repos = RepoModel().get_repos_for_root(root=parent)
267 267 else:
268 268 if traverse:
269 269 repos = RepoModel().get_all()
270 270 else:
271 271 # return just top-level
272 272 repos = RepoModel().get_repos_for_root(root=None)
273 273
274 274 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
275 275 return [repo.get_api_data(include_secrets=include_secrets)
276 276 for repo in repo_list]
277 277
278 278
279 279 @jsonrpc_method()
280 280 def get_repo_changeset(request, apiuser, repoid, revision,
281 281 details=Optional('basic')):
282 282 """
283 283 Returns information about a changeset.
284 284
285 285 Additionally parameters define the amount of details returned by
286 286 this function.
287 287
288 288 This command can only be run using an |authtoken| with admin rights,
289 289 or users with at least read rights to the |repo|.
290 290
291 291 :param apiuser: This is filled automatically from the |authtoken|.
292 292 :type apiuser: AuthUser
293 293 :param repoid: The repository name or repository id
294 294 :type repoid: str or int
295 295 :param revision: revision for which listing should be done
296 296 :type revision: str
297 297 :param details: details can be 'basic|extended|full' full gives diff
298 298 info details like the diff itself, and number of changed files etc.
299 299 :type details: Optional(str)
300 300
301 301 """
302 302 repo = get_repo_or_error(repoid)
303 303 if not has_superadmin_permission(apiuser):
304 304 _perms = (
305 305 'repository.admin', 'repository.write', 'repository.read',)
306 306 validate_repo_permissions(apiuser, repoid, repo, _perms)
307 307
308 308 changes_details = Optional.extract(details)
309 309 _changes_details_types = ['basic', 'extended', 'full']
310 310 if changes_details not in _changes_details_types:
311 311 raise JSONRPCError(
312 312 'ret_type must be one of %s' % (
313 313 ','.join(_changes_details_types)))
314 314
315 315 pre_load = ['author', 'branch', 'date', 'message', 'parents',
316 316 'status', '_commit', '_file_paths']
317 317
318 318 try:
319 319 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
320 320 except TypeError as e:
321 321 raise JSONRPCError(safe_str(e))
322 322 _cs_json = cs.__json__()
323 323 _cs_json['diff'] = build_commit_data(cs, changes_details)
324 324 if changes_details == 'full':
325 325 _cs_json['refs'] = cs._get_refs()
326 326 return _cs_json
327 327
328 328
329 329 @jsonrpc_method()
330 330 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
331 331 details=Optional('basic')):
332 332 """
333 333 Returns a set of commits limited by the number starting
334 334 from the `start_rev` option.
335 335
336 336 Additional parameters define the amount of details returned by this
337 337 function.
338 338
339 339 This command can only be run using an |authtoken| with admin rights,
340 340 or users with at least read rights to |repos|.
341 341
342 342 :param apiuser: This is filled automatically from the |authtoken|.
343 343 :type apiuser: AuthUser
344 344 :param repoid: The repository name or repository ID.
345 345 :type repoid: str or int
346 346 :param start_rev: The starting revision from where to get changesets.
347 347 :type start_rev: str
348 348 :param limit: Limit the number of commits to this amount
349 349 :type limit: str or int
350 350 :param details: Set the level of detail returned. Valid option are:
351 351 ``basic``, ``extended`` and ``full``.
352 352 :type details: Optional(str)
353 353
354 354 .. note::
355 355
356 356 Setting the parameter `details` to the value ``full`` is extensive
357 357 and returns details like the diff itself, and the number
358 358 of changed files.
359 359
360 360 """
361 361 repo = get_repo_or_error(repoid)
362 362 if not has_superadmin_permission(apiuser):
363 363 _perms = (
364 364 'repository.admin', 'repository.write', 'repository.read',)
365 365 validate_repo_permissions(apiuser, repoid, repo, _perms)
366 366
367 367 changes_details = Optional.extract(details)
368 368 _changes_details_types = ['basic', 'extended', 'full']
369 369 if changes_details not in _changes_details_types:
370 370 raise JSONRPCError(
371 371 'ret_type must be one of %s' % (
372 372 ','.join(_changes_details_types)))
373 373
374 374 limit = int(limit)
375 375 pre_load = ['author', 'branch', 'date', 'message', 'parents',
376 376 'status', '_commit', '_file_paths']
377 377
378 378 vcs_repo = repo.scm_instance()
379 379 # SVN needs a special case to distinguish its index and commit id
380 380 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
381 381 start_rev = vcs_repo.commit_ids[0]
382 382
383 383 try:
384 384 commits = vcs_repo.get_commits(
385 385 start_id=start_rev, pre_load=pre_load, translate_tags=False)
386 386 except TypeError as e:
387 387 raise JSONRPCError(safe_str(e))
388 388 except Exception:
389 389 log.exception('Fetching of commits failed')
390 390 raise JSONRPCError('Error occurred during commit fetching')
391 391
392 392 ret = []
393 393 for cnt, commit in enumerate(commits):
394 394 if cnt >= limit != -1:
395 395 break
396 396 _cs_json = commit.__json__()
397 397 _cs_json['diff'] = build_commit_data(commit, changes_details)
398 398 if changes_details == 'full':
399 399 _cs_json['refs'] = {
400 400 'branches': [commit.branch],
401 401 'bookmarks': getattr(commit, 'bookmarks', []),
402 402 'tags': commit.tags
403 403 }
404 404 ret.append(_cs_json)
405 405 return ret
406 406
407 407
408 408 @jsonrpc_method()
409 409 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
410 410 ret_type=Optional('all'), details=Optional('basic'),
411 411 max_file_bytes=Optional(None)):
412 412 """
413 413 Returns a list of nodes and children in a flat list for a given
414 414 path at given revision.
415 415
416 416 It's possible to specify ret_type to show only `files` or `dirs`.
417 417
418 418 This command can only be run using an |authtoken| with admin rights,
419 419 or users with at least read rights to |repos|.
420 420
421 421 :param apiuser: This is filled automatically from the |authtoken|.
422 422 :type apiuser: AuthUser
423 423 :param repoid: The repository name or repository ID.
424 424 :type repoid: str or int
425 425 :param revision: The revision for which listing should be done.
426 426 :type revision: str
427 427 :param root_path: The path from which to start displaying.
428 428 :type root_path: str
429 429 :param ret_type: Set the return type. Valid options are
430 430 ``all`` (default), ``files`` and ``dirs``.
431 431 :type ret_type: Optional(str)
432 432 :param details: Returns extended information about nodes, such as
433 433 md5, binary, and or content.
434 434 The valid options are ``basic`` and ``full``.
435 435 :type details: Optional(str)
436 436 :param max_file_bytes: Only return file content under this file size bytes
437 437 :type details: Optional(int)
438 438
439 439 Example output:
440 440
441 441 .. code-block:: bash
442 442
443 443 id : <id_given_in_input>
444 444 result: [
445 445 {
446 446 "binary": false,
447 447 "content": "File line",
448 448 "extension": "md",
449 449 "lines": 2,
450 450 "md5": "059fa5d29b19c0657e384749480f6422",
451 451 "mimetype": "text/x-minidsrc",
452 452 "name": "file.md",
453 453 "size": 580,
454 454 "type": "file"
455 455 },
456 456 ...
457 457 ]
458 458 error: null
459 459 """
460 460
461 461 repo = get_repo_or_error(repoid)
462 462 if not has_superadmin_permission(apiuser):
463 463 _perms = ('repository.admin', 'repository.write', 'repository.read',)
464 464 validate_repo_permissions(apiuser, repoid, repo, _perms)
465 465
466 466 ret_type = Optional.extract(ret_type)
467 467 details = Optional.extract(details)
468 468 _extended_types = ['basic', 'full']
469 469 if details not in _extended_types:
470 470 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
471 471 extended_info = False
472 472 content = False
473 473 if details == 'basic':
474 474 extended_info = True
475 475
476 476 if details == 'full':
477 477 extended_info = content = True
478 478
479 479 _map = {}
480 480 try:
481 481 # check if repo is not empty by any chance, skip quicker if it is.
482 482 _scm = repo.scm_instance()
483 483 if _scm.is_empty():
484 484 return []
485 485
486 486 _d, _f = ScmModel().get_nodes(
487 487 repo, revision, root_path, flat=False,
488 488 extended_info=extended_info, content=content,
489 489 max_file_bytes=max_file_bytes)
490 490 _map = {
491 491 'all': _d + _f,
492 492 'files': _f,
493 493 'dirs': _d,
494 494 }
495 495 return _map[ret_type]
496 496 except KeyError:
497 497 raise JSONRPCError(
498 498 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
499 499 except Exception:
500 500 log.exception("Exception occurred while trying to get repo nodes")
501 501 raise JSONRPCError(
502 502 'failed to get repo: `%s` nodes' % repo.repo_name
503 503 )
504 504
505 505
506 506 @jsonrpc_method()
507 507 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
508 508 max_file_bytes=Optional(None), details=Optional('basic'),
509 509 cache=Optional(True)):
510 510 """
511 511 Returns a single file from repository at given revision.
512 512
513 513 This command can only be run using an |authtoken| with admin rights,
514 514 or users with at least read rights to |repos|.
515 515
516 516 :param apiuser: This is filled automatically from the |authtoken|.
517 517 :type apiuser: AuthUser
518 518 :param repoid: The repository name or repository ID.
519 519 :type repoid: str or int
520 520 :param commit_id: The revision for which listing should be done.
521 521 :type commit_id: str
522 522 :param file_path: The path from which to start displaying.
523 523 :type file_path: str
524 524 :param details: Returns different set of information about nodes.
525 525 The valid options are ``minimal`` ``basic`` and ``full``.
526 526 :type details: Optional(str)
527 527 :param max_file_bytes: Only return file content under this file size bytes
528 528 :type max_file_bytes: Optional(int)
529 529 :param cache: Use internal caches for fetching files. If disabled fetching
530 530 files is slower but more memory efficient
531 531 :type cache: Optional(bool)
532 532
533 533 Example output:
534 534
535 535 .. code-block:: bash
536 536
537 537 id : <id_given_in_input>
538 538 result: {
539 539 "binary": false,
540 540 "extension": "py",
541 541 "lines": 35,
542 542 "content": "....",
543 543 "md5": "76318336366b0f17ee249e11b0c99c41",
544 544 "mimetype": "text/x-python",
545 545 "name": "python.py",
546 546 "size": 817,
547 547 "type": "file",
548 548 }
549 549 error: null
550 550 """
551 551
552 552 repo = get_repo_or_error(repoid)
553 553 if not has_superadmin_permission(apiuser):
554 554 _perms = ('repository.admin', 'repository.write', 'repository.read',)
555 555 validate_repo_permissions(apiuser, repoid, repo, _perms)
556 556
557 557 cache = Optional.extract(cache, binary=True)
558 558 details = Optional.extract(details)
559 559 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
560 560 if details not in _extended_types:
561 561 raise JSONRPCError(
562 562 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
563 563 extended_info = False
564 564 content = False
565 565
566 566 if details == 'minimal':
567 567 extended_info = False
568 568
569 569 elif details == 'basic':
570 570 extended_info = True
571 571
572 572 elif details == 'full':
573 573 extended_info = content = True
574 574
575 575 try:
576 576 # check if repo is not empty by any chance, skip quicker if it is.
577 577 _scm = repo.scm_instance()
578 578 if _scm.is_empty():
579 579 return None
580 580
581 581 node = ScmModel().get_node(
582 582 repo, commit_id, file_path, extended_info=extended_info,
583 583 content=content, max_file_bytes=max_file_bytes, cache=cache)
584 584 except NodeDoesNotExistError:
585 585 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
586 586 repo.repo_name, file_path, commit_id))
587 587 except Exception:
588 588 log.exception("Exception occurred while trying to get repo %s file",
589 589 repo.repo_name)
590 590 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
591 591 repo.repo_name, file_path))
592 592
593 593 return node
594 594
595 595
596 596 @jsonrpc_method()
597 597 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
598 598 """
599 599 Returns a list of tree nodes for path at given revision. This api is built
600 600 strictly for usage in full text search building, and shouldn't be consumed
601 601
602 602 This command can only be run using an |authtoken| with admin rights,
603 603 or users with at least read rights to |repos|.
604 604
605 605 """
606 606
607 607 repo = get_repo_or_error(repoid)
608 608 if not has_superadmin_permission(apiuser):
609 609 _perms = ('repository.admin', 'repository.write', 'repository.read',)
610 610 validate_repo_permissions(apiuser, repoid, repo, _perms)
611 611
612 612 repo_id = repo.repo_id
613 613 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
614 614 cache_on = cache_seconds > 0
615 615
616 616 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
617 617 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
618 618
619 619 def compute_fts_tree(repo_id, commit_id, root_path, cache_ver):
620 620 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
621 621
622 622 try:
623 623 # check if repo is not empty by any chance, skip quicker if it is.
624 624 _scm = repo.scm_instance()
625 625 if _scm.is_empty():
626 626 return []
627 627 except RepositoryError:
628 628 log.exception("Exception occurred while trying to get repo nodes")
629 629 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
630 630
631 631 try:
632 632 # we need to resolve commit_id to a FULL sha for cache to work correctly.
633 633 # sending 'master' is a pointer that needs to be translated to current commit.
634 634 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
635 635 log.debug(
636 636 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
637 637 'with caching: %s[TTL: %ss]' % (
638 638 repo_id, commit_id, cache_on, cache_seconds or 0))
639 639
640 640 tree_files = compute_fts_tree(repo_id, commit_id, root_path, 'v1')
641 641 return tree_files
642 642
643 643 except Exception:
644 644 log.exception("Exception occurred while trying to get repo nodes")
645 645 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
646 646
647 647
648 648 @jsonrpc_method()
649 649 def get_repo_refs(request, apiuser, repoid):
650 650 """
651 651 Returns a dictionary of current references. It returns
652 652 bookmarks, branches, closed_branches, and tags for given repository
653 653
654 654 It's possible to specify ret_type to show only `files` or `dirs`.
655 655
656 656 This command can only be run using an |authtoken| with admin rights,
657 657 or users with at least read rights to |repos|.
658 658
659 659 :param apiuser: This is filled automatically from the |authtoken|.
660 660 :type apiuser: AuthUser
661 661 :param repoid: The repository name or repository ID.
662 662 :type repoid: str or int
663 663
664 664 Example output:
665 665
666 666 .. code-block:: bash
667 667
668 668 id : <id_given_in_input>
669 669 "result": {
670 670 "bookmarks": {
671 671 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
672 672 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
673 673 },
674 674 "branches": {
675 675 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
676 676 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
677 677 },
678 678 "branches_closed": {},
679 679 "tags": {
680 680 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
681 681 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
682 682 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
683 683 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
684 684 }
685 685 }
686 686 error: null
687 687 """
688 688
689 689 repo = get_repo_or_error(repoid)
690 690 if not has_superadmin_permission(apiuser):
691 691 _perms = ('repository.admin', 'repository.write', 'repository.read',)
692 692 validate_repo_permissions(apiuser, repoid, repo, _perms)
693 693
694 694 try:
695 695 # check if repo is not empty by any chance, skip quicker if it is.
696 696 vcs_instance = repo.scm_instance()
697 697 refs = vcs_instance.refs()
698 698 return refs
699 699 except Exception:
700 700 log.exception("Exception occurred while trying to get repo refs")
701 701 raise JSONRPCError(
702 702 'failed to get repo: `%s` references' % repo.repo_name
703 703 )
704 704
705 705
706 706 @jsonrpc_method()
707 707 def create_repo(
708 708 request, apiuser, repo_name, repo_type,
709 709 owner=Optional(OAttr('apiuser')),
710 710 description=Optional(''),
711 711 private=Optional(False),
712 712 clone_uri=Optional(None),
713 713 push_uri=Optional(None),
714 landing_rev=Optional('rev:tip'),
714 landing_rev=Optional(None),
715 715 enable_statistics=Optional(False),
716 716 enable_locking=Optional(False),
717 717 enable_downloads=Optional(False),
718 718 copy_permissions=Optional(False)):
719 719 """
720 720 Creates a repository.
721 721
722 722 * If the repository name contains "/", repository will be created inside
723 723 a repository group or nested repository groups
724 724
725 725 For example "foo/bar/repo1" will create |repo| called "repo1" inside
726 726 group "foo/bar". You have to have permissions to access and write to
727 727 the last repository group ("bar" in this example)
728 728
729 729 This command can only be run using an |authtoken| with at least
730 730 permissions to create repositories, or write permissions to
731 731 parent repository groups.
732 732
733 733 :param apiuser: This is filled automatically from the |authtoken|.
734 734 :type apiuser: AuthUser
735 735 :param repo_name: Set the repository name.
736 736 :type repo_name: str
737 737 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
738 738 :type repo_type: str
739 739 :param owner: user_id or username
740 740 :type owner: Optional(str)
741 741 :param description: Set the repository description.
742 742 :type description: Optional(str)
743 743 :param private: set repository as private
744 744 :type private: bool
745 745 :param clone_uri: set clone_uri
746 746 :type clone_uri: str
747 747 :param push_uri: set push_uri
748 748 :type push_uri: str
749 :param landing_rev: <rev_type>:<rev>
749 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
750 750 :type landing_rev: str
751 751 :param enable_locking:
752 752 :type enable_locking: bool
753 753 :param enable_downloads:
754 754 :type enable_downloads: bool
755 755 :param enable_statistics:
756 756 :type enable_statistics: bool
757 757 :param copy_permissions: Copy permission from group in which the
758 758 repository is being created.
759 759 :type copy_permissions: bool
760 760
761 761
762 762 Example output:
763 763
764 764 .. code-block:: bash
765 765
766 766 id : <id_given_in_input>
767 767 result: {
768 768 "msg": "Created new repository `<reponame>`",
769 769 "success": true,
770 770 "task": "<celery task id or None if done sync>"
771 771 }
772 772 error: null
773 773
774 774
775 775 Example error output:
776 776
777 777 .. code-block:: bash
778 778
779 779 id : <id_given_in_input>
780 780 result : null
781 781 error : {
782 782 'failed to create repository `<repo_name>`'
783 783 }
784 784
785 785 """
786 786
787 787 owner = validate_set_owner_permissions(apiuser, owner)
788 788
789 789 description = Optional.extract(description)
790 790 copy_permissions = Optional.extract(copy_permissions)
791 791 clone_uri = Optional.extract(clone_uri)
792 792 push_uri = Optional.extract(push_uri)
793 landing_commit_ref = Optional.extract(landing_rev)
794 793
795 794 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
796 795 if isinstance(private, Optional):
797 796 private = defs.get('repo_private') or Optional.extract(private)
798 797 if isinstance(repo_type, Optional):
799 798 repo_type = defs.get('repo_type')
800 799 if isinstance(enable_statistics, Optional):
801 800 enable_statistics = defs.get('repo_enable_statistics')
802 801 if isinstance(enable_locking, Optional):
803 802 enable_locking = defs.get('repo_enable_locking')
804 803 if isinstance(enable_downloads, Optional):
805 804 enable_downloads = defs.get('repo_enable_downloads')
806 805
806 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
807 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
808 ref_choices = list(set(ref_choices + [landing_ref]))
809
810 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
811
807 812 schema = repo_schema.RepoSchema().bind(
808 813 repo_type_options=rhodecode.BACKENDS.keys(),
814 repo_ref_options=ref_choices,
809 815 repo_type=repo_type,
810 816 # user caller
811 817 user=apiuser)
812 818
813 819 try:
814 820 schema_data = schema.deserialize(dict(
815 821 repo_name=repo_name,
816 822 repo_type=repo_type,
817 823 repo_owner=owner.username,
818 824 repo_description=description,
819 825 repo_landing_commit_ref=landing_commit_ref,
820 826 repo_clone_uri=clone_uri,
821 827 repo_push_uri=push_uri,
822 828 repo_private=private,
823 829 repo_copy_permissions=copy_permissions,
824 830 repo_enable_statistics=enable_statistics,
825 831 repo_enable_downloads=enable_downloads,
826 832 repo_enable_locking=enable_locking))
827 833 except validation_schema.Invalid as err:
828 834 raise JSONRPCValidationError(colander_exc=err)
829 835
830 836 try:
831 837 data = {
832 838 'owner': owner,
833 839 'repo_name': schema_data['repo_group']['repo_name_without_group'],
834 840 'repo_name_full': schema_data['repo_name'],
835 841 'repo_group': schema_data['repo_group']['repo_group_id'],
836 842 'repo_type': schema_data['repo_type'],
837 843 'repo_description': schema_data['repo_description'],
838 844 'repo_private': schema_data['repo_private'],
839 845 'clone_uri': schema_data['repo_clone_uri'],
840 846 'push_uri': schema_data['repo_push_uri'],
841 847 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
842 848 'enable_statistics': schema_data['repo_enable_statistics'],
843 849 'enable_locking': schema_data['repo_enable_locking'],
844 850 'enable_downloads': schema_data['repo_enable_downloads'],
845 851 'repo_copy_permissions': schema_data['repo_copy_permissions'],
846 852 }
847 853
848 854 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
849 855 task_id = get_task_id(task)
850 856 # no commit, it's done in RepoModel, or async via celery
851 857 return {
852 858 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
853 859 'success': True, # cannot return the repo data here since fork
854 860 # can be done async
855 861 'task': task_id
856 862 }
857 863 except Exception:
858 864 log.exception(
859 865 u"Exception while trying to create the repository %s",
860 866 schema_data['repo_name'])
861 867 raise JSONRPCError(
862 868 'failed to create repository `%s`' % (schema_data['repo_name'],))
863 869
864 870
865 871 @jsonrpc_method()
866 872 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
867 873 description=Optional('')):
868 874 """
869 875 Adds an extra field to a repository.
870 876
871 877 This command can only be run using an |authtoken| with at least
872 878 write permissions to the |repo|.
873 879
874 880 :param apiuser: This is filled automatically from the |authtoken|.
875 881 :type apiuser: AuthUser
876 882 :param repoid: Set the repository name or repository id.
877 883 :type repoid: str or int
878 884 :param key: Create a unique field key for this repository.
879 885 :type key: str
880 886 :param label:
881 887 :type label: Optional(str)
882 888 :param description:
883 889 :type description: Optional(str)
884 890 """
885 891 repo = get_repo_or_error(repoid)
886 892 if not has_superadmin_permission(apiuser):
887 893 _perms = ('repository.admin',)
888 894 validate_repo_permissions(apiuser, repoid, repo, _perms)
889 895
890 896 label = Optional.extract(label) or key
891 897 description = Optional.extract(description)
892 898
893 899 field = RepositoryField.get_by_key_name(key, repo)
894 900 if field:
895 901 raise JSONRPCError('Field with key '
896 902 '`%s` exists for repo `%s`' % (key, repoid))
897 903
898 904 try:
899 905 RepoModel().add_repo_field(repo, key, field_label=label,
900 906 field_desc=description)
901 907 Session().commit()
902 908 return {
903 909 'msg': "Added new repository field `%s`" % (key,),
904 910 'success': True,
905 911 }
906 912 except Exception:
907 913 log.exception("Exception occurred while trying to add field to repo")
908 914 raise JSONRPCError(
909 915 'failed to create new field for repository `%s`' % (repoid,))
910 916
911 917
912 918 @jsonrpc_method()
913 919 def remove_field_from_repo(request, apiuser, repoid, key):
914 920 """
915 921 Removes an extra field from a repository.
916 922
917 923 This command can only be run using an |authtoken| with at least
918 924 write permissions to the |repo|.
919 925
920 926 :param apiuser: This is filled automatically from the |authtoken|.
921 927 :type apiuser: AuthUser
922 928 :param repoid: Set the repository name or repository ID.
923 929 :type repoid: str or int
924 930 :param key: Set the unique field key for this repository.
925 931 :type key: str
926 932 """
927 933
928 934 repo = get_repo_or_error(repoid)
929 935 if not has_superadmin_permission(apiuser):
930 936 _perms = ('repository.admin',)
931 937 validate_repo_permissions(apiuser, repoid, repo, _perms)
932 938
933 939 field = RepositoryField.get_by_key_name(key, repo)
934 940 if not field:
935 941 raise JSONRPCError('Field with key `%s` does not '
936 942 'exists for repo `%s`' % (key, repoid))
937 943
938 944 try:
939 945 RepoModel().delete_repo_field(repo, field_key=key)
940 946 Session().commit()
941 947 return {
942 948 'msg': "Deleted repository field `%s`" % (key,),
943 949 'success': True,
944 950 }
945 951 except Exception:
946 952 log.exception(
947 953 "Exception occurred while trying to delete field from repo")
948 954 raise JSONRPCError(
949 955 'failed to delete field for repository `%s`' % (repoid,))
950 956
951 957
952 958 @jsonrpc_method()
953 959 def update_repo(
954 960 request, apiuser, repoid, repo_name=Optional(None),
955 961 owner=Optional(OAttr('apiuser')), description=Optional(''),
956 962 private=Optional(False),
957 963 clone_uri=Optional(None), push_uri=Optional(None),
958 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
964 landing_rev=Optional(None), fork_of=Optional(None),
959 965 enable_statistics=Optional(False),
960 966 enable_locking=Optional(False),
961 967 enable_downloads=Optional(False), fields=Optional('')):
962 968 """
963 969 Updates a repository with the given information.
964 970
965 971 This command can only be run using an |authtoken| with at least
966 972 admin permissions to the |repo|.
967 973
968 974 * If the repository name contains "/", repository will be updated
969 975 accordingly with a repository group or nested repository groups
970 976
971 977 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
972 978 called "repo-test" and place it inside group "foo/bar".
973 979 You have to have permissions to access and write to the last repository
974 980 group ("bar" in this example)
975 981
976 982 :param apiuser: This is filled automatically from the |authtoken|.
977 983 :type apiuser: AuthUser
978 984 :param repoid: repository name or repository ID.
979 985 :type repoid: str or int
980 986 :param repo_name: Update the |repo| name, including the
981 987 repository group it's in.
982 988 :type repo_name: str
983 989 :param owner: Set the |repo| owner.
984 990 :type owner: str
985 991 :param fork_of: Set the |repo| as fork of another |repo|.
986 992 :type fork_of: str
987 993 :param description: Update the |repo| description.
988 994 :type description: str
989 995 :param private: Set the |repo| as private. (True | False)
990 996 :type private: bool
991 997 :param clone_uri: Update the |repo| clone URI.
992 998 :type clone_uri: str
993 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
999 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
994 1000 :type landing_rev: str
995 1001 :param enable_statistics: Enable statistics on the |repo|, (True | False).
996 1002 :type enable_statistics: bool
997 1003 :param enable_locking: Enable |repo| locking.
998 1004 :type enable_locking: bool
999 1005 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1000 1006 :type enable_downloads: bool
1001 1007 :param fields: Add extra fields to the |repo|. Use the following
1002 1008 example format: ``field_key=field_val,field_key2=fieldval2``.
1003 1009 Escape ', ' with \,
1004 1010 :type fields: str
1005 1011 """
1006 1012
1007 1013 repo = get_repo_or_error(repoid)
1008 1014
1009 1015 include_secrets = False
1010 1016 if not has_superadmin_permission(apiuser):
1011 1017 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1012 1018 else:
1013 1019 include_secrets = True
1014 1020
1015 1021 updates = dict(
1016 1022 repo_name=repo_name
1017 1023 if not isinstance(repo_name, Optional) else repo.repo_name,
1018 1024
1019 1025 fork_id=fork_of
1020 1026 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1021 1027
1022 1028 user=owner
1023 1029 if not isinstance(owner, Optional) else repo.user.username,
1024 1030
1025 1031 repo_description=description
1026 1032 if not isinstance(description, Optional) else repo.description,
1027 1033
1028 1034 repo_private=private
1029 1035 if not isinstance(private, Optional) else repo.private,
1030 1036
1031 1037 clone_uri=clone_uri
1032 1038 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1033 1039
1034 1040 push_uri=push_uri
1035 1041 if not isinstance(push_uri, Optional) else repo.push_uri,
1036 1042
1037 1043 repo_landing_rev=landing_rev
1038 1044 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1039 1045
1040 1046 repo_enable_statistics=enable_statistics
1041 1047 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1042 1048
1043 1049 repo_enable_locking=enable_locking
1044 1050 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1045 1051
1046 1052 repo_enable_downloads=enable_downloads
1047 1053 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1048 1054
1055 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1049 1056 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1050 1057 request.translate, repo=repo)
1058 ref_choices = list(set(ref_choices + [landing_ref]))
1051 1059
1052 1060 old_values = repo.get_api_data()
1053 1061 repo_type = repo.repo_type
1054 1062 schema = repo_schema.RepoSchema().bind(
1055 1063 repo_type_options=rhodecode.BACKENDS.keys(),
1056 1064 repo_ref_options=ref_choices,
1057 1065 repo_type=repo_type,
1058 1066 # user caller
1059 1067 user=apiuser,
1060 1068 old_values=old_values)
1061 1069 try:
1062 1070 schema_data = schema.deserialize(dict(
1063 1071 # we save old value, users cannot change type
1064 1072 repo_type=repo_type,
1065 1073
1066 1074 repo_name=updates['repo_name'],
1067 1075 repo_owner=updates['user'],
1068 1076 repo_description=updates['repo_description'],
1069 1077 repo_clone_uri=updates['clone_uri'],
1070 1078 repo_push_uri=updates['push_uri'],
1071 1079 repo_fork_of=updates['fork_id'],
1072 1080 repo_private=updates['repo_private'],
1073 1081 repo_landing_commit_ref=updates['repo_landing_rev'],
1074 1082 repo_enable_statistics=updates['repo_enable_statistics'],
1075 1083 repo_enable_downloads=updates['repo_enable_downloads'],
1076 1084 repo_enable_locking=updates['repo_enable_locking']))
1077 1085 except validation_schema.Invalid as err:
1078 1086 raise JSONRPCValidationError(colander_exc=err)
1079 1087
1080 1088 # save validated data back into the updates dict
1081 1089 validated_updates = dict(
1082 1090 repo_name=schema_data['repo_group']['repo_name_without_group'],
1083 1091 repo_group=schema_data['repo_group']['repo_group_id'],
1084 1092
1085 1093 user=schema_data['repo_owner'],
1086 1094 repo_description=schema_data['repo_description'],
1087 1095 repo_private=schema_data['repo_private'],
1088 1096 clone_uri=schema_data['repo_clone_uri'],
1089 1097 push_uri=schema_data['repo_push_uri'],
1090 1098 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1091 1099 repo_enable_statistics=schema_data['repo_enable_statistics'],
1092 1100 repo_enable_locking=schema_data['repo_enable_locking'],
1093 1101 repo_enable_downloads=schema_data['repo_enable_downloads'],
1094 1102 )
1095 1103
1096 1104 if schema_data['repo_fork_of']:
1097 1105 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1098 1106 validated_updates['fork_id'] = fork_repo.repo_id
1099 1107
1100 1108 # extra fields
1101 1109 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1102 1110 if fields:
1103 1111 validated_updates.update(fields)
1104 1112
1105 1113 try:
1106 1114 RepoModel().update(repo, **validated_updates)
1107 1115 audit_logger.store_api(
1108 1116 'repo.edit', action_data={'old_data': old_values},
1109 1117 user=apiuser, repo=repo)
1110 1118 Session().commit()
1111 1119 return {
1112 1120 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1113 1121 'repository': repo.get_api_data(include_secrets=include_secrets)
1114 1122 }
1115 1123 except Exception:
1116 1124 log.exception(
1117 1125 u"Exception while trying to update the repository %s",
1118 1126 repoid)
1119 1127 raise JSONRPCError('failed to update repo `%s`' % repoid)
1120 1128
1121 1129
1122 1130 @jsonrpc_method()
1123 1131 def fork_repo(request, apiuser, repoid, fork_name,
1124 1132 owner=Optional(OAttr('apiuser')),
1125 1133 description=Optional(''),
1126 1134 private=Optional(False),
1127 1135 clone_uri=Optional(None),
1128 landing_rev=Optional('rev:tip'),
1136 landing_rev=Optional(None),
1129 1137 copy_permissions=Optional(False)):
1130 1138 """
1131 1139 Creates a fork of the specified |repo|.
1132 1140
1133 1141 * If the fork_name contains "/", fork will be created inside
1134 1142 a repository group or nested repository groups
1135 1143
1136 1144 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1137 1145 inside group "foo/bar". You have to have permissions to access and
1138 1146 write to the last repository group ("bar" in this example)
1139 1147
1140 1148 This command can only be run using an |authtoken| with minimum
1141 1149 read permissions of the forked repo, create fork permissions for an user.
1142 1150
1143 1151 :param apiuser: This is filled automatically from the |authtoken|.
1144 1152 :type apiuser: AuthUser
1145 1153 :param repoid: Set repository name or repository ID.
1146 1154 :type repoid: str or int
1147 1155 :param fork_name: Set the fork name, including it's repository group membership.
1148 1156 :type fork_name: str
1149 1157 :param owner: Set the fork owner.
1150 1158 :type owner: str
1151 1159 :param description: Set the fork description.
1152 1160 :type description: str
1153 1161 :param copy_permissions: Copy permissions from parent |repo|. The
1154 1162 default is False.
1155 1163 :type copy_permissions: bool
1156 1164 :param private: Make the fork private. The default is False.
1157 1165 :type private: bool
1158 :param landing_rev: Set the landing revision. The default is tip.
1166 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1159 1167
1160 1168 Example output:
1161 1169
1162 1170 .. code-block:: bash
1163 1171
1164 1172 id : <id_for_response>
1165 1173 api_key : "<api_key>"
1166 1174 args: {
1167 1175 "repoid" : "<reponame or repo_id>",
1168 1176 "fork_name": "<forkname>",
1169 1177 "owner": "<username or user_id = Optional(=apiuser)>",
1170 1178 "description": "<description>",
1171 1179 "copy_permissions": "<bool>",
1172 1180 "private": "<bool>",
1173 1181 "landing_rev": "<landing_rev>"
1174 1182 }
1175 1183
1176 1184 Example error output:
1177 1185
1178 1186 .. code-block:: bash
1179 1187
1180 1188 id : <id_given_in_input>
1181 1189 result: {
1182 1190 "msg": "Created fork of `<reponame>` as `<forkname>`",
1183 1191 "success": true,
1184 1192 "task": "<celery task id or None if done sync>"
1185 1193 }
1186 1194 error: null
1187 1195
1188 1196 """
1189 1197
1190 1198 repo = get_repo_or_error(repoid)
1191 1199 repo_name = repo.repo_name
1192 1200
1193 1201 if not has_superadmin_permission(apiuser):
1194 1202 # check if we have at least read permission for
1195 1203 # this repo that we fork !
1196 1204 _perms = (
1197 1205 'repository.admin', 'repository.write', 'repository.read')
1198 1206 validate_repo_permissions(apiuser, repoid, repo, _perms)
1199 1207
1200 1208 # check if the regular user has at least fork permissions as well
1201 1209 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1202 1210 raise JSONRPCForbidden()
1203 1211
1204 1212 # check if user can set owner parameter
1205 1213 owner = validate_set_owner_permissions(apiuser, owner)
1206 1214
1207 1215 description = Optional.extract(description)
1208 1216 copy_permissions = Optional.extract(copy_permissions)
1209 1217 clone_uri = Optional.extract(clone_uri)
1210 landing_commit_ref = Optional.extract(landing_rev)
1218
1219 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1220 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1221 ref_choices = list(set(ref_choices + [landing_ref]))
1222 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1223
1211 1224 private = Optional.extract(private)
1212 1225
1213 1226 schema = repo_schema.RepoSchema().bind(
1214 1227 repo_type_options=rhodecode.BACKENDS.keys(),
1228 repo_ref_options=ref_choices,
1215 1229 repo_type=repo.repo_type,
1216 1230 # user caller
1217 1231 user=apiuser)
1218 1232
1219 1233 try:
1220 1234 schema_data = schema.deserialize(dict(
1221 1235 repo_name=fork_name,
1222 1236 repo_type=repo.repo_type,
1223 1237 repo_owner=owner.username,
1224 1238 repo_description=description,
1225 1239 repo_landing_commit_ref=landing_commit_ref,
1226 1240 repo_clone_uri=clone_uri,
1227 1241 repo_private=private,
1228 1242 repo_copy_permissions=copy_permissions))
1229 1243 except validation_schema.Invalid as err:
1230 1244 raise JSONRPCValidationError(colander_exc=err)
1231 1245
1232 1246 try:
1233 1247 data = {
1234 1248 'fork_parent_id': repo.repo_id,
1235 1249
1236 1250 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1237 1251 'repo_name_full': schema_data['repo_name'],
1238 1252 'repo_group': schema_data['repo_group']['repo_group_id'],
1239 1253 'repo_type': schema_data['repo_type'],
1240 1254 'description': schema_data['repo_description'],
1241 1255 'private': schema_data['repo_private'],
1242 1256 'copy_permissions': schema_data['repo_copy_permissions'],
1243 1257 'landing_rev': schema_data['repo_landing_commit_ref'],
1244 1258 }
1245 1259
1246 1260 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1247 1261 # no commit, it's done in RepoModel, or async via celery
1248 1262 task_id = get_task_id(task)
1249 1263
1250 1264 return {
1251 1265 'msg': 'Created fork of `%s` as `%s`' % (
1252 1266 repo.repo_name, schema_data['repo_name']),
1253 1267 'success': True, # cannot return the repo data here since fork
1254 1268 # can be done async
1255 1269 'task': task_id
1256 1270 }
1257 1271 except Exception:
1258 1272 log.exception(
1259 1273 u"Exception while trying to create fork %s",
1260 1274 schema_data['repo_name'])
1261 1275 raise JSONRPCError(
1262 1276 'failed to fork repository `%s` as `%s`' % (
1263 1277 repo_name, schema_data['repo_name']))
1264 1278
1265 1279
1266 1280 @jsonrpc_method()
1267 1281 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1268 1282 """
1269 1283 Deletes a repository.
1270 1284
1271 1285 * When the `forks` parameter is set it's possible to detach or delete
1272 1286 forks of deleted repository.
1273 1287
1274 1288 This command can only be run using an |authtoken| with admin
1275 1289 permissions on the |repo|.
1276 1290
1277 1291 :param apiuser: This is filled automatically from the |authtoken|.
1278 1292 :type apiuser: AuthUser
1279 1293 :param repoid: Set the repository name or repository ID.
1280 1294 :type repoid: str or int
1281 1295 :param forks: Set to `detach` or `delete` forks from the |repo|.
1282 1296 :type forks: Optional(str)
1283 1297
1284 1298 Example error output:
1285 1299
1286 1300 .. code-block:: bash
1287 1301
1288 1302 id : <id_given_in_input>
1289 1303 result: {
1290 1304 "msg": "Deleted repository `<reponame>`",
1291 1305 "success": true
1292 1306 }
1293 1307 error: null
1294 1308 """
1295 1309
1296 1310 repo = get_repo_or_error(repoid)
1297 1311 repo_name = repo.repo_name
1298 1312 if not has_superadmin_permission(apiuser):
1299 1313 _perms = ('repository.admin',)
1300 1314 validate_repo_permissions(apiuser, repoid, repo, _perms)
1301 1315
1302 1316 try:
1303 1317 handle_forks = Optional.extract(forks)
1304 1318 _forks_msg = ''
1305 1319 _forks = [f for f in repo.forks]
1306 1320 if handle_forks == 'detach':
1307 1321 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1308 1322 elif handle_forks == 'delete':
1309 1323 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1310 1324 elif _forks:
1311 1325 raise JSONRPCError(
1312 1326 'Cannot delete `%s` it still contains attached forks' %
1313 1327 (repo.repo_name,)
1314 1328 )
1315 1329 old_data = repo.get_api_data()
1316 1330 RepoModel().delete(repo, forks=forks)
1317 1331
1318 1332 repo = audit_logger.RepoWrap(repo_id=None,
1319 1333 repo_name=repo.repo_name)
1320 1334
1321 1335 audit_logger.store_api(
1322 1336 'repo.delete', action_data={'old_data': old_data},
1323 1337 user=apiuser, repo=repo)
1324 1338
1325 1339 ScmModel().mark_for_invalidation(repo_name, delete=True)
1326 1340 Session().commit()
1327 1341 return {
1328 1342 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1329 1343 'success': True
1330 1344 }
1331 1345 except Exception:
1332 1346 log.exception("Exception occurred while trying to delete repo")
1333 1347 raise JSONRPCError(
1334 1348 'failed to delete repository `%s`' % (repo_name,)
1335 1349 )
1336 1350
1337 1351
1338 1352 #TODO: marcink, change name ?
1339 1353 @jsonrpc_method()
1340 1354 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1341 1355 """
1342 1356 Invalidates the cache for the specified repository.
1343 1357
1344 1358 This command can only be run using an |authtoken| with admin rights to
1345 1359 the specified repository.
1346 1360
1347 1361 This command takes the following options:
1348 1362
1349 1363 :param apiuser: This is filled automatically from |authtoken|.
1350 1364 :type apiuser: AuthUser
1351 1365 :param repoid: Sets the repository name or repository ID.
1352 1366 :type repoid: str or int
1353 1367 :param delete_keys: This deletes the invalidated keys instead of
1354 1368 just flagging them.
1355 1369 :type delete_keys: Optional(``True`` | ``False``)
1356 1370
1357 1371 Example output:
1358 1372
1359 1373 .. code-block:: bash
1360 1374
1361 1375 id : <id_given_in_input>
1362 1376 result : {
1363 1377 'msg': Cache for repository `<repository name>` was invalidated,
1364 1378 'repository': <repository name>
1365 1379 }
1366 1380 error : null
1367 1381
1368 1382 Example error output:
1369 1383
1370 1384 .. code-block:: bash
1371 1385
1372 1386 id : <id_given_in_input>
1373 1387 result : null
1374 1388 error : {
1375 1389 'Error occurred during cache invalidation action'
1376 1390 }
1377 1391
1378 1392 """
1379 1393
1380 1394 repo = get_repo_or_error(repoid)
1381 1395 if not has_superadmin_permission(apiuser):
1382 1396 _perms = ('repository.admin', 'repository.write',)
1383 1397 validate_repo_permissions(apiuser, repoid, repo, _perms)
1384 1398
1385 1399 delete = Optional.extract(delete_keys)
1386 1400 try:
1387 1401 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1388 1402 return {
1389 1403 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1390 1404 'repository': repo.repo_name
1391 1405 }
1392 1406 except Exception:
1393 1407 log.exception(
1394 1408 "Exception occurred while trying to invalidate repo cache")
1395 1409 raise JSONRPCError(
1396 1410 'Error occurred during cache invalidation action'
1397 1411 )
1398 1412
1399 1413
1400 1414 #TODO: marcink, change name ?
1401 1415 @jsonrpc_method()
1402 1416 def lock(request, apiuser, repoid, locked=Optional(None),
1403 1417 userid=Optional(OAttr('apiuser'))):
1404 1418 """
1405 1419 Sets the lock state of the specified |repo| by the given user.
1406 1420 From more information, see :ref:`repo-locking`.
1407 1421
1408 1422 * If the ``userid`` option is not set, the repository is locked to the
1409 1423 user who called the method.
1410 1424 * If the ``locked`` parameter is not set, the current lock state of the
1411 1425 repository is displayed.
1412 1426
1413 1427 This command can only be run using an |authtoken| with admin rights to
1414 1428 the specified repository.
1415 1429
1416 1430 This command takes the following options:
1417 1431
1418 1432 :param apiuser: This is filled automatically from the |authtoken|.
1419 1433 :type apiuser: AuthUser
1420 1434 :param repoid: Sets the repository name or repository ID.
1421 1435 :type repoid: str or int
1422 1436 :param locked: Sets the lock state.
1423 1437 :type locked: Optional(``True`` | ``False``)
1424 1438 :param userid: Set the repository lock to this user.
1425 1439 :type userid: Optional(str or int)
1426 1440
1427 1441 Example error output:
1428 1442
1429 1443 .. code-block:: bash
1430 1444
1431 1445 id : <id_given_in_input>
1432 1446 result : {
1433 1447 'repo': '<reponame>',
1434 1448 'locked': <bool: lock state>,
1435 1449 'locked_since': <int: lock timestamp>,
1436 1450 'locked_by': <username of person who made the lock>,
1437 1451 'lock_reason': <str: reason for locking>,
1438 1452 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1439 1453 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1440 1454 or
1441 1455 'msg': 'Repo `<repository name>` not locked.'
1442 1456 or
1443 1457 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1444 1458 }
1445 1459 error : null
1446 1460
1447 1461 Example error output:
1448 1462
1449 1463 .. code-block:: bash
1450 1464
1451 1465 id : <id_given_in_input>
1452 1466 result : null
1453 1467 error : {
1454 1468 'Error occurred locking repository `<reponame>`'
1455 1469 }
1456 1470 """
1457 1471
1458 1472 repo = get_repo_or_error(repoid)
1459 1473 if not has_superadmin_permission(apiuser):
1460 1474 # check if we have at least write permission for this repo !
1461 1475 _perms = ('repository.admin', 'repository.write',)
1462 1476 validate_repo_permissions(apiuser, repoid, repo, _perms)
1463 1477
1464 1478 # make sure normal user does not pass someone else userid,
1465 1479 # he is not allowed to do that
1466 1480 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1467 1481 raise JSONRPCError('userid is not the same as your user')
1468 1482
1469 1483 if isinstance(userid, Optional):
1470 1484 userid = apiuser.user_id
1471 1485
1472 1486 user = get_user_or_error(userid)
1473 1487
1474 1488 if isinstance(locked, Optional):
1475 1489 lockobj = repo.locked
1476 1490
1477 1491 if lockobj[0] is None:
1478 1492 _d = {
1479 1493 'repo': repo.repo_name,
1480 1494 'locked': False,
1481 1495 'locked_since': None,
1482 1496 'locked_by': None,
1483 1497 'lock_reason': None,
1484 1498 'lock_state_changed': False,
1485 1499 'msg': 'Repo `%s` not locked.' % repo.repo_name
1486 1500 }
1487 1501 return _d
1488 1502 else:
1489 1503 _user_id, _time, _reason = lockobj
1490 1504 lock_user = get_user_or_error(userid)
1491 1505 _d = {
1492 1506 'repo': repo.repo_name,
1493 1507 'locked': True,
1494 1508 'locked_since': _time,
1495 1509 'locked_by': lock_user.username,
1496 1510 'lock_reason': _reason,
1497 1511 'lock_state_changed': False,
1498 1512 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1499 1513 % (repo.repo_name, lock_user.username,
1500 1514 json.dumps(time_to_datetime(_time))))
1501 1515 }
1502 1516 return _d
1503 1517
1504 1518 # force locked state through a flag
1505 1519 else:
1506 1520 locked = str2bool(locked)
1507 1521 lock_reason = Repository.LOCK_API
1508 1522 try:
1509 1523 if locked:
1510 1524 lock_time = time.time()
1511 1525 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1512 1526 else:
1513 1527 lock_time = None
1514 1528 Repository.unlock(repo)
1515 1529 _d = {
1516 1530 'repo': repo.repo_name,
1517 1531 'locked': locked,
1518 1532 'locked_since': lock_time,
1519 1533 'locked_by': user.username,
1520 1534 'lock_reason': lock_reason,
1521 1535 'lock_state_changed': True,
1522 1536 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1523 1537 % (user.username, repo.repo_name, locked))
1524 1538 }
1525 1539 return _d
1526 1540 except Exception:
1527 1541 log.exception(
1528 1542 "Exception occurred while trying to lock repository")
1529 1543 raise JSONRPCError(
1530 1544 'Error occurred locking repository `%s`' % repo.repo_name
1531 1545 )
1532 1546
1533 1547
1534 1548 @jsonrpc_method()
1535 1549 def comment_commit(
1536 1550 request, apiuser, repoid, commit_id, message, status=Optional(None),
1537 1551 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1538 1552 resolves_comment_id=Optional(None),
1539 1553 userid=Optional(OAttr('apiuser'))):
1540 1554 """
1541 1555 Set a commit comment, and optionally change the status of the commit.
1542 1556
1543 1557 :param apiuser: This is filled automatically from the |authtoken|.
1544 1558 :type apiuser: AuthUser
1545 1559 :param repoid: Set the repository name or repository ID.
1546 1560 :type repoid: str or int
1547 1561 :param commit_id: Specify the commit_id for which to set a comment.
1548 1562 :type commit_id: str
1549 1563 :param message: The comment text.
1550 1564 :type message: str
1551 1565 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1552 1566 'approved', 'rejected', 'under_review'
1553 1567 :type status: str
1554 1568 :param comment_type: Comment type, one of: 'note', 'todo'
1555 1569 :type comment_type: Optional(str), default: 'note'
1556 1570 :param userid: Set the user name of the comment creator.
1557 1571 :type userid: Optional(str or int)
1558 1572
1559 1573 Example error output:
1560 1574
1561 1575 .. code-block:: bash
1562 1576
1563 1577 {
1564 1578 "id" : <id_given_in_input>,
1565 1579 "result" : {
1566 1580 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1567 1581 "status_change": null or <status>,
1568 1582 "success": true
1569 1583 },
1570 1584 "error" : null
1571 1585 }
1572 1586
1573 1587 """
1574 1588 repo = get_repo_or_error(repoid)
1575 1589 if not has_superadmin_permission(apiuser):
1576 1590 _perms = ('repository.read', 'repository.write', 'repository.admin')
1577 1591 validate_repo_permissions(apiuser, repoid, repo, _perms)
1578 1592
1579 1593 try:
1580 1594 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1581 1595 except Exception as e:
1582 1596 log.exception('Failed to fetch commit')
1583 1597 raise JSONRPCError(safe_str(e))
1584 1598
1585 1599 if isinstance(userid, Optional):
1586 1600 userid = apiuser.user_id
1587 1601
1588 1602 user = get_user_or_error(userid)
1589 1603 status = Optional.extract(status)
1590 1604 comment_type = Optional.extract(comment_type)
1591 1605 resolves_comment_id = Optional.extract(resolves_comment_id)
1592 1606
1593 1607 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1594 1608 if status and status not in allowed_statuses:
1595 1609 raise JSONRPCError('Bad status, must be on '
1596 1610 'of %s got %s' % (allowed_statuses, status,))
1597 1611
1598 1612 if resolves_comment_id:
1599 1613 comment = ChangesetComment.get(resolves_comment_id)
1600 1614 if not comment:
1601 1615 raise JSONRPCError(
1602 1616 'Invalid resolves_comment_id `%s` for this commit.'
1603 1617 % resolves_comment_id)
1604 1618 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1605 1619 raise JSONRPCError(
1606 1620 'Comment `%s` is wrong type for setting status to resolved.'
1607 1621 % resolves_comment_id)
1608 1622
1609 1623 try:
1610 1624 rc_config = SettingsModel().get_all_settings()
1611 1625 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1612 1626 status_change_label = ChangesetStatus.get_status_lbl(status)
1613 1627 comment = CommentsModel().create(
1614 1628 message, repo, user, commit_id=commit_id,
1615 1629 status_change=status_change_label,
1616 1630 status_change_type=status,
1617 1631 renderer=renderer,
1618 1632 comment_type=comment_type,
1619 1633 resolves_comment_id=resolves_comment_id,
1620 1634 auth_user=apiuser
1621 1635 )
1622 1636 if status:
1623 1637 # also do a status change
1624 1638 try:
1625 1639 ChangesetStatusModel().set_status(
1626 1640 repo, status, user, comment, revision=commit_id,
1627 1641 dont_allow_on_closed_pull_request=True
1628 1642 )
1629 1643 except StatusChangeOnClosedPullRequestError:
1630 1644 log.exception(
1631 1645 "Exception occurred while trying to change repo commit status")
1632 1646 msg = ('Changing status on a changeset associated with '
1633 1647 'a closed pull request is not allowed')
1634 1648 raise JSONRPCError(msg)
1635 1649
1636 1650 Session().commit()
1637 1651 return {
1638 1652 'msg': (
1639 1653 'Commented on commit `%s` for repository `%s`' % (
1640 1654 comment.revision, repo.repo_name)),
1641 1655 'status_change': status,
1642 1656 'success': True,
1643 1657 }
1644 1658 except JSONRPCError:
1645 1659 # catch any inside errors, and re-raise them to prevent from
1646 1660 # below global catch to silence them
1647 1661 raise
1648 1662 except Exception:
1649 1663 log.exception("Exception occurred while trying to comment on commit")
1650 1664 raise JSONRPCError(
1651 1665 'failed to set comment on repository `%s`' % (repo.repo_name,)
1652 1666 )
1653 1667
1654 1668
1655 1669 @jsonrpc_method()
1656 1670 def get_repo_comments(request, apiuser, repoid,
1657 1671 commit_id=Optional(None), comment_type=Optional(None),
1658 1672 userid=Optional(None)):
1659 1673 """
1660 1674 Get all comments for a repository
1661 1675
1662 1676 :param apiuser: This is filled automatically from the |authtoken|.
1663 1677 :type apiuser: AuthUser
1664 1678 :param repoid: Set the repository name or repository ID.
1665 1679 :type repoid: str or int
1666 1680 :param commit_id: Optionally filter the comments by the commit_id
1667 1681 :type commit_id: Optional(str), default: None
1668 1682 :param comment_type: Optionally filter the comments by the comment_type
1669 1683 one of: 'note', 'todo'
1670 1684 :type comment_type: Optional(str), default: None
1671 1685 :param userid: Optionally filter the comments by the author of comment
1672 1686 :type userid: Optional(str or int), Default: None
1673 1687
1674 1688 Example error output:
1675 1689
1676 1690 .. code-block:: bash
1677 1691
1678 1692 {
1679 1693 "id" : <id_given_in_input>,
1680 1694 "result" : [
1681 1695 {
1682 1696 "comment_author": <USER_DETAILS>,
1683 1697 "comment_created_on": "2017-02-01T14:38:16.309",
1684 1698 "comment_f_path": "file.txt",
1685 1699 "comment_id": 282,
1686 1700 "comment_lineno": "n1",
1687 1701 "comment_resolved_by": null,
1688 1702 "comment_status": [],
1689 1703 "comment_text": "This file needs a header",
1690 1704 "comment_type": "todo"
1691 1705 }
1692 1706 ],
1693 1707 "error" : null
1694 1708 }
1695 1709
1696 1710 """
1697 1711 repo = get_repo_or_error(repoid)
1698 1712 if not has_superadmin_permission(apiuser):
1699 1713 _perms = ('repository.read', 'repository.write', 'repository.admin')
1700 1714 validate_repo_permissions(apiuser, repoid, repo, _perms)
1701 1715
1702 1716 commit_id = Optional.extract(commit_id)
1703 1717
1704 1718 userid = Optional.extract(userid)
1705 1719 if userid:
1706 1720 user = get_user_or_error(userid)
1707 1721 else:
1708 1722 user = None
1709 1723
1710 1724 comment_type = Optional.extract(comment_type)
1711 1725 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1712 1726 raise JSONRPCError(
1713 1727 'comment_type must be one of `{}` got {}'.format(
1714 1728 ChangesetComment.COMMENT_TYPES, comment_type)
1715 1729 )
1716 1730
1717 1731 comments = CommentsModel().get_repository_comments(
1718 1732 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1719 1733 return comments
1720 1734
1721 1735
1722 1736 @jsonrpc_method()
1723 1737 def grant_user_permission(request, apiuser, repoid, userid, perm):
1724 1738 """
1725 1739 Grant permissions for the specified user on the given repository,
1726 1740 or update existing permissions if found.
1727 1741
1728 1742 This command can only be run using an |authtoken| with admin
1729 1743 permissions on the |repo|.
1730 1744
1731 1745 :param apiuser: This is filled automatically from the |authtoken|.
1732 1746 :type apiuser: AuthUser
1733 1747 :param repoid: Set the repository name or repository ID.
1734 1748 :type repoid: str or int
1735 1749 :param userid: Set the user name.
1736 1750 :type userid: str
1737 1751 :param perm: Set the user permissions, using the following format
1738 1752 ``(repository.(none|read|write|admin))``
1739 1753 :type perm: str
1740 1754
1741 1755 Example output:
1742 1756
1743 1757 .. code-block:: bash
1744 1758
1745 1759 id : <id_given_in_input>
1746 1760 result: {
1747 1761 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1748 1762 "success": true
1749 1763 }
1750 1764 error: null
1751 1765 """
1752 1766
1753 1767 repo = get_repo_or_error(repoid)
1754 1768 user = get_user_or_error(userid)
1755 1769 perm = get_perm_or_error(perm)
1756 1770 if not has_superadmin_permission(apiuser):
1757 1771 _perms = ('repository.admin',)
1758 1772 validate_repo_permissions(apiuser, repoid, repo, _perms)
1759 1773
1760 1774 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1761 1775 try:
1762 1776 changes = RepoModel().update_permissions(
1763 1777 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1764 1778
1765 1779 action_data = {
1766 1780 'added': changes['added'],
1767 1781 'updated': changes['updated'],
1768 1782 'deleted': changes['deleted'],
1769 1783 }
1770 1784 audit_logger.store_api(
1771 1785 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1772 1786
1773 1787 Session().commit()
1774 1788 return {
1775 1789 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1776 1790 perm.permission_name, user.username, repo.repo_name
1777 1791 ),
1778 1792 'success': True
1779 1793 }
1780 1794 except Exception:
1781 1795 log.exception("Exception occurred while trying edit permissions for repo")
1782 1796 raise JSONRPCError(
1783 1797 'failed to edit permission for user: `%s` in repo: `%s`' % (
1784 1798 userid, repoid
1785 1799 )
1786 1800 )
1787 1801
1788 1802
1789 1803 @jsonrpc_method()
1790 1804 def revoke_user_permission(request, apiuser, repoid, userid):
1791 1805 """
1792 1806 Revoke permission for a user on the specified repository.
1793 1807
1794 1808 This command can only be run using an |authtoken| with admin
1795 1809 permissions on the |repo|.
1796 1810
1797 1811 :param apiuser: This is filled automatically from the |authtoken|.
1798 1812 :type apiuser: AuthUser
1799 1813 :param repoid: Set the repository name or repository ID.
1800 1814 :type repoid: str or int
1801 1815 :param userid: Set the user name of revoked user.
1802 1816 :type userid: str or int
1803 1817
1804 1818 Example error output:
1805 1819
1806 1820 .. code-block:: bash
1807 1821
1808 1822 id : <id_given_in_input>
1809 1823 result: {
1810 1824 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1811 1825 "success": true
1812 1826 }
1813 1827 error: null
1814 1828 """
1815 1829
1816 1830 repo = get_repo_or_error(repoid)
1817 1831 user = get_user_or_error(userid)
1818 1832 if not has_superadmin_permission(apiuser):
1819 1833 _perms = ('repository.admin',)
1820 1834 validate_repo_permissions(apiuser, repoid, repo, _perms)
1821 1835
1822 1836 perm_deletions = [[user.user_id, None, "user"]]
1823 1837 try:
1824 1838 changes = RepoModel().update_permissions(
1825 1839 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1826 1840
1827 1841 action_data = {
1828 1842 'added': changes['added'],
1829 1843 'updated': changes['updated'],
1830 1844 'deleted': changes['deleted'],
1831 1845 }
1832 1846 audit_logger.store_api(
1833 1847 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1834 1848
1835 1849 Session().commit()
1836 1850 return {
1837 1851 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1838 1852 user.username, repo.repo_name
1839 1853 ),
1840 1854 'success': True
1841 1855 }
1842 1856 except Exception:
1843 1857 log.exception("Exception occurred while trying revoke permissions to repo")
1844 1858 raise JSONRPCError(
1845 1859 'failed to edit permission for user: `%s` in repo: `%s`' % (
1846 1860 userid, repoid
1847 1861 )
1848 1862 )
1849 1863
1850 1864
1851 1865 @jsonrpc_method()
1852 1866 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1853 1867 """
1854 1868 Grant permission for a user group on the specified repository,
1855 1869 or update existing permissions.
1856 1870
1857 1871 This command can only be run using an |authtoken| with admin
1858 1872 permissions on the |repo|.
1859 1873
1860 1874 :param apiuser: This is filled automatically from the |authtoken|.
1861 1875 :type apiuser: AuthUser
1862 1876 :param repoid: Set the repository name or repository ID.
1863 1877 :type repoid: str or int
1864 1878 :param usergroupid: Specify the ID of the user group.
1865 1879 :type usergroupid: str or int
1866 1880 :param perm: Set the user group permissions using the following
1867 1881 format: (repository.(none|read|write|admin))
1868 1882 :type perm: str
1869 1883
1870 1884 Example output:
1871 1885
1872 1886 .. code-block:: bash
1873 1887
1874 1888 id : <id_given_in_input>
1875 1889 result : {
1876 1890 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1877 1891 "success": true
1878 1892
1879 1893 }
1880 1894 error : null
1881 1895
1882 1896 Example error output:
1883 1897
1884 1898 .. code-block:: bash
1885 1899
1886 1900 id : <id_given_in_input>
1887 1901 result : null
1888 1902 error : {
1889 1903 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1890 1904 }
1891 1905
1892 1906 """
1893 1907
1894 1908 repo = get_repo_or_error(repoid)
1895 1909 perm = get_perm_or_error(perm)
1896 1910 if not has_superadmin_permission(apiuser):
1897 1911 _perms = ('repository.admin',)
1898 1912 validate_repo_permissions(apiuser, repoid, repo, _perms)
1899 1913
1900 1914 user_group = get_user_group_or_error(usergroupid)
1901 1915 if not has_superadmin_permission(apiuser):
1902 1916 # check if we have at least read permission for this user group !
1903 1917 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1904 1918 if not HasUserGroupPermissionAnyApi(*_perms)(
1905 1919 user=apiuser, user_group_name=user_group.users_group_name):
1906 1920 raise JSONRPCError(
1907 1921 'user group `%s` does not exist' % (usergroupid,))
1908 1922
1909 1923 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1910 1924 try:
1911 1925 changes = RepoModel().update_permissions(
1912 1926 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1913 1927 action_data = {
1914 1928 'added': changes['added'],
1915 1929 'updated': changes['updated'],
1916 1930 'deleted': changes['deleted'],
1917 1931 }
1918 1932 audit_logger.store_api(
1919 1933 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1920 1934
1921 1935 Session().commit()
1922 1936 return {
1923 1937 'msg': 'Granted perm: `%s` for user group: `%s` in '
1924 1938 'repo: `%s`' % (
1925 1939 perm.permission_name, user_group.users_group_name,
1926 1940 repo.repo_name
1927 1941 ),
1928 1942 'success': True
1929 1943 }
1930 1944 except Exception:
1931 1945 log.exception(
1932 1946 "Exception occurred while trying change permission on repo")
1933 1947 raise JSONRPCError(
1934 1948 'failed to edit permission for user group: `%s` in '
1935 1949 'repo: `%s`' % (
1936 1950 usergroupid, repo.repo_name
1937 1951 )
1938 1952 )
1939 1953
1940 1954
1941 1955 @jsonrpc_method()
1942 1956 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1943 1957 """
1944 1958 Revoke the permissions of a user group on a given repository.
1945 1959
1946 1960 This command can only be run using an |authtoken| with admin
1947 1961 permissions on the |repo|.
1948 1962
1949 1963 :param apiuser: This is filled automatically from the |authtoken|.
1950 1964 :type apiuser: AuthUser
1951 1965 :param repoid: Set the repository name or repository ID.
1952 1966 :type repoid: str or int
1953 1967 :param usergroupid: Specify the user group ID.
1954 1968 :type usergroupid: str or int
1955 1969
1956 1970 Example output:
1957 1971
1958 1972 .. code-block:: bash
1959 1973
1960 1974 id : <id_given_in_input>
1961 1975 result: {
1962 1976 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1963 1977 "success": true
1964 1978 }
1965 1979 error: null
1966 1980 """
1967 1981
1968 1982 repo = get_repo_or_error(repoid)
1969 1983 if not has_superadmin_permission(apiuser):
1970 1984 _perms = ('repository.admin',)
1971 1985 validate_repo_permissions(apiuser, repoid, repo, _perms)
1972 1986
1973 1987 user_group = get_user_group_or_error(usergroupid)
1974 1988 if not has_superadmin_permission(apiuser):
1975 1989 # check if we have at least read permission for this user group !
1976 1990 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1977 1991 if not HasUserGroupPermissionAnyApi(*_perms)(
1978 1992 user=apiuser, user_group_name=user_group.users_group_name):
1979 1993 raise JSONRPCError(
1980 1994 'user group `%s` does not exist' % (usergroupid,))
1981 1995
1982 1996 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1983 1997 try:
1984 1998 changes = RepoModel().update_permissions(
1985 1999 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1986 2000 action_data = {
1987 2001 'added': changes['added'],
1988 2002 'updated': changes['updated'],
1989 2003 'deleted': changes['deleted'],
1990 2004 }
1991 2005 audit_logger.store_api(
1992 2006 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1993 2007
1994 2008 Session().commit()
1995 2009 return {
1996 2010 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1997 2011 user_group.users_group_name, repo.repo_name
1998 2012 ),
1999 2013 'success': True
2000 2014 }
2001 2015 except Exception:
2002 2016 log.exception("Exception occurred while trying revoke "
2003 2017 "user group permission on repo")
2004 2018 raise JSONRPCError(
2005 2019 'failed to edit permission for user group: `%s` in '
2006 2020 'repo: `%s`' % (
2007 2021 user_group.users_group_name, repo.repo_name
2008 2022 )
2009 2023 )
2010 2024
2011 2025
2012 2026 @jsonrpc_method()
2013 2027 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2014 2028 """
2015 2029 Triggers a pull on the given repository from a remote location. You
2016 2030 can use this to keep remote repositories up-to-date.
2017 2031
2018 2032 This command can only be run using an |authtoken| with admin
2019 2033 rights to the specified repository. For more information,
2020 2034 see :ref:`config-token-ref`.
2021 2035
2022 2036 This command takes the following options:
2023 2037
2024 2038 :param apiuser: This is filled automatically from the |authtoken|.
2025 2039 :type apiuser: AuthUser
2026 2040 :param repoid: The repository name or repository ID.
2027 2041 :type repoid: str or int
2028 2042 :param remote_uri: Optional remote URI to pass in for pull
2029 2043 :type remote_uri: str
2030 2044
2031 2045 Example output:
2032 2046
2033 2047 .. code-block:: bash
2034 2048
2035 2049 id : <id_given_in_input>
2036 2050 result : {
2037 2051 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2038 2052 "repository": "<repository name>"
2039 2053 }
2040 2054 error : null
2041 2055
2042 2056 Example error output:
2043 2057
2044 2058 .. code-block:: bash
2045 2059
2046 2060 id : <id_given_in_input>
2047 2061 result : null
2048 2062 error : {
2049 2063 "Unable to push changes from `<remote_url>`"
2050 2064 }
2051 2065
2052 2066 """
2053 2067
2054 2068 repo = get_repo_or_error(repoid)
2055 2069 remote_uri = Optional.extract(remote_uri)
2056 2070 remote_uri_display = remote_uri or repo.clone_uri_hidden
2057 2071 if not has_superadmin_permission(apiuser):
2058 2072 _perms = ('repository.admin',)
2059 2073 validate_repo_permissions(apiuser, repoid, repo, _perms)
2060 2074
2061 2075 try:
2062 2076 ScmModel().pull_changes(
2063 2077 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2064 2078 return {
2065 2079 'msg': 'Pulled from url `%s` on repo `%s`' % (
2066 2080 remote_uri_display, repo.repo_name),
2067 2081 'repository': repo.repo_name
2068 2082 }
2069 2083 except Exception:
2070 2084 log.exception("Exception occurred while trying to "
2071 2085 "pull changes from remote location")
2072 2086 raise JSONRPCError(
2073 2087 'Unable to pull changes from `%s`' % remote_uri_display
2074 2088 )
2075 2089
2076 2090
2077 2091 @jsonrpc_method()
2078 2092 def strip(request, apiuser, repoid, revision, branch):
2079 2093 """
2080 2094 Strips the given revision from the specified repository.
2081 2095
2082 2096 * This will remove the revision and all of its decendants.
2083 2097
2084 2098 This command can only be run using an |authtoken| with admin rights to
2085 2099 the specified repository.
2086 2100
2087 2101 This command takes the following options:
2088 2102
2089 2103 :param apiuser: This is filled automatically from the |authtoken|.
2090 2104 :type apiuser: AuthUser
2091 2105 :param repoid: The repository name or repository ID.
2092 2106 :type repoid: str or int
2093 2107 :param revision: The revision you wish to strip.
2094 2108 :type revision: str
2095 2109 :param branch: The branch from which to strip the revision.
2096 2110 :type branch: str
2097 2111
2098 2112 Example output:
2099 2113
2100 2114 .. code-block:: bash
2101 2115
2102 2116 id : <id_given_in_input>
2103 2117 result : {
2104 2118 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2105 2119 "repository": "<repository name>"
2106 2120 }
2107 2121 error : null
2108 2122
2109 2123 Example error output:
2110 2124
2111 2125 .. code-block:: bash
2112 2126
2113 2127 id : <id_given_in_input>
2114 2128 result : null
2115 2129 error : {
2116 2130 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2117 2131 }
2118 2132
2119 2133 """
2120 2134
2121 2135 repo = get_repo_or_error(repoid)
2122 2136 if not has_superadmin_permission(apiuser):
2123 2137 _perms = ('repository.admin',)
2124 2138 validate_repo_permissions(apiuser, repoid, repo, _perms)
2125 2139
2126 2140 try:
2127 2141 ScmModel().strip(repo, revision, branch)
2128 2142 audit_logger.store_api(
2129 2143 'repo.commit.strip', action_data={'commit_id': revision},
2130 2144 repo=repo,
2131 2145 user=apiuser, commit=True)
2132 2146
2133 2147 return {
2134 2148 'msg': 'Stripped commit %s from repo `%s`' % (
2135 2149 revision, repo.repo_name),
2136 2150 'repository': repo.repo_name
2137 2151 }
2138 2152 except Exception:
2139 2153 log.exception("Exception while trying to strip")
2140 2154 raise JSONRPCError(
2141 2155 'Unable to strip commit %s from repo `%s`' % (
2142 2156 revision, repo.repo_name)
2143 2157 )
2144 2158
2145 2159
2146 2160 @jsonrpc_method()
2147 2161 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2148 2162 """
2149 2163 Returns all settings for a repository. If key is given it only returns the
2150 2164 setting identified by the key or null.
2151 2165
2152 2166 :param apiuser: This is filled automatically from the |authtoken|.
2153 2167 :type apiuser: AuthUser
2154 2168 :param repoid: The repository name or repository id.
2155 2169 :type repoid: str or int
2156 2170 :param key: Key of the setting to return.
2157 2171 :type: key: Optional(str)
2158 2172
2159 2173 Example output:
2160 2174
2161 2175 .. code-block:: bash
2162 2176
2163 2177 {
2164 2178 "error": null,
2165 2179 "id": 237,
2166 2180 "result": {
2167 2181 "extensions_largefiles": true,
2168 2182 "extensions_evolve": true,
2169 2183 "hooks_changegroup_push_logger": true,
2170 2184 "hooks_changegroup_repo_size": false,
2171 2185 "hooks_outgoing_pull_logger": true,
2172 2186 "phases_publish": "True",
2173 2187 "rhodecode_hg_use_rebase_for_merging": true,
2174 2188 "rhodecode_pr_merge_enabled": true,
2175 2189 "rhodecode_use_outdated_comments": true
2176 2190 }
2177 2191 }
2178 2192 """
2179 2193
2180 2194 # Restrict access to this api method to admins only.
2181 2195 if not has_superadmin_permission(apiuser):
2182 2196 raise JSONRPCForbidden()
2183 2197
2184 2198 try:
2185 2199 repo = get_repo_or_error(repoid)
2186 2200 settings_model = VcsSettingsModel(repo=repo)
2187 2201 settings = settings_model.get_global_settings()
2188 2202 settings.update(settings_model.get_repo_settings())
2189 2203
2190 2204 # If only a single setting is requested fetch it from all settings.
2191 2205 key = Optional.extract(key)
2192 2206 if key is not None:
2193 2207 settings = settings.get(key, None)
2194 2208 except Exception:
2195 2209 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2196 2210 log.exception(msg)
2197 2211 raise JSONRPCError(msg)
2198 2212
2199 2213 return settings
2200 2214
2201 2215
2202 2216 @jsonrpc_method()
2203 2217 def set_repo_settings(request, apiuser, repoid, settings):
2204 2218 """
2205 2219 Update repository settings. Returns true on success.
2206 2220
2207 2221 :param apiuser: This is filled automatically from the |authtoken|.
2208 2222 :type apiuser: AuthUser
2209 2223 :param repoid: The repository name or repository id.
2210 2224 :type repoid: str or int
2211 2225 :param settings: The new settings for the repository.
2212 2226 :type: settings: dict
2213 2227
2214 2228 Example output:
2215 2229
2216 2230 .. code-block:: bash
2217 2231
2218 2232 {
2219 2233 "error": null,
2220 2234 "id": 237,
2221 2235 "result": true
2222 2236 }
2223 2237 """
2224 2238 # Restrict access to this api method to admins only.
2225 2239 if not has_superadmin_permission(apiuser):
2226 2240 raise JSONRPCForbidden()
2227 2241
2228 2242 if type(settings) is not dict:
2229 2243 raise JSONRPCError('Settings have to be a JSON Object.')
2230 2244
2231 2245 try:
2232 2246 settings_model = VcsSettingsModel(repo=repoid)
2233 2247
2234 2248 # Merge global, repo and incoming settings.
2235 2249 new_settings = settings_model.get_global_settings()
2236 2250 new_settings.update(settings_model.get_repo_settings())
2237 2251 new_settings.update(settings)
2238 2252
2239 2253 # Update the settings.
2240 2254 inherit_global_settings = new_settings.get(
2241 2255 'inherit_global_settings', False)
2242 2256 settings_model.create_or_update_repo_settings(
2243 2257 new_settings, inherit_global_settings=inherit_global_settings)
2244 2258 Session().commit()
2245 2259 except Exception:
2246 2260 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2247 2261 log.exception(msg)
2248 2262 raise JSONRPCError(msg)
2249 2263
2250 2264 # Indicate success.
2251 2265 return True
2252 2266
2253 2267
2254 2268 @jsonrpc_method()
2255 2269 def maintenance(request, apiuser, repoid):
2256 2270 """
2257 2271 Triggers a maintenance on the given repository.
2258 2272
2259 2273 This command can only be run using an |authtoken| with admin
2260 2274 rights to the specified repository. For more information,
2261 2275 see :ref:`config-token-ref`.
2262 2276
2263 2277 This command takes the following options:
2264 2278
2265 2279 :param apiuser: This is filled automatically from the |authtoken|.
2266 2280 :type apiuser: AuthUser
2267 2281 :param repoid: The repository name or repository ID.
2268 2282 :type repoid: str or int
2269 2283
2270 2284 Example output:
2271 2285
2272 2286 .. code-block:: bash
2273 2287
2274 2288 id : <id_given_in_input>
2275 2289 result : {
2276 2290 "msg": "executed maintenance command",
2277 2291 "executed_actions": [
2278 2292 <action_message>, <action_message2>...
2279 2293 ],
2280 2294 "repository": "<repository name>"
2281 2295 }
2282 2296 error : null
2283 2297
2284 2298 Example error output:
2285 2299
2286 2300 .. code-block:: bash
2287 2301
2288 2302 id : <id_given_in_input>
2289 2303 result : null
2290 2304 error : {
2291 2305 "Unable to execute maintenance on `<reponame>`"
2292 2306 }
2293 2307
2294 2308 """
2295 2309
2296 2310 repo = get_repo_or_error(repoid)
2297 2311 if not has_superadmin_permission(apiuser):
2298 2312 _perms = ('repository.admin',)
2299 2313 validate_repo_permissions(apiuser, repoid, repo, _perms)
2300 2314
2301 2315 try:
2302 2316 maintenance = repo_maintenance.RepoMaintenance()
2303 2317 executed_actions = maintenance.execute(repo)
2304 2318
2305 2319 return {
2306 2320 'msg': 'executed maintenance command',
2307 2321 'executed_actions': executed_actions,
2308 2322 'repository': repo.repo_name
2309 2323 }
2310 2324 except Exception:
2311 2325 log.exception("Exception occurred while trying to run maintenance")
2312 2326 raise JSONRPCError(
2313 2327 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,189 +1,186 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.celerylib.utils import get_task_id
33 33
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, CSRFRequired, NotAnonymous,
37 37 HasPermissionAny, HasRepoGroupPermissionAny)
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.utils import repo_name_slug
40 40 from rhodecode.lib.utils2 import safe_int, safe_unicode
41 41 from rhodecode.model.forms import RepoForm
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.db import Repository, RepoGroup
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminReposView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 59 perm_set=['group.write', 'group.admin'])
60 60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
62 c.landing_revs_choices, c.landing_revs = \
63 ScmModel().get_repo_landing_revs(self.request.translate)
64 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
65 63
66 64 @LoginRequired()
67 65 @NotAnonymous()
68 66 # perms check inside
69 67 @view_config(
70 68 route_name='repos', request_method='GET',
71 69 renderer='rhodecode:templates/admin/repos/repos.mako')
72 70 def repository_list(self):
73 71 c = self.load_default_context()
74 72
75 73 repo_list = Repository.get_all_repos()
76 74 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
77 75 repos_data = RepoModel().get_repos_as_dict(
78 76 repo_list=c.repo_list, admin=True, super_user_actions=True)
79 77 # json used to render the grid
80 78 c.data = json.dumps(repos_data)
81 79
82 80 return self._get_template_context(c)
83 81
84 82 @LoginRequired()
85 83 @NotAnonymous()
86 84 # perms check inside
87 85 @view_config(
88 86 route_name='repo_new', request_method='GET',
89 87 renderer='rhodecode:templates/admin/repos/repo_add.mako')
90 88 def repository_new(self):
91 89 c = self.load_default_context()
92 90
93 91 new_repo = self.request.GET.get('repo', '')
94 92 parent_group = safe_int(self.request.GET.get('parent_group'))
95 93 _gr = RepoGroup.get(parent_group)
96 94
97 95 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
98 96 # you're not super admin nor have global create permissions,
99 97 # but maybe you have at least write permission to a parent group ?
100 98
101 99 gr_name = _gr.group_name if _gr else None
102 100 # create repositories with write permission on group is set to true
103 101 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
104 102 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
105 103 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
106 104 if not (group_admin or (group_write and create_on_write)):
107 105 raise HTTPForbidden()
108 106
109 107 self._load_form_data(c)
110 108 c.new_repo = repo_name_slug(new_repo)
111 109
112 110 # apply the defaults from defaults page
113 111 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
114 112 # set checkbox to autochecked
115 113 defaults['repo_copy_permissions'] = True
116 114
117 115 parent_group_choice = '-1'
118 116 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
119 117 parent_group_choice = self._rhodecode_user.personal_repo_group
120 118
121 119 if parent_group and _gr:
122 120 if parent_group in [x[0] for x in c.repo_groups]:
123 121 parent_group_choice = safe_unicode(parent_group)
124 122
125 123 defaults.update({'repo_group': parent_group_choice})
126 124
127 125 data = render('rhodecode:templates/admin/repos/repo_add.mako',
128 126 self._get_template_context(c), self.request)
129 127 html = formencode.htmlfill.render(
130 128 data,
131 129 defaults=defaults,
132 130 encoding="UTF-8",
133 131 force_defaults=False
134 132 )
135 133 return Response(html)
136 134
137 135 @LoginRequired()
138 136 @NotAnonymous()
139 137 @CSRFRequired()
140 138 # perms check inside
141 139 @view_config(
142 140 route_name='repo_create', request_method='POST',
143 141 renderer='rhodecode:templates/admin/repos/repos.mako')
144 142 def repository_create(self):
145 143 c = self.load_default_context()
146 144
147 145 form_result = {}
148 146 self._load_form_data(c)
149 147
150 148 try:
151 149 # CanWriteToGroup validators checks permissions of this POST
152 150 form = RepoForm(
153 self.request.translate, repo_groups=c.repo_groups_choices,
154 landing_revs=c.landing_revs_choices)()
151 self.request.translate, repo_groups=c.repo_groups_choices)()
155 152 form_result = form.to_python(dict(self.request.POST))
156 153 copy_permissions = form_result.get('repo_copy_permissions')
157 154 # create is done sometimes async on celery, db transaction
158 155 # management is handled there.
159 156 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
160 157 task_id = get_task_id(task)
161 158 except formencode.Invalid as errors:
162 159 data = render('rhodecode:templates/admin/repos/repo_add.mako',
163 160 self._get_template_context(c), self.request)
164 161 html = formencode.htmlfill.render(
165 162 data,
166 163 defaults=errors.value,
167 164 errors=errors.error_dict or {},
168 165 prefix_error=False,
169 166 encoding="UTF-8",
170 167 force_defaults=False
171 168 )
172 169 return Response(html)
173 170
174 171 except Exception as e:
175 172 msg = self._log_creation_exception(e, form_result.get('repo_name'))
176 173 h.flash(msg, category='error')
177 174 raise HTTPFound(h.route_path('home'))
178 175
179 176 repo_name = form_result.get('repo_name_full')
180 177
181 178 affected_user_ids = [self._rhodecode_user.user_id]
182 179 if copy_permissions:
183 180 # permission flush is done in repo creating
184 181 pass
185 182 events.trigger(events.UserPermissionsChange(affected_user_ids))
186 183
187 184 raise HTTPFound(
188 185 h.route_path('repo_creating', repo_name=repo_name,
189 186 _query=dict(task_id=task_id)))
@@ -1,266 +1,264 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 36 import rhodecode.lib.helpers as h
37 37 from rhodecode.lib.celerylib.utils import get_task_id
38 38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.forms import RepoForkForm
41 41 from rhodecode.model.scm import ScmModel, RepoGroupList
42 42 from rhodecode.lib.utils2 import safe_int, safe_unicode
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class RepoForksView(RepoAppView, DataGridAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context(include_app_defaults=True)
51 51 c.rhodecode_repo = self.rhodecode_vcs_repo
52 52
53 53 acl_groups = RepoGroupList(
54 54 RepoGroup.query().all(),
55 55 perm_set=['group.write', 'group.admin'])
56 56 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
57 57 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
58 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
59 self.request.translate)
60 c.landing_revs_choices = choices
58
61 59 c.personal_repo_group = c.rhodecode_user.personal_repo_group
62 60
63 61 return c
64 62
65 63 @LoginRequired()
66 64 @HasRepoPermissionAnyDecorator(
67 65 'repository.read', 'repository.write', 'repository.admin')
68 66 @view_config(
69 67 route_name='repo_forks_show_all', request_method='GET',
70 68 renderer='rhodecode:templates/forks/forks.mako')
71 69 def repo_forks_show_all(self):
72 70 c = self.load_default_context()
73 71 return self._get_template_context(c)
74 72
75 73 @LoginRequired()
76 74 @HasRepoPermissionAnyDecorator(
77 75 'repository.read', 'repository.write', 'repository.admin')
78 76 @view_config(
79 77 route_name='repo_forks_data', request_method='GET',
80 78 renderer='json_ext', xhr=True)
81 79 def repo_forks_data(self):
82 80 _ = self.request.translate
83 81 self.load_default_context()
84 82 column_map = {
85 83 'fork_name': 'repo_name',
86 84 'fork_date': 'created_on',
87 85 'last_activity': 'updated_on'
88 86 }
89 87 draw, start, limit = self._extract_chunk(self.request)
90 88 search_q, order_by, order_dir = self._extract_ordering(
91 89 self.request, column_map=column_map)
92 90
93 91 acl_check = HasRepoPermissionAny(
94 92 'repository.read', 'repository.write', 'repository.admin')
95 93 repo_id = self.db_repo.repo_id
96 94 allowed_ids = [-1]
97 95 for f in Repository.query().filter(Repository.fork_id == repo_id):
98 96 if acl_check(f.repo_name, 'get forks check'):
99 97 allowed_ids.append(f.repo_id)
100 98
101 99 forks_data_total_count = Repository.query()\
102 100 .filter(Repository.fork_id == repo_id)\
103 101 .filter(Repository.repo_id.in_(allowed_ids))\
104 102 .count()
105 103
106 104 # json generate
107 105 base_q = Repository.query()\
108 106 .filter(Repository.fork_id == repo_id)\
109 107 .filter(Repository.repo_id.in_(allowed_ids))\
110 108
111 109 if search_q:
112 110 like_expression = u'%{}%'.format(safe_unicode(search_q))
113 111 base_q = base_q.filter(or_(
114 112 Repository.repo_name.ilike(like_expression),
115 113 Repository.description.ilike(like_expression),
116 114 ))
117 115
118 116 forks_data_total_filtered_count = base_q.count()
119 117
120 118 sort_col = getattr(Repository, order_by, None)
121 119 if sort_col:
122 120 if order_dir == 'asc':
123 121 # handle null values properly to order by NULL last
124 122 if order_by in ['last_activity']:
125 123 sort_col = coalesce(sort_col, datetime.date.max)
126 124 sort_col = sort_col.asc()
127 125 else:
128 126 # handle null values properly to order by NULL last
129 127 if order_by in ['last_activity']:
130 128 sort_col = coalesce(sort_col, datetime.date.min)
131 129 sort_col = sort_col.desc()
132 130
133 131 base_q = base_q.order_by(sort_col)
134 132 base_q = base_q.offset(start).limit(limit)
135 133
136 134 fork_list = base_q.all()
137 135
138 136 def fork_actions(fork):
139 137 url_link = h.route_path(
140 138 'repo_compare',
141 139 repo_name=fork.repo_name,
142 140 source_ref_type=self.db_repo.landing_rev[0],
143 141 source_ref=self.db_repo.landing_rev[1],
144 142 target_ref_type=self.db_repo.landing_rev[0],
145 143 target_ref=self.db_repo.landing_rev[1],
146 144 _query=dict(merge=1, target_repo=f.repo_name))
147 145 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
148 146
149 147 def fork_name(fork):
150 148 return h.link_to(fork.repo_name,
151 149 h.route_path('repo_summary', repo_name=fork.repo_name))
152 150
153 151 forks_data = []
154 152 for fork in fork_list:
155 153 forks_data.append({
156 154 "username": h.gravatar_with_user(self.request, fork.user.username),
157 155 "fork_name": fork_name(fork),
158 156 "description": fork.description_safe,
159 157 "fork_date": h.age_component(fork.created_on, time_is_local=True),
160 158 "last_activity": h.format_date(fork.updated_on),
161 159 "action": fork_actions(fork),
162 160 })
163 161
164 162 data = ({
165 163 'draw': draw,
166 164 'data': forks_data,
167 165 'recordsTotal': forks_data_total_count,
168 166 'recordsFiltered': forks_data_total_filtered_count,
169 167 })
170 168
171 169 return data
172 170
173 171 @LoginRequired()
174 172 @NotAnonymous()
175 173 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
176 174 @HasRepoPermissionAnyDecorator(
177 175 'repository.read', 'repository.write', 'repository.admin')
178 176 @view_config(
179 177 route_name='repo_fork_new', request_method='GET',
180 178 renderer='rhodecode:templates/forks/forks.mako')
181 179 def repo_fork_new(self):
182 180 c = self.load_default_context()
183 181
184 182 defaults = RepoModel()._get_defaults(self.db_repo_name)
185 183 # alter the description to indicate a fork
186 184 defaults['description'] = (
187 185 'fork of repository: %s \n%s' % (
188 186 defaults['repo_name'], defaults['description']))
189 187 # add suffix to fork
190 188 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
191 189
192 190 data = render('rhodecode:templates/forks/fork.mako',
193 191 self._get_template_context(c), self.request)
194 192 html = formencode.htmlfill.render(
195 193 data,
196 194 defaults=defaults,
197 195 encoding="UTF-8",
198 196 force_defaults=False
199 197 )
200 198 return Response(html)
201 199
202 200 @LoginRequired()
203 201 @NotAnonymous()
204 202 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
205 203 @HasRepoPermissionAnyDecorator(
206 204 'repository.read', 'repository.write', 'repository.admin')
207 205 @CSRFRequired()
208 206 @view_config(
209 207 route_name='repo_fork_create', request_method='POST',
210 208 renderer='rhodecode:templates/forks/fork.mako')
211 209 def repo_fork_create(self):
212 210 _ = self.request.translate
213 211 c = self.load_default_context()
214 212
215 _form = RepoForkForm(self.request.translate, old_data={'repo_type': self.db_repo.repo_type},
216 repo_groups=c.repo_groups_choices,
217 landing_revs=c.landing_revs_choices)()
213 _form = RepoForkForm(self.request.translate,
214 old_data={'repo_type': self.db_repo.repo_type},
215 repo_groups=c.repo_groups_choices)()
218 216 post_data = dict(self.request.POST)
219 217
220 218 # forbid injecting other repo by forging a request
221 219 post_data['fork_parent_id'] = self.db_repo.repo_id
222 220
223 221 form_result = {}
224 222 task_id = None
225 223 try:
226 224 form_result = _form.to_python(post_data)
227 225 copy_permissions = form_result.get('copy_permissions')
228 226 # create fork is done sometimes async on celery, db transaction
229 227 # management is handled there.
230 228 task = RepoModel().create_fork(
231 229 form_result, c.rhodecode_user.user_id)
232 230
233 231 task_id = get_task_id(task)
234 232 except formencode.Invalid as errors:
235 233 c.rhodecode_db_repo = self.db_repo
236 234
237 235 data = render('rhodecode:templates/forks/fork.mako',
238 236 self._get_template_context(c), self.request)
239 237 html = formencode.htmlfill.render(
240 238 data,
241 239 defaults=errors.value,
242 240 errors=errors.error_dict or {},
243 241 prefix_error=False,
244 242 encoding="UTF-8",
245 243 force_defaults=False
246 244 )
247 245 return Response(html)
248 246 except Exception:
249 247 log.exception(
250 248 u'Exception while trying to fork the repository %s', self.db_repo_name)
251 249 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
252 250 h.flash(msg, category='error')
253 251 raise HTTPFound(h.route_path('home'))
254 252
255 253 repo_name = form_result.get('repo_name_full', self.db_repo_name)
256 254
257 255 affected_user_ids = [self._rhodecode_user.user_id]
258 256 if copy_permissions:
259 257 # permission flush is done in repo creating
260 258 pass
261 259
262 260 events.trigger(events.UserPermissionsChange(affected_user_ids))
263 261
264 262 raise HTTPFound(
265 263 h.route_path('repo_creating', repo_name=repo_name,
266 264 _query=dict(task_id=task_id)))
@@ -1,396 +1,376 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 string
23 import time
24
23 25 import rhodecode
24 26
25 27 from pyramid.view import view_config
26 28
27 29 from rhodecode.lib.view_utils import get_format_ref_id
28 30 from rhodecode.apps._base import RepoAppView
29 31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 32 from rhodecode.lib import helpers as h, rc_cache
31 33 from rhodecode.lib.utils2 import safe_str, safe_int
32 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 35 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 36 from rhodecode.lib.ext_json import json
35 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 38 from rhodecode.lib.vcs.exceptions import (
37 39 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 40 from rhodecode.model.db import Statistics, CacheKey, User
39 41 from rhodecode.model.meta import Session
40 42 from rhodecode.model.repo import ReadmeFinder
41 43 from rhodecode.model.scm import ScmModel
42 44
43 45 log = logging.getLogger(__name__)
44 46
45 47
46 48 class RepoSummaryView(RepoAppView):
47 49
48 50 def load_default_context(self):
49 51 c = self._get_local_tmpl_context(include_app_defaults=True)
50 52 c.rhodecode_repo = None
51 53 if not c.repository_requirements_missing:
52 54 c.rhodecode_repo = self.rhodecode_vcs_repo
53 55 return c
54 56
55 57 def _get_readme_data(self, db_repo, renderer_type):
56
57 58 log.debug('Looking for README file')
59 landing_commit = db_repo.get_landing_commit()
60 if isinstance(landing_commit, EmptyCommit):
61 return None, None
58 62
59 63 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
60 64 db_repo.repo_id, CacheKey.CACHE_TYPE_README)
61 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
62 repo_id=self.db_repo.repo_id)
63 65 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
66 start = time.time()
64 67
65 68 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
66 def generate_repo_readme(repo_id, _repo_name, _renderer_type):
69 def generate_repo_readme(repo_id, commit_id, _repo_name, _renderer_type):
67 70 readme_data = None
68 readme_node = None
69 71 readme_filename = None
70 commit = self._get_landing_commit_or_none(db_repo)
71 if commit:
72 log.debug("Searching for a README file.")
73 readme_node = ReadmeFinder(_renderer_type).search(commit)
72
73 commit = db_repo.get_commit(commit_id)
74 log.debug("Searching for a README file at commit %s.", commit_id)
75 readme_node = ReadmeFinder(_renderer_type).search(commit)
76
74 77 if readme_node:
75 78 log.debug('Found README node: %s', readme_node)
76 79 relative_urls = {
77 80 'raw': h.route_path(
78 81 'repo_file_raw', repo_name=_repo_name,
79 82 commit_id=commit.raw_id, f_path=readme_node.path),
80 83 'standard': h.route_path(
81 84 'repo_files', repo_name=_repo_name,
82 85 commit_id=commit.raw_id, f_path=readme_node.path),
83 86 }
84 readme_data = self._render_readme_or_none(
85 commit, readme_node, relative_urls)
87 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
86 88 readme_filename = readme_node.unicode_path
87 89
88 90 return readme_data, readme_filename
89 91
90 inv_context_manager = rc_cache.InvalidationContext(
91 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
92 with inv_context_manager as invalidation_context:
93 args = (db_repo.repo_id, db_repo.repo_name, renderer_type,)
94 # re-compute and store cache if we get invalidate signal
95 if invalidation_context.should_invalidate():
96 instance = generate_repo_readme.refresh(*args)
97 else:
98 instance = generate_repo_readme(*args)
99
100 log.debug(
101 'Repo readme generated and computed in %.4fs',
102 inv_context_manager.compute_time)
103 return instance
104
105 def _get_landing_commit_or_none(self, db_repo):
106 log.debug("Getting the landing commit.")
107 try:
108 commit = db_repo.get_landing_commit()
109 if not isinstance(commit, EmptyCommit):
110 return commit
111 else:
112 log.debug("Repository is empty, no README to render.")
113 except CommitError:
114 log.exception(
115 "Problem getting commit when trying to render the README.")
92 readme_data, readme_filename = generate_repo_readme(
93 db_repo.repo_id, landing_commit.raw_id, db_repo.repo_name, renderer_type,)
94 compute_time = time.time() - start
95 log.debug('Repo readme generated and computed in %.4fs', compute_time)
96 return readme_data, readme_filename
116 97
117 98 def _render_readme_or_none(self, commit, readme_node, relative_urls):
118 log.debug(
119 'Found README file `%s` rendering...', readme_node.path)
99 log.debug('Found README file `%s` rendering...', readme_node.path)
120 100 renderer = MarkupRenderer()
121 101 try:
122 102 html_source = renderer.render(
123 103 readme_node.content, filename=readme_node.path)
124 104 if relative_urls:
125 105 return relative_links(html_source, relative_urls)
126 106 return html_source
127 107 except Exception:
128 108 log.exception(
129 109 "Exception while trying to render the README")
130 110
131 111 def _load_commits_context(self, c):
132 112 p = safe_int(self.request.GET.get('page'), 1)
133 113 size = safe_int(self.request.GET.get('size'), 10)
134 114
135 115 def url_generator(**kw):
136 116 query_params = {
137 117 'size': size
138 118 }
139 119 query_params.update(kw)
140 120 return h.route_path(
141 121 'repo_summary_commits',
142 122 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
143 123
144 124 pre_load = ['author', 'branch', 'date', 'message']
145 125 try:
146 126 collection = self.rhodecode_vcs_repo.get_commits(
147 127 pre_load=pre_load, translate_tags=False)
148 128 except EmptyRepositoryError:
149 129 collection = self.rhodecode_vcs_repo
150 130
151 131 c.repo_commits = h.RepoPage(
152 132 collection, page=p, items_per_page=size, url=url_generator)
153 133 page_ids = [x.raw_id for x in c.repo_commits]
154 134 c.comments = self.db_repo.get_comments(page_ids)
155 135 c.statuses = self.db_repo.statuses(page_ids)
156 136
157 137 def _prepare_and_set_clone_url(self, c):
158 138 username = ''
159 139 if self._rhodecode_user.username != User.DEFAULT_USER:
160 140 username = safe_str(self._rhodecode_user.username)
161 141
162 142 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
163 143 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
164 144
165 145 if '{repo}' in _def_clone_uri:
166 146 _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}')
167 147 elif '{repoid}' in _def_clone_uri:
168 148 _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}')
169 149
170 150 c.clone_repo_url = self.db_repo.clone_url(
171 151 user=username, uri_tmpl=_def_clone_uri)
172 152 c.clone_repo_url_id = self.db_repo.clone_url(
173 153 user=username, uri_tmpl=_def_clone_uri_id)
174 154 c.clone_repo_url_ssh = self.db_repo.clone_url(
175 155 uri_tmpl=_def_clone_uri_ssh, ssh=True)
176 156
177 157 @LoginRequired()
178 158 @HasRepoPermissionAnyDecorator(
179 159 'repository.read', 'repository.write', 'repository.admin')
180 160 @view_config(
181 161 route_name='repo_summary_commits', request_method='GET',
182 162 renderer='rhodecode:templates/summary/summary_commits.mako')
183 163 def summary_commits(self):
184 164 c = self.load_default_context()
185 165 self._prepare_and_set_clone_url(c)
186 166 self._load_commits_context(c)
187 167 return self._get_template_context(c)
188 168
189 169 @LoginRequired()
190 170 @HasRepoPermissionAnyDecorator(
191 171 'repository.read', 'repository.write', 'repository.admin')
192 172 @view_config(
193 173 route_name='repo_summary', request_method='GET',
194 174 renderer='rhodecode:templates/summary/summary.mako')
195 175 @view_config(
196 176 route_name='repo_summary_slash', request_method='GET',
197 177 renderer='rhodecode:templates/summary/summary.mako')
198 178 @view_config(
199 179 route_name='repo_summary_explicit', request_method='GET',
200 180 renderer='rhodecode:templates/summary/summary.mako')
201 181 def summary(self):
202 182 c = self.load_default_context()
203 183
204 184 # Prepare the clone URL
205 185 self._prepare_and_set_clone_url(c)
206 186
207 187 # update every 5 min
208 188 if self.db_repo.last_commit_cache_update_diff > 60 * 5:
209 189 self.db_repo.update_commit_cache()
210 190
211 191 # If enabled, get statistics data
212 192
213 193 c.show_stats = bool(self.db_repo.enable_statistics)
214 194
215 195 stats = Session().query(Statistics) \
216 196 .filter(Statistics.repository == self.db_repo) \
217 197 .scalar()
218 198
219 199 c.stats_percentage = 0
220 200
221 201 if stats and stats.languages:
222 202 c.no_data = False is self.db_repo.enable_statistics
223 203 lang_stats_d = json.loads(stats.languages)
224 204
225 205 # Sort first by decreasing count and second by the file extension,
226 206 # so we have a consistent output.
227 207 lang_stats_items = sorted(lang_stats_d.iteritems(),
228 208 key=lambda k: (-k[1], k[0]))[:10]
229 209 lang_stats = [(x, {"count": y,
230 210 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
231 211 for x, y in lang_stats_items]
232 212
233 213 c.trending_languages = json.dumps(lang_stats)
234 214 else:
235 215 c.no_data = True
236 216 c.trending_languages = json.dumps({})
237 217
238 218 scm_model = ScmModel()
239 219 c.enable_downloads = self.db_repo.enable_downloads
240 220 c.repository_followers = scm_model.get_followers(self.db_repo)
241 221 c.repository_forks = scm_model.get_forks(self.db_repo)
242 222
243 223 # first interaction with the VCS instance after here...
244 224 if c.repository_requirements_missing:
245 225 self.request.override_renderer = \
246 226 'rhodecode:templates/summary/missing_requirements.mako'
247 227 return self._get_template_context(c)
248 228
249 229 c.readme_data, c.readme_file = \
250 230 self._get_readme_data(self.db_repo, c.visual.default_renderer)
251 231
252 232 # loads the summary commits template context
253 233 self._load_commits_context(c)
254 234
255 235 return self._get_template_context(c)
256 236
257 237 def get_request_commit_id(self):
258 238 return self.request.matchdict['commit_id']
259 239
260 240 @LoginRequired()
261 241 @HasRepoPermissionAnyDecorator(
262 242 'repository.read', 'repository.write', 'repository.admin')
263 243 @view_config(
264 244 route_name='repo_stats', request_method='GET',
265 245 renderer='json_ext')
266 246 def repo_stats(self):
267 247 commit_id = self.get_request_commit_id()
268 248 show_stats = bool(self.db_repo.enable_statistics)
269 249 repo_id = self.db_repo.repo_id
270 250
271 251 cache_seconds = safe_int(
272 252 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
273 253 cache_on = cache_seconds > 0
274 254 log.debug(
275 255 'Computing REPO TREE for repo_id %s commit_id `%s` '
276 256 'with caching: %s[TTL: %ss]' % (
277 257 repo_id, commit_id, cache_on, cache_seconds or 0))
278 258
279 259 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
280 260 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
281 261
282 262 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
283 263 condition=cache_on)
284 264 def compute_stats(repo_id, commit_id, show_stats):
285 265 code_stats = {}
286 266 size = 0
287 267 try:
288 268 scm_instance = self.db_repo.scm_instance()
289 269 commit = scm_instance.get_commit(commit_id)
290 270
291 271 for node in commit.get_filenodes_generator():
292 272 size += node.size
293 273 if not show_stats:
294 274 continue
295 275 ext = string.lower(node.extension)
296 276 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
297 277 if ext_info:
298 278 if ext in code_stats:
299 279 code_stats[ext]['count'] += 1
300 280 else:
301 281 code_stats[ext] = {"count": 1, "desc": ext_info}
302 282 except (EmptyRepositoryError, CommitDoesNotExistError):
303 283 pass
304 284 return {'size': h.format_byte_size_binary(size),
305 285 'code_stats': code_stats}
306 286
307 287 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
308 288 return stats
309 289
310 290 @LoginRequired()
311 291 @HasRepoPermissionAnyDecorator(
312 292 'repository.read', 'repository.write', 'repository.admin')
313 293 @view_config(
314 294 route_name='repo_refs_data', request_method='GET',
315 295 renderer='json_ext')
316 296 def repo_refs_data(self):
317 297 _ = self.request.translate
318 298 self.load_default_context()
319 299
320 300 repo = self.rhodecode_vcs_repo
321 301 refs_to_create = [
322 302 (_("Branch"), repo.branches, 'branch'),
323 303 (_("Tag"), repo.tags, 'tag'),
324 304 (_("Bookmark"), repo.bookmarks, 'book'),
325 305 ]
326 306 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
327 307 data = {
328 308 'more': False,
329 309 'results': res
330 310 }
331 311 return data
332 312
333 313 @LoginRequired()
334 314 @HasRepoPermissionAnyDecorator(
335 315 'repository.read', 'repository.write', 'repository.admin')
336 316 @view_config(
337 317 route_name='repo_refs_changelog_data', request_method='GET',
338 318 renderer='json_ext')
339 319 def repo_refs_changelog_data(self):
340 320 _ = self.request.translate
341 321 self.load_default_context()
342 322
343 323 repo = self.rhodecode_vcs_repo
344 324
345 325 refs_to_create = [
346 326 (_("Branches"), repo.branches, 'branch'),
347 327 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
348 328 # TODO: enable when vcs can handle bookmarks filters
349 329 # (_("Bookmarks"), repo.bookmarks, "book"),
350 330 ]
351 331 res = self._create_reference_data(
352 332 repo, self.db_repo_name, refs_to_create)
353 333 data = {
354 334 'more': False,
355 335 'results': res
356 336 }
357 337 return data
358 338
359 339 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
360 340 format_ref_id = get_format_ref_id(repo)
361 341
362 342 result = []
363 343 for title, refs, ref_type in refs_to_create:
364 344 if refs:
365 345 result.append({
366 346 'text': title,
367 347 'children': self._create_reference_items(
368 348 repo, full_repo_name, refs, ref_type,
369 349 format_ref_id),
370 350 })
371 351 return result
372 352
373 353 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
374 354 result = []
375 355 is_svn = h.is_svn(repo)
376 356 for ref_name, raw_id in refs.iteritems():
377 357 files_url = self._create_files_url(
378 358 repo, full_repo_name, ref_name, raw_id, is_svn)
379 359 result.append({
380 360 'text': ref_name,
381 361 'id': format_ref_id(ref_name, raw_id),
382 362 'raw_id': raw_id,
383 363 'type': ref_type,
384 364 'files_url': files_url,
385 365 'idx': 0,
386 366 })
387 367 return result
388 368
389 369 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
390 370 use_commit_id = '/' in ref_name or is_svn
391 371 return h.route_path(
392 372 'repo_files',
393 373 repo_name=full_repo_name,
394 374 f_path=ref_name if is_svn else '',
395 375 commit_id=raw_id if use_commit_id else ref_name,
396 376 _query=dict(at=ref_name))
@@ -1,340 +1,343 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2019 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 RhodeCode task modules, containing all task that suppose to be run
23 23 by celery daemon
24 24 """
25 25
26 26 import os
27 27 import time
28 28
29 29 from pyramid import compat
30 30 from pyramid_mailer.mailer import Mailer
31 31 from pyramid_mailer.message import Message
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 36 from rhodecode.lib.hooks_base import log_create_repository
37 37 from rhodecode.lib.utils2 import safe_int, str2bool
38 38 from rhodecode.model.db import Session, IntegrityError, Repository, User, true
39 39
40 40
41 41 @async_task(ignore_result=True, base=RequestContextTask)
42 42 def send_email(recipients, subject, body='', html_body='', email_config=None):
43 43 """
44 44 Sends an email with defined parameters from the .ini files.
45 45
46 46 :param recipients: list of recipients, it this is empty the defined email
47 47 address from field 'email_to' is used instead
48 48 :param subject: subject of the mail
49 49 :param body: body of the mail
50 50 :param html_body: html version of body
51 51 """
52 52 log = get_logger(send_email)
53 53
54 54 email_config = email_config or rhodecode.CONFIG
55 55
56 56 mail_server = email_config.get('smtp_server') or None
57 57 if mail_server is None:
58 58 log.error("SMTP server information missing. Sending email failed. "
59 59 "Make sure that `smtp_server` variable is configured "
60 60 "inside the .ini file")
61 61 return False
62 62
63 63 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
64 64
65 65 if recipients:
66 66 if isinstance(recipients, compat.string_types):
67 67 recipients = recipients.split(',')
68 68 else:
69 69 # if recipients are not defined we send to email_config + all admins
70 70 admins = []
71 71 for u in User.query().filter(User.admin == true()).all():
72 72 if u.email:
73 73 admins.append(u.email)
74 74 recipients = []
75 75 config_email = email_config.get('email_to')
76 76 if config_email:
77 77 recipients += [config_email]
78 78 recipients += admins
79 79
80 80 # translate our LEGACY config into the one that pyramid_mailer supports
81 81 email_conf = dict(
82 82 host=mail_server,
83 83 port=email_config.get('smtp_port', 25),
84 84 username=email_config.get('smtp_username'),
85 85 password=email_config.get('smtp_password'),
86 86
87 87 tls=str2bool(email_config.get('smtp_use_tls')),
88 88 ssl=str2bool(email_config.get('smtp_use_ssl')),
89 89
90 90 # SSL key file
91 91 # keyfile='',
92 92
93 93 # SSL certificate file
94 94 # certfile='',
95 95
96 96 # Location of maildir
97 97 # queue_path='',
98 98
99 99 default_sender=email_config.get('app_email_from', 'RhodeCode'),
100 100
101 101 debug=str2bool(email_config.get('smtp_debug')),
102 102 # /usr/sbin/sendmail Sendmail executable
103 103 # sendmail_app='',
104 104
105 105 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
106 106 # sendmail_template='',
107 107 )
108 108
109 109 try:
110 110 mailer = Mailer(**email_conf)
111 111
112 112 message = Message(subject=subject,
113 113 sender=email_conf['default_sender'],
114 114 recipients=recipients,
115 115 body=body, html=html_body)
116 116 mailer.send_immediately(message)
117 117
118 118 except Exception:
119 119 log.exception('Mail sending failed')
120 120 return False
121 121 return True
122 122
123 123
124 124 @async_task(ignore_result=True, base=RequestContextTask)
125 125 def create_repo(form_data, cur_user):
126 126 from rhodecode.model.repo import RepoModel
127 127 from rhodecode.model.user import UserModel
128 from rhodecode.model.scm import ScmModel
128 129 from rhodecode.model.settings import SettingsModel
129 130
130 131 log = get_logger(create_repo)
131 132
132 133 cur_user = UserModel()._get_user(cur_user)
133 134 owner = cur_user
134 135
135 136 repo_name = form_data['repo_name']
136 137 repo_name_full = form_data['repo_name_full']
137 138 repo_type = form_data['repo_type']
138 139 description = form_data['repo_description']
139 140 private = form_data['repo_private']
140 141 clone_uri = form_data.get('clone_uri')
141 142 repo_group = safe_int(form_data['repo_group'])
142 landing_rev = form_data['repo_landing_rev']
143 143 copy_fork_permissions = form_data.get('copy_permissions')
144 144 copy_group_permissions = form_data.get('repo_copy_permissions')
145 145 fork_of = form_data.get('fork_parent_id')
146 146 state = form_data.get('repo_state', Repository.STATE_PENDING)
147 147
148 148 # repo creation defaults, private and repo_type are filled in form
149 149 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
150 150 enable_statistics = form_data.get(
151 151 'enable_statistics', defs.get('repo_enable_statistics'))
152 152 enable_locking = form_data.get(
153 153 'enable_locking', defs.get('repo_enable_locking'))
154 154 enable_downloads = form_data.get(
155 155 'enable_downloads', defs.get('repo_enable_downloads'))
156 156
157 # set landing rev based on default branches for SCM
158 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
159
157 160 try:
158 161 RepoModel()._create_repo(
159 162 repo_name=repo_name_full,
160 163 repo_type=repo_type,
161 164 description=description,
162 165 owner=owner,
163 166 private=private,
164 167 clone_uri=clone_uri,
165 168 repo_group=repo_group,
166 landing_rev=landing_rev,
169 landing_rev=landing_ref,
167 170 fork_of=fork_of,
168 171 copy_fork_permissions=copy_fork_permissions,
169 172 copy_group_permissions=copy_group_permissions,
170 173 enable_statistics=enable_statistics,
171 174 enable_locking=enable_locking,
172 175 enable_downloads=enable_downloads,
173 176 state=state
174 177 )
175 178 Session().commit()
176 179
177 180 # now create this repo on Filesystem
178 181 RepoModel()._create_filesystem_repo(
179 182 repo_name=repo_name,
180 183 repo_type=repo_type,
181 184 repo_group=RepoModel()._get_repo_group(repo_group),
182 185 clone_uri=clone_uri,
183 186 )
184 187 repo = Repository.get_by_repo_name(repo_name_full)
185 188 log_create_repository(created_by=owner.username, **repo.get_dict())
186 189
187 190 # update repo commit caches initially
188 191 repo.update_commit_cache()
189 192
190 193 # set new created state
191 194 repo.set_state(Repository.STATE_CREATED)
192 195 repo_id = repo.repo_id
193 196 repo_data = repo.get_api_data()
194 197
195 198 audit_logger.store(
196 199 'repo.create', action_data={'data': repo_data},
197 200 user=cur_user,
198 201 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
199 202
200 203 Session().commit()
201 204 except Exception as e:
202 205 log.warning('Exception occurred when creating repository, '
203 206 'doing cleanup...', exc_info=True)
204 207 if isinstance(e, IntegrityError):
205 208 Session().rollback()
206 209
207 210 # rollback things manually !
208 211 repo = Repository.get_by_repo_name(repo_name_full)
209 212 if repo:
210 213 Repository.delete(repo.repo_id)
211 214 Session().commit()
212 215 RepoModel()._delete_filesystem_repo(repo)
213 216 log.info('Cleanup of repo %s finished', repo_name_full)
214 217 raise
215 218
216 219 return True
217 220
218 221
219 222 @async_task(ignore_result=True, base=RequestContextTask)
220 223 def create_repo_fork(form_data, cur_user):
221 224 """
222 225 Creates a fork of repository using internal VCS methods
223 226 """
224 227 from rhodecode.model.repo import RepoModel
225 228 from rhodecode.model.user import UserModel
226 229
227 230 log = get_logger(create_repo_fork)
228 231
229 232 cur_user = UserModel()._get_user(cur_user)
230 233 owner = cur_user
231 234
232 235 repo_name = form_data['repo_name'] # fork in this case
233 236 repo_name_full = form_data['repo_name_full']
234 237 repo_type = form_data['repo_type']
235 238 description = form_data['description']
236 239 private = form_data['private']
237 240 clone_uri = form_data.get('clone_uri')
238 241 repo_group = safe_int(form_data['repo_group'])
239 242 landing_rev = form_data['landing_rev']
240 243 copy_fork_permissions = form_data.get('copy_permissions')
241 244 fork_id = safe_int(form_data.get('fork_parent_id'))
242 245
243 246 try:
244 247 fork_of = RepoModel()._get_repo(fork_id)
245 248 RepoModel()._create_repo(
246 249 repo_name=repo_name_full,
247 250 repo_type=repo_type,
248 251 description=description,
249 252 owner=owner,
250 253 private=private,
251 254 clone_uri=clone_uri,
252 255 repo_group=repo_group,
253 256 landing_rev=landing_rev,
254 257 fork_of=fork_of,
255 258 copy_fork_permissions=copy_fork_permissions
256 259 )
257 260
258 261 Session().commit()
259 262
260 263 base_path = Repository.base_path()
261 264 source_repo_path = os.path.join(base_path, fork_of.repo_name)
262 265
263 266 # now create this repo on Filesystem
264 267 RepoModel()._create_filesystem_repo(
265 268 repo_name=repo_name,
266 269 repo_type=repo_type,
267 270 repo_group=RepoModel()._get_repo_group(repo_group),
268 271 clone_uri=source_repo_path,
269 272 )
270 273 repo = Repository.get_by_repo_name(repo_name_full)
271 274 log_create_repository(created_by=owner.username, **repo.get_dict())
272 275
273 276 # update repo commit caches initially
274 277 config = repo._config
275 278 config.set('extensions', 'largefiles', '')
276 279 repo.update_commit_cache(config=config)
277 280
278 281 # set new created state
279 282 repo.set_state(Repository.STATE_CREATED)
280 283
281 284 repo_id = repo.repo_id
282 285 repo_data = repo.get_api_data()
283 286 audit_logger.store(
284 287 'repo.fork', action_data={'data': repo_data},
285 288 user=cur_user,
286 289 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
287 290
288 291 Session().commit()
289 292 except Exception as e:
290 293 log.warning('Exception occurred when forking repository, '
291 294 'doing cleanup...', exc_info=True)
292 295 if isinstance(e, IntegrityError):
293 296 Session().rollback()
294 297
295 298 # rollback things manually !
296 299 repo = Repository.get_by_repo_name(repo_name_full)
297 300 if repo:
298 301 Repository.delete(repo.repo_id)
299 302 Session().commit()
300 303 RepoModel()._delete_filesystem_repo(repo)
301 304 log.info('Cleanup of repo %s finished', repo_name_full)
302 305 raise
303 306
304 307 return True
305 308
306 309
307 310 @async_task(ignore_result=True)
308 311 def repo_maintenance(repoid):
309 312 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
310 313 log = get_logger(repo_maintenance)
311 314 repo = Repository.get_by_id_or_repo_name(repoid)
312 315 if repo:
313 316 maintenance = repo_maintenance_lib.RepoMaintenance()
314 317 tasks = maintenance.get_tasks_for_repo(repo)
315 318 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
316 319 executed_types = maintenance.execute(repo)
317 320 log.debug('Got execution results %s', executed_types)
318 321 else:
319 322 log.debug('Repo `%s` not found or without a clone_url', repoid)
320 323
321 324
322 325 @async_task(ignore_result=True)
323 326 def check_for_update():
324 327 from rhodecode.model.update import UpdateModel
325 328 update_url = UpdateModel().get_update_url()
326 329 cur_ver = rhodecode.__version__
327 330
328 331 try:
329 332 data = UpdateModel().get_update_data(update_url)
330 333 latest = data['versions'][0]
331 334 UpdateModel().store_version(latest['version'])
332 335 except Exception:
333 336 pass
334 337
335 338
336 339 @async_task(ignore_result=False)
337 340 def beat_check(*args, **kwargs):
338 341 log = get_logger(beat_check)
339 342 log.info('Got args: %r and kwargs %r', args, kwargs)
340 343 return time.time()
@@ -1,635 +1,629 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pyramid.threadlocal import get_current_request
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 kw['request'] = get_current_request()
70 70 return self.load(template_name)(**kw)
71 71
72 72
73 73 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
74 74 deform.Form.set_default_renderer(form_renderer)
75 75
76 76
77 77 def LoginForm(localizer):
78 78 _ = localizer
79 79
80 80 class _LoginForm(formencode.Schema):
81 81 allow_extra_fields = True
82 82 filter_extra_fields = True
83 83 username = v.UnicodeString(
84 84 strip=True,
85 85 min=1,
86 86 not_empty=True,
87 87 messages={
88 88 'empty': _(u'Please enter a login'),
89 89 'tooShort': _(u'Enter a value %(min)i characters long or more')
90 90 }
91 91 )
92 92
93 93 password = v.UnicodeString(
94 94 strip=False,
95 95 min=3,
96 96 max=72,
97 97 not_empty=True,
98 98 messages={
99 99 'empty': _(u'Please enter a password'),
100 100 'tooShort': _(u'Enter %(min)i characters or more')}
101 101 )
102 102
103 103 remember = v.StringBoolean(if_missing=False)
104 104
105 105 chained_validators = [v.ValidAuth(localizer)]
106 106 return _LoginForm
107 107
108 108
109 109 def UserForm(localizer, edit=False, available_languages=None, old_data=None):
110 110 old_data = old_data or {}
111 111 available_languages = available_languages or []
112 112 _ = localizer
113 113
114 114 class _UserForm(formencode.Schema):
115 115 allow_extra_fields = True
116 116 filter_extra_fields = True
117 117 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
118 118 v.ValidUsername(localizer, edit, old_data))
119 119 if edit:
120 120 new_password = All(
121 121 v.ValidPassword(localizer),
122 122 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
123 123 )
124 124 password_confirmation = All(
125 125 v.ValidPassword(localizer),
126 126 v.UnicodeString(strip=False, min=6, max=72, not_empty=False),
127 127 )
128 128 admin = v.StringBoolean(if_missing=False)
129 129 else:
130 130 password = All(
131 131 v.ValidPassword(localizer),
132 132 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
133 133 )
134 134 password_confirmation = All(
135 135 v.ValidPassword(localizer),
136 136 v.UnicodeString(strip=False, min=6, max=72, not_empty=False)
137 137 )
138 138
139 139 password_change = v.StringBoolean(if_missing=False)
140 140 create_repo_group = v.StringBoolean(if_missing=False)
141 141
142 142 active = v.StringBoolean(if_missing=False)
143 143 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
144 144 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
145 145 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
146 146 extern_name = v.UnicodeString(strip=True)
147 147 extern_type = v.UnicodeString(strip=True)
148 148 language = v.OneOf(available_languages, hideList=False,
149 149 testValueList=True, if_missing=None)
150 150 chained_validators = [v.ValidPasswordsMatch(localizer)]
151 151 return _UserForm
152 152
153 153
154 154 def UserGroupForm(localizer, edit=False, old_data=None, allow_disabled=False):
155 155 old_data = old_data or {}
156 156 _ = localizer
157 157
158 158 class _UserGroupForm(formencode.Schema):
159 159 allow_extra_fields = True
160 160 filter_extra_fields = True
161 161
162 162 users_group_name = All(
163 163 v.UnicodeString(strip=True, min=1, not_empty=True),
164 164 v.ValidUserGroup(localizer, edit, old_data)
165 165 )
166 166 user_group_description = v.UnicodeString(strip=True, min=1,
167 167 not_empty=False)
168 168
169 169 users_group_active = v.StringBoolean(if_missing=False)
170 170
171 171 if edit:
172 172 # this is user group owner
173 173 user = All(
174 174 v.UnicodeString(not_empty=True),
175 175 v.ValidRepoUser(localizer, allow_disabled))
176 176 return _UserGroupForm
177 177
178 178
179 179 def RepoGroupForm(localizer, edit=False, old_data=None, available_groups=None,
180 180 can_create_in_root=False, allow_disabled=False):
181 181 _ = localizer
182 182 old_data = old_data or {}
183 183 available_groups = available_groups or []
184 184
185 185 class _RepoGroupForm(formencode.Schema):
186 186 allow_extra_fields = True
187 187 filter_extra_fields = False
188 188
189 189 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
190 190 v.SlugifyName(localizer),)
191 191 group_description = v.UnicodeString(strip=True, min=1,
192 192 not_empty=False)
193 193 group_copy_permissions = v.StringBoolean(if_missing=False)
194 194
195 195 group_parent_id = v.OneOf(available_groups, hideList=False,
196 196 testValueList=True, not_empty=True)
197 197 enable_locking = v.StringBoolean(if_missing=False)
198 198 chained_validators = [
199 199 v.ValidRepoGroup(localizer, edit, old_data, can_create_in_root)]
200 200
201 201 if edit:
202 202 # this is repo group owner
203 203 user = All(
204 204 v.UnicodeString(not_empty=True),
205 205 v.ValidRepoUser(localizer, allow_disabled))
206 206 return _RepoGroupForm
207 207
208 208
209 209 def RegisterForm(localizer, edit=False, old_data=None):
210 210 _ = localizer
211 211 old_data = old_data or {}
212 212
213 213 class _RegisterForm(formencode.Schema):
214 214 allow_extra_fields = True
215 215 filter_extra_fields = True
216 216 username = All(
217 217 v.ValidUsername(localizer, edit, old_data),
218 218 v.UnicodeString(strip=True, min=1, not_empty=True)
219 219 )
220 220 password = All(
221 221 v.ValidPassword(localizer),
222 222 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
223 223 )
224 224 password_confirmation = All(
225 225 v.ValidPassword(localizer),
226 226 v.UnicodeString(strip=False, min=6, max=72, not_empty=True)
227 227 )
228 228 active = v.StringBoolean(if_missing=False)
229 229 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
230 230 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
231 231 email = All(v.UniqSystemEmail(localizer, old_data), v.Email(not_empty=True))
232 232
233 233 chained_validators = [v.ValidPasswordsMatch(localizer)]
234 234 return _RegisterForm
235 235
236 236
237 237 def PasswordResetForm(localizer):
238 238 _ = localizer
239 239
240 240 class _PasswordResetForm(formencode.Schema):
241 241 allow_extra_fields = True
242 242 filter_extra_fields = True
243 243 email = All(v.ValidSystemEmail(localizer), v.Email(not_empty=True))
244 244 return _PasswordResetForm
245 245
246 246
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None,
248 landing_revs=None, allow_disabled=False):
247 def RepoForm(localizer, edit=False, old_data=None, repo_groups=None, allow_disabled=False):
249 248 _ = localizer
250 249 old_data = old_data or {}
251 250 repo_groups = repo_groups or []
252 landing_revs = landing_revs or []
253 251 supported_backends = BACKENDS.keys()
254 252
255 253 class _RepoForm(formencode.Schema):
256 254 allow_extra_fields = True
257 255 filter_extra_fields = False
258 256 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
259 257 v.SlugifyName(localizer), v.CannotHaveGitSuffix(localizer))
260 258 repo_group = All(v.CanWriteGroup(localizer, old_data),
261 259 v.OneOf(repo_groups, hideList=True))
262 260 repo_type = v.OneOf(supported_backends, required=False,
263 261 if_missing=old_data.get('repo_type'))
264 262 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
265 263 repo_private = v.StringBoolean(if_missing=False)
266 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
267 264 repo_copy_permissions = v.StringBoolean(if_missing=False)
268 265 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
269 266
270 267 repo_enable_statistics = v.StringBoolean(if_missing=False)
271 268 repo_enable_downloads = v.StringBoolean(if_missing=False)
272 269 repo_enable_locking = v.StringBoolean(if_missing=False)
273 270
274 271 if edit:
275 272 # this is repo owner
276 273 user = All(
277 274 v.UnicodeString(not_empty=True),
278 275 v.ValidRepoUser(localizer, allow_disabled))
279 276 clone_uri_change = v.UnicodeString(
280 277 not_empty=False, if_missing=v.Missing)
281 278
282 279 chained_validators = [v.ValidCloneUri(localizer),
283 280 v.ValidRepoName(localizer, edit, old_data)]
284 281 return _RepoForm
285 282
286 283
287 284 def RepoPermsForm(localizer):
288 285 _ = localizer
289 286
290 287 class _RepoPermsForm(formencode.Schema):
291 288 allow_extra_fields = True
292 289 filter_extra_fields = False
293 290 chained_validators = [v.ValidPerms(localizer, type_='repo')]
294 291 return _RepoPermsForm
295 292
296 293
297 294 def RepoGroupPermsForm(localizer, valid_recursive_choices):
298 295 _ = localizer
299 296
300 297 class _RepoGroupPermsForm(formencode.Schema):
301 298 allow_extra_fields = True
302 299 filter_extra_fields = False
303 300 recursive = v.OneOf(valid_recursive_choices)
304 301 chained_validators = [v.ValidPerms(localizer, type_='repo_group')]
305 302 return _RepoGroupPermsForm
306 303
307 304
308 305 def UserGroupPermsForm(localizer):
309 306 _ = localizer
310 307
311 308 class _UserPermsForm(formencode.Schema):
312 309 allow_extra_fields = True
313 310 filter_extra_fields = False
314 311 chained_validators = [v.ValidPerms(localizer, type_='user_group')]
315 312 return _UserPermsForm
316 313
317 314
318 315 def RepoFieldForm(localizer):
319 316 _ = localizer
320 317
321 318 class _RepoFieldForm(formencode.Schema):
322 319 filter_extra_fields = True
323 320 allow_extra_fields = True
324 321
325 322 new_field_key = All(v.FieldKey(localizer),
326 323 v.UnicodeString(strip=True, min=3, not_empty=True))
327 324 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
328 325 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
329 326 if_missing='str')
330 327 new_field_label = v.UnicodeString(not_empty=False)
331 328 new_field_desc = v.UnicodeString(not_empty=False)
332 329 return _RepoFieldForm
333 330
334 331
335 332 def RepoForkForm(localizer, edit=False, old_data=None,
336 supported_backends=BACKENDS.keys(), repo_groups=None,
337 landing_revs=None):
333 supported_backends=BACKENDS.keys(), repo_groups=None):
338 334 _ = localizer
339 335 old_data = old_data or {}
340 336 repo_groups = repo_groups or []
341 landing_revs = landing_revs or []
342 337
343 338 class _RepoForkForm(formencode.Schema):
344 339 allow_extra_fields = True
345 340 filter_extra_fields = False
346 341 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
347 342 v.SlugifyName(localizer))
348 343 repo_group = All(v.CanWriteGroup(localizer, ),
349 344 v.OneOf(repo_groups, hideList=True))
350 345 repo_type = All(v.ValidForkType(localizer, old_data), v.OneOf(supported_backends))
351 346 description = v.UnicodeString(strip=True, min=1, not_empty=True)
352 347 private = v.StringBoolean(if_missing=False)
353 348 copy_permissions = v.StringBoolean(if_missing=False)
354 349 fork_parent_id = v.UnicodeString()
355 350 chained_validators = [v.ValidForkName(localizer, edit, old_data)]
356 landing_rev = v.OneOf(landing_revs, hideList=True)
357 351 return _RepoForkForm
358 352
359 353
360 354 def ApplicationSettingsForm(localizer):
361 355 _ = localizer
362 356
363 357 class _ApplicationSettingsForm(formencode.Schema):
364 358 allow_extra_fields = True
365 359 filter_extra_fields = False
366 360 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
367 361 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
368 362 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
369 363 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
370 364 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
371 365 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
372 366 rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False)
373 367 rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False)
374 368 return _ApplicationSettingsForm
375 369
376 370
377 371 def ApplicationVisualisationForm(localizer):
378 372 from rhodecode.model.db import Repository
379 373 _ = localizer
380 374
381 375 class _ApplicationVisualisationForm(formencode.Schema):
382 376 allow_extra_fields = True
383 377 filter_extra_fields = False
384 378 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
385 379 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
386 380 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
387 381
388 382 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
389 383 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
390 384 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
391 385 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
392 386 rhodecode_show_version = v.StringBoolean(if_missing=False)
393 387 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
394 388 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
395 389 rhodecode_gravatar_url = v.UnicodeString(min=3)
396 390 rhodecode_clone_uri_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI)
397 391 rhodecode_clone_uri_ssh_tmpl = v.UnicodeString(not_empty=False, if_empty=Repository.DEFAULT_CLONE_URI_SSH)
398 392 rhodecode_support_url = v.UnicodeString()
399 393 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
400 394 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
401 395 return _ApplicationVisualisationForm
402 396
403 397
404 398 class _BaseVcsSettingsForm(formencode.Schema):
405 399
406 400 allow_extra_fields = True
407 401 filter_extra_fields = False
408 402 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
409 403 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
410 404 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
411 405
412 406 # PR/Code-review
413 407 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
414 408 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
415 409
416 410 # hg
417 411 extensions_largefiles = v.StringBoolean(if_missing=False)
418 412 extensions_evolve = v.StringBoolean(if_missing=False)
419 413 phases_publish = v.StringBoolean(if_missing=False)
420 414
421 415 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
422 416 rhodecode_hg_close_branch_before_merging = v.StringBoolean(if_missing=False)
423 417
424 418 # git
425 419 vcs_git_lfs_enabled = v.StringBoolean(if_missing=False)
426 420 rhodecode_git_use_rebase_for_merging = v.StringBoolean(if_missing=False)
427 421 rhodecode_git_close_branch_before_merging = v.StringBoolean(if_missing=False)
428 422
429 423 # svn
430 424 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
431 425 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
432 426
433 427 # cache
434 428 rhodecode_diff_cache = v.StringBoolean(if_missing=False)
435 429
436 430
437 431 def ApplicationUiSettingsForm(localizer):
438 432 _ = localizer
439 433
440 434 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
441 435 web_push_ssl = v.StringBoolean(if_missing=False)
442 436 paths_root_path = All(
443 437 v.ValidPath(localizer),
444 438 v.UnicodeString(strip=True, min=1, not_empty=True)
445 439 )
446 440 largefiles_usercache = All(
447 441 v.ValidPath(localizer),
448 442 v.UnicodeString(strip=True, min=2, not_empty=True))
449 443 vcs_git_lfs_store_location = All(
450 444 v.ValidPath(localizer),
451 445 v.UnicodeString(strip=True, min=2, not_empty=True))
452 446 extensions_hgsubversion = v.StringBoolean(if_missing=False)
453 447 extensions_hggit = v.StringBoolean(if_missing=False)
454 448 new_svn_branch = v.ValidSvnPattern(localizer, section='vcs_svn_branch')
455 449 new_svn_tag = v.ValidSvnPattern(localizer, section='vcs_svn_tag')
456 450 return _ApplicationUiSettingsForm
457 451
458 452
459 453 def RepoVcsSettingsForm(localizer, repo_name):
460 454 _ = localizer
461 455
462 456 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
463 457 inherit_global_settings = v.StringBoolean(if_missing=False)
464 458 new_svn_branch = v.ValidSvnPattern(localizer,
465 459 section='vcs_svn_branch', repo_name=repo_name)
466 460 new_svn_tag = v.ValidSvnPattern(localizer,
467 461 section='vcs_svn_tag', repo_name=repo_name)
468 462 return _RepoVcsSettingsForm
469 463
470 464
471 465 def LabsSettingsForm(localizer):
472 466 _ = localizer
473 467
474 468 class _LabSettingsForm(formencode.Schema):
475 469 allow_extra_fields = True
476 470 filter_extra_fields = False
477 471 return _LabSettingsForm
478 472
479 473
480 474 def ApplicationPermissionsForm(
481 475 localizer, register_choices, password_reset_choices,
482 476 extern_activate_choices):
483 477 _ = localizer
484 478
485 479 class _DefaultPermissionsForm(formencode.Schema):
486 480 allow_extra_fields = True
487 481 filter_extra_fields = True
488 482
489 483 anonymous = v.StringBoolean(if_missing=False)
490 484 default_register = v.OneOf(register_choices)
491 485 default_register_message = v.UnicodeString()
492 486 default_password_reset = v.OneOf(password_reset_choices)
493 487 default_extern_activate = v.OneOf(extern_activate_choices)
494 488 return _DefaultPermissionsForm
495 489
496 490
497 491 def ObjectPermissionsForm(localizer, repo_perms_choices, group_perms_choices,
498 492 user_group_perms_choices):
499 493 _ = localizer
500 494
501 495 class _ObjectPermissionsForm(formencode.Schema):
502 496 allow_extra_fields = True
503 497 filter_extra_fields = True
504 498 overwrite_default_repo = v.StringBoolean(if_missing=False)
505 499 overwrite_default_group = v.StringBoolean(if_missing=False)
506 500 overwrite_default_user_group = v.StringBoolean(if_missing=False)
507 501
508 502 default_repo_perm = v.OneOf(repo_perms_choices)
509 503 default_group_perm = v.OneOf(group_perms_choices)
510 504 default_user_group_perm = v.OneOf(user_group_perms_choices)
511 505
512 506 return _ObjectPermissionsForm
513 507
514 508
515 509 def BranchPermissionsForm(localizer, branch_perms_choices):
516 510 _ = localizer
517 511
518 512 class _BranchPermissionsForm(formencode.Schema):
519 513 allow_extra_fields = True
520 514 filter_extra_fields = True
521 515 overwrite_default_branch = v.StringBoolean(if_missing=False)
522 516 default_branch_perm = v.OneOf(branch_perms_choices)
523 517
524 518 return _BranchPermissionsForm
525 519
526 520
527 521 def UserPermissionsForm(localizer, create_choices, create_on_write_choices,
528 522 repo_group_create_choices, user_group_create_choices,
529 523 fork_choices, inherit_default_permissions_choices):
530 524 _ = localizer
531 525
532 526 class _DefaultPermissionsForm(formencode.Schema):
533 527 allow_extra_fields = True
534 528 filter_extra_fields = True
535 529
536 530 anonymous = v.StringBoolean(if_missing=False)
537 531
538 532 default_repo_create = v.OneOf(create_choices)
539 533 default_repo_create_on_write = v.OneOf(create_on_write_choices)
540 534 default_user_group_create = v.OneOf(user_group_create_choices)
541 535 default_repo_group_create = v.OneOf(repo_group_create_choices)
542 536 default_fork_create = v.OneOf(fork_choices)
543 537 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
544 538 return _DefaultPermissionsForm
545 539
546 540
547 541 def UserIndividualPermissionsForm(localizer):
548 542 _ = localizer
549 543
550 544 class _DefaultPermissionsForm(formencode.Schema):
551 545 allow_extra_fields = True
552 546 filter_extra_fields = True
553 547
554 548 inherit_default_permissions = v.StringBoolean(if_missing=False)
555 549 return _DefaultPermissionsForm
556 550
557 551
558 552 def DefaultsForm(localizer, edit=False, old_data=None, supported_backends=BACKENDS.keys()):
559 553 _ = localizer
560 554 old_data = old_data or {}
561 555
562 556 class _DefaultsForm(formencode.Schema):
563 557 allow_extra_fields = True
564 558 filter_extra_fields = True
565 559 default_repo_type = v.OneOf(supported_backends)
566 560 default_repo_private = v.StringBoolean(if_missing=False)
567 561 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
568 562 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
569 563 default_repo_enable_locking = v.StringBoolean(if_missing=False)
570 564 return _DefaultsForm
571 565
572 566
573 567 def AuthSettingsForm(localizer):
574 568 _ = localizer
575 569
576 570 class _AuthSettingsForm(formencode.Schema):
577 571 allow_extra_fields = True
578 572 filter_extra_fields = True
579 573 auth_plugins = All(v.ValidAuthPlugins(localizer),
580 574 v.UniqueListFromString(localizer)(not_empty=True))
581 575 return _AuthSettingsForm
582 576
583 577
584 578 def UserExtraEmailForm(localizer):
585 579 _ = localizer
586 580
587 581 class _UserExtraEmailForm(formencode.Schema):
588 582 email = All(v.UniqSystemEmail(localizer), v.Email(not_empty=True))
589 583 return _UserExtraEmailForm
590 584
591 585
592 586 def UserExtraIpForm(localizer):
593 587 _ = localizer
594 588
595 589 class _UserExtraIpForm(formencode.Schema):
596 590 ip = v.ValidIp(localizer)(not_empty=True)
597 591 return _UserExtraIpForm
598 592
599 593
600 594 def PullRequestForm(localizer, repo_id):
601 595 _ = localizer
602 596
603 597 class ReviewerForm(formencode.Schema):
604 598 user_id = v.Int(not_empty=True)
605 599 reasons = All()
606 600 rules = All(v.UniqueList(localizer, convert=int)())
607 601 mandatory = v.StringBoolean()
608 602
609 603 class _PullRequestForm(formencode.Schema):
610 604 allow_extra_fields = True
611 605 filter_extra_fields = True
612 606
613 607 common_ancestor = v.UnicodeString(strip=True, required=True)
614 608 source_repo = v.UnicodeString(strip=True, required=True)
615 609 source_ref = v.UnicodeString(strip=True, required=True)
616 610 target_repo = v.UnicodeString(strip=True, required=True)
617 611 target_ref = v.UnicodeString(strip=True, required=True)
618 612 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
619 613 v.UniqueList(localizer)(not_empty=True))
620 614 review_members = formencode.ForEach(ReviewerForm())
621 615 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
622 616 pullrequest_desc = v.UnicodeString(strip=True, required=False)
623 617 description_renderer = v.UnicodeString(strip=True, required=False)
624 618
625 619 return _PullRequestForm
626 620
627 621
628 622 def IssueTrackerPatternsForm(localizer):
629 623 _ = localizer
630 624
631 625 class _IssueTrackerPatternsForm(formencode.Schema):
632 626 allow_extra_fields = True
633 627 filter_extra_fields = False
634 628 chained_validators = [v.ValidPattern(localizer)]
635 629 return _IssueTrackerPatternsForm
@@ -1,942 +1,972 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Scm model for RhodeCode
23 23 """
24 24
25 25 import os.path
26 26 import traceback
27 27 import logging
28 28 import cStringIO
29 29
30 30 from sqlalchemy import func
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 import rhodecode
34 34 from rhodecode.lib.vcs import get_backend
35 35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib import helpers as h, rc_cache
39 39 from rhodecode.lib.auth import (
40 40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
41 41 HasUserGroupPermissionAny)
42 42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
43 43 from rhodecode.lib import hooks_utils
44 44 from rhodecode.lib.utils import (
45 45 get_filesystem_repos, make_db_config)
46 46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
47 47 from rhodecode.lib.system_info import get_system_info
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.db import (
50 50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 51 PullRequest)
52 52 from rhodecode.model.settings import VcsSettingsModel
53 53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class SimpleCachedRepoList(object):
75 75 """
76 76 Lighter version of of iteration of repos without the scm initialisation,
77 77 and with cache usage
78 78 """
79 79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 80 self.db_repo_list = db_repo_list
81 81 self.repos_path = repos_path
82 82 self.order_by = order_by
83 83 self.reversed = (order_by or '').startswith('-')
84 84 if not perm_set:
85 85 perm_set = ['repository.read', 'repository.write',
86 86 'repository.admin']
87 87 self.perm_set = perm_set
88 88
89 89 def __len__(self):
90 90 return len(self.db_repo_list)
91 91
92 92 def __repr__(self):
93 93 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
94 94
95 95 def __iter__(self):
96 96 for dbr in self.db_repo_list:
97 97 # check permission at this level
98 98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 99 dbr.repo_name, 'SimpleCachedRepoList check')
100 100 if not has_perm:
101 101 continue
102 102
103 103 tmp_d = {
104 104 'name': dbr.repo_name,
105 105 'dbrepo': dbr.get_dict(),
106 106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 107 }
108 108 yield tmp_d
109 109
110 110
111 111 class _PermCheckIterator(object):
112 112
113 113 def __init__(
114 114 self, obj_list, obj_attr, perm_set, perm_checker,
115 115 extra_kwargs=None):
116 116 """
117 117 Creates iterator from given list of objects, additionally
118 118 checking permission for them from perm_set var
119 119
120 120 :param obj_list: list of db objects
121 121 :param obj_attr: attribute of object to pass into perm_checker
122 122 :param perm_set: list of permissions to check
123 123 :param perm_checker: callable to check permissions against
124 124 """
125 125 self.obj_list = obj_list
126 126 self.obj_attr = obj_attr
127 127 self.perm_set = perm_set
128 128 self.perm_checker = perm_checker
129 129 self.extra_kwargs = extra_kwargs or {}
130 130
131 131 def __len__(self):
132 132 return len(self.obj_list)
133 133
134 134 def __repr__(self):
135 135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 checker = self.perm_checker(*self.perm_set)
139 139 for db_obj in self.obj_list:
140 140 # check permission at this level
141 141 name = getattr(db_obj, self.obj_attr, None)
142 142 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
143 143 continue
144 144
145 145 yield db_obj
146 146
147 147
148 148 class RepoList(_PermCheckIterator):
149 149
150 150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 151 if not perm_set:
152 152 perm_set = [
153 153 'repository.read', 'repository.write', 'repository.admin']
154 154
155 155 super(RepoList, self).__init__(
156 156 obj_list=db_repo_list,
157 157 obj_attr='repo_name', perm_set=perm_set,
158 158 perm_checker=HasRepoPermissionAny,
159 159 extra_kwargs=extra_kwargs)
160 160
161 161
162 162 class RepoGroupList(_PermCheckIterator):
163 163
164 164 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
165 165 if not perm_set:
166 166 perm_set = ['group.read', 'group.write', 'group.admin']
167 167
168 168 super(RepoGroupList, self).__init__(
169 169 obj_list=db_repo_group_list,
170 170 obj_attr='group_name', perm_set=perm_set,
171 171 perm_checker=HasRepoGroupPermissionAny,
172 172 extra_kwargs=extra_kwargs)
173 173
174 174
175 175 class UserGroupList(_PermCheckIterator):
176 176
177 177 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
178 178 if not perm_set:
179 179 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
180 180
181 181 super(UserGroupList, self).__init__(
182 182 obj_list=db_user_group_list,
183 183 obj_attr='users_group_name', perm_set=perm_set,
184 184 perm_checker=HasUserGroupPermissionAny,
185 185 extra_kwargs=extra_kwargs)
186 186
187 187
188 188 class ScmModel(BaseModel):
189 189 """
190 190 Generic Scm Model
191 191 """
192 192
193 193 @LazyProperty
194 194 def repos_path(self):
195 195 """
196 196 Gets the repositories root path from database
197 197 """
198 198
199 199 settings_model = VcsSettingsModel(sa=self.sa)
200 200 return settings_model.get_repos_location()
201 201
202 202 def repo_scan(self, repos_path=None):
203 203 """
204 204 Listing of repositories in given path. This path should not be a
205 205 repository itself. Return a dictionary of repository objects
206 206
207 207 :param repos_path: path to directory containing repositories
208 208 """
209 209
210 210 if repos_path is None:
211 211 repos_path = self.repos_path
212 212
213 213 log.info('scanning for repositories in %s', repos_path)
214 214
215 215 config = make_db_config()
216 216 config.set('extensions', 'largefiles', '')
217 217 repos = {}
218 218
219 219 for name, path in get_filesystem_repos(repos_path, recursive=True):
220 220 # name need to be decomposed and put back together using the /
221 221 # since this is internal storage separator for rhodecode
222 222 name = Repository.normalize_repo_name(name)
223 223
224 224 try:
225 225 if name in repos:
226 226 raise RepositoryError('Duplicate repository name %s '
227 227 'found in %s' % (name, path))
228 228 elif path[0] in rhodecode.BACKENDS:
229 229 backend = get_backend(path[0])
230 230 repos[name] = backend(path[1], config=config,
231 231 with_wire={"cache": False})
232 232 except OSError:
233 233 continue
234 234 log.debug('found %s paths with repositories', len(repos))
235 235 return repos
236 236
237 237 def get_repos(self, all_repos=None, sort_key=None):
238 238 """
239 239 Get all repositories from db and for each repo create it's
240 240 backend instance and fill that backed with information from database
241 241
242 242 :param all_repos: list of repository names as strings
243 243 give specific repositories list, good for filtering
244 244
245 245 :param sort_key: initial sorting of repositories
246 246 """
247 247 if all_repos is None:
248 248 all_repos = self.sa.query(Repository)\
249 249 .filter(Repository.group_id == None)\
250 250 .order_by(func.lower(Repository.repo_name)).all()
251 251 repo_iter = SimpleCachedRepoList(
252 252 all_repos, repos_path=self.repos_path, order_by=sort_key)
253 253 return repo_iter
254 254
255 255 def get_repo_groups(self, all_groups=None):
256 256 if all_groups is None:
257 257 all_groups = RepoGroup.query()\
258 258 .filter(RepoGroup.group_parent_id == None).all()
259 259 return [x for x in RepoGroupList(all_groups)]
260 260
261 261 def mark_for_invalidation(self, repo_name, delete=False):
262 262 """
263 263 Mark caches of this repo invalid in the database. `delete` flag
264 264 removes the cache entries
265 265
266 266 :param repo_name: the repo_name for which caches should be marked
267 267 invalid, or deleted
268 268 :param delete: delete the entry keys instead of setting bool
269 269 flag on them, and also purge caches used by the dogpile
270 270 """
271 271 repo = Repository.get_by_repo_name(repo_name)
272 272
273 273 if repo:
274 274 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
275 275 repo_id=repo.repo_id)
276 276 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
277 277
278 278 repo_id = repo.repo_id
279 279 config = repo._config
280 280 config.set('extensions', 'largefiles', '')
281 281 repo.update_commit_cache(config=config, cs_cache=None)
282 282 if delete:
283 283 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
284 284 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
285 285
286 286 def toggle_following_repo(self, follow_repo_id, user_id):
287 287
288 288 f = self.sa.query(UserFollowing)\
289 289 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
290 290 .filter(UserFollowing.user_id == user_id).scalar()
291 291
292 292 if f is not None:
293 293 try:
294 294 self.sa.delete(f)
295 295 return
296 296 except Exception:
297 297 log.error(traceback.format_exc())
298 298 raise
299 299
300 300 try:
301 301 f = UserFollowing()
302 302 f.user_id = user_id
303 303 f.follows_repo_id = follow_repo_id
304 304 self.sa.add(f)
305 305 except Exception:
306 306 log.error(traceback.format_exc())
307 307 raise
308 308
309 309 def toggle_following_user(self, follow_user_id, user_id):
310 310 f = self.sa.query(UserFollowing)\
311 311 .filter(UserFollowing.follows_user_id == follow_user_id)\
312 312 .filter(UserFollowing.user_id == user_id).scalar()
313 313
314 314 if f is not None:
315 315 try:
316 316 self.sa.delete(f)
317 317 return
318 318 except Exception:
319 319 log.error(traceback.format_exc())
320 320 raise
321 321
322 322 try:
323 323 f = UserFollowing()
324 324 f.user_id = user_id
325 325 f.follows_user_id = follow_user_id
326 326 self.sa.add(f)
327 327 except Exception:
328 328 log.error(traceback.format_exc())
329 329 raise
330 330
331 331 def is_following_repo(self, repo_name, user_id, cache=False):
332 332 r = self.sa.query(Repository)\
333 333 .filter(Repository.repo_name == repo_name).scalar()
334 334
335 335 f = self.sa.query(UserFollowing)\
336 336 .filter(UserFollowing.follows_repository == r)\
337 337 .filter(UserFollowing.user_id == user_id).scalar()
338 338
339 339 return f is not None
340 340
341 341 def is_following_user(self, username, user_id, cache=False):
342 342 u = User.get_by_username(username)
343 343
344 344 f = self.sa.query(UserFollowing)\
345 345 .filter(UserFollowing.follows_user == u)\
346 346 .filter(UserFollowing.user_id == user_id).scalar()
347 347
348 348 return f is not None
349 349
350 350 def get_followers(self, repo):
351 351 repo = self._get_repo(repo)
352 352
353 353 return self.sa.query(UserFollowing)\
354 354 .filter(UserFollowing.follows_repository == repo).count()
355 355
356 356 def get_forks(self, repo):
357 357 repo = self._get_repo(repo)
358 358 return self.sa.query(Repository)\
359 359 .filter(Repository.fork == repo).count()
360 360
361 361 def get_pull_requests(self, repo):
362 362 repo = self._get_repo(repo)
363 363 return self.sa.query(PullRequest)\
364 364 .filter(PullRequest.target_repo == repo)\
365 365 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
366 366
367 367 def mark_as_fork(self, repo, fork, user):
368 368 repo = self._get_repo(repo)
369 369 fork = self._get_repo(fork)
370 370 if fork and repo.repo_id == fork.repo_id:
371 371 raise Exception("Cannot set repository as fork of itself")
372 372
373 373 if fork and repo.repo_type != fork.repo_type:
374 374 raise RepositoryError(
375 375 "Cannot set repository as fork of repository with other type")
376 376
377 377 repo.fork = fork
378 378 self.sa.add(repo)
379 379 return repo
380 380
381 381 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
382 382 dbrepo = self._get_repo(repo)
383 383 remote_uri = remote_uri or dbrepo.clone_uri
384 384 if not remote_uri:
385 385 raise Exception("This repository doesn't have a clone uri")
386 386
387 387 repo = dbrepo.scm_instance(cache=False)
388 388 repo.config.clear_section('hooks')
389 389
390 390 try:
391 391 # NOTE(marcink): add extra validation so we skip invalid urls
392 392 # this is due this tasks can be executed via scheduler without
393 393 # proper validation of remote_uri
394 394 if validate_uri:
395 395 config = make_db_config(clear_session=False)
396 396 url_validator(remote_uri, dbrepo.repo_type, config)
397 397 except InvalidCloneUrl:
398 398 raise
399 399
400 400 repo_name = dbrepo.repo_name
401 401 try:
402 402 # TODO: we need to make sure those operations call proper hooks !
403 403 repo.fetch(remote_uri)
404 404
405 405 self.mark_for_invalidation(repo_name)
406 406 except Exception:
407 407 log.error(traceback.format_exc())
408 408 raise
409 409
410 410 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
411 411 dbrepo = self._get_repo(repo)
412 412 remote_uri = remote_uri or dbrepo.push_uri
413 413 if not remote_uri:
414 414 raise Exception("This repository doesn't have a clone uri")
415 415
416 416 repo = dbrepo.scm_instance(cache=False)
417 417 repo.config.clear_section('hooks')
418 418
419 419 try:
420 420 # NOTE(marcink): add extra validation so we skip invalid urls
421 421 # this is due this tasks can be executed via scheduler without
422 422 # proper validation of remote_uri
423 423 if validate_uri:
424 424 config = make_db_config(clear_session=False)
425 425 url_validator(remote_uri, dbrepo.repo_type, config)
426 426 except InvalidCloneUrl:
427 427 raise
428 428
429 429 try:
430 430 repo.push(remote_uri)
431 431 except Exception:
432 432 log.error(traceback.format_exc())
433 433 raise
434 434
435 435 def commit_change(self, repo, repo_name, commit, user, author, message,
436 436 content, f_path):
437 437 """
438 438 Commits changes
439 439
440 440 :param repo: SCM instance
441 441
442 442 """
443 443 user = self._get_user(user)
444 444
445 445 # decoding here will force that we have proper encoded values
446 446 # in any other case this will throw exceptions and deny commit
447 447 content = safe_str(content)
448 448 path = safe_str(f_path)
449 449 # message and author needs to be unicode
450 450 # proper backend should then translate that into required type
451 451 message = safe_unicode(message)
452 452 author = safe_unicode(author)
453 453 imc = repo.in_memory_commit
454 454 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
455 455 try:
456 456 # TODO: handle pre-push action !
457 457 tip = imc.commit(
458 458 message=message, author=author, parents=[commit],
459 459 branch=commit.branch)
460 460 except Exception as e:
461 461 log.error(traceback.format_exc())
462 462 raise IMCCommitError(str(e))
463 463 finally:
464 464 # always clear caches, if commit fails we want fresh object also
465 465 self.mark_for_invalidation(repo_name)
466 466
467 467 # We trigger the post-push action
468 468 hooks_utils.trigger_post_push_hook(
469 469 username=user.username, action='push_local', hook_type='post_push',
470 470 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
471 471 return tip
472 472
473 473 def _sanitize_path(self, f_path):
474 474 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
475 475 raise NonRelativePathError('%s is not an relative path' % f_path)
476 476 if f_path:
477 477 f_path = os.path.normpath(f_path)
478 478 return f_path
479 479
480 480 def get_dirnode_metadata(self, request, commit, dir_node):
481 481 if not dir_node.is_dir():
482 482 return []
483 483
484 484 data = []
485 485 for node in dir_node:
486 486 if not node.is_file():
487 487 # we skip file-nodes
488 488 continue
489 489
490 490 last_commit = node.last_commit
491 491 last_commit_date = last_commit.date
492 492 data.append({
493 493 'name': node.name,
494 494 'size': h.format_byte_size_binary(node.size),
495 495 'modified_at': h.format_date(last_commit_date),
496 496 'modified_ts': last_commit_date.isoformat(),
497 497 'revision': last_commit.revision,
498 498 'short_id': last_commit.short_id,
499 499 'message': h.escape(last_commit.message),
500 500 'author': h.escape(last_commit.author),
501 501 'user_profile': h.gravatar_with_user(
502 502 request, last_commit.author),
503 503 })
504 504
505 505 return data
506 506
507 507 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
508 508 extended_info=False, content=False, max_file_bytes=None):
509 509 """
510 510 recursive walk in root dir and return a set of all path in that dir
511 511 based on repository walk function
512 512
513 513 :param repo_name: name of repository
514 514 :param commit_id: commit id for which to list nodes
515 515 :param root_path: root path to list
516 516 :param flat: return as a list, if False returns a dict with description
517 517 :param extended_info: show additional info such as md5, binary, size etc
518 518 :param content: add nodes content to the return data
519 519 :param max_file_bytes: will not return file contents over this limit
520 520
521 521 """
522 522 _files = list()
523 523 _dirs = list()
524 524 try:
525 525 _repo = self._get_repo(repo_name)
526 526 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
527 527 root_path = root_path.lstrip('/')
528 528 for __, dirs, files in commit.walk(root_path):
529 529
530 530 for f in files:
531 531 _content = None
532 532 _data = f_name = f.unicode_path
533 533
534 534 if not flat:
535 535 _data = {
536 536 "name": h.escape(f_name),
537 537 "type": "file",
538 538 }
539 539 if extended_info:
540 540 _data.update({
541 541 "md5": f.md5,
542 542 "binary": f.is_binary,
543 543 "size": f.size,
544 544 "extension": f.extension,
545 545 "mimetype": f.mimetype,
546 546 "lines": f.lines()[0]
547 547 })
548 548
549 549 if content:
550 550 over_size_limit = (max_file_bytes is not None
551 551 and f.size > max_file_bytes)
552 552 full_content = None
553 553 if not f.is_binary and not over_size_limit:
554 554 full_content = safe_str(f.content)
555 555
556 556 _data.update({
557 557 "content": full_content,
558 558 })
559 559 _files.append(_data)
560 560
561 561 for d in dirs:
562 562 _data = d_name = d.unicode_path
563 563 if not flat:
564 564 _data = {
565 565 "name": h.escape(d_name),
566 566 "type": "dir",
567 567 }
568 568 if extended_info:
569 569 _data.update({
570 570 "md5": None,
571 571 "binary": None,
572 572 "size": None,
573 573 "extension": None,
574 574 })
575 575 if content:
576 576 _data.update({
577 577 "content": None
578 578 })
579 579 _dirs.append(_data)
580 580 except RepositoryError:
581 581 log.exception("Exception in get_nodes")
582 582 raise
583 583
584 584 return _dirs, _files
585 585
586 586 def get_node(self, repo_name, commit_id, file_path,
587 587 extended_info=False, content=False, max_file_bytes=None, cache=True):
588 588 """
589 589 retrieve single node from commit
590 590 """
591 591 try:
592 592
593 593 _repo = self._get_repo(repo_name)
594 594 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
595 595
596 596 file_node = commit.get_node(file_path)
597 597 if file_node.is_dir():
598 598 raise RepositoryError('The given path is a directory')
599 599
600 600 _content = None
601 601 f_name = file_node.unicode_path
602 602
603 603 file_data = {
604 604 "name": h.escape(f_name),
605 605 "type": "file",
606 606 }
607 607
608 608 if extended_info:
609 609 file_data.update({
610 610 "extension": file_node.extension,
611 611 "mimetype": file_node.mimetype,
612 612 })
613 613
614 614 if cache:
615 615 md5 = file_node.md5
616 616 is_binary = file_node.is_binary
617 617 size = file_node.size
618 618 else:
619 619 is_binary, md5, size, _content = file_node.metadata_uncached()
620 620
621 621 file_data.update({
622 622 "md5": md5,
623 623 "binary": is_binary,
624 624 "size": size,
625 625 })
626 626
627 627 if content and cache:
628 628 # get content + cache
629 629 size = file_node.size
630 630 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
631 631 full_content = None
632 632 if not file_node.is_binary and not over_size_limit:
633 633 full_content = safe_unicode(file_node.content)
634 634
635 635 file_data.update({
636 636 "content": full_content,
637 637 })
638 638 elif content:
639 639 # get content *without* cache
640 640 if _content is None:
641 641 is_binary, md5, size, _content = file_node.metadata_uncached()
642 642
643 643 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
644 644 full_content = None
645 645 if not is_binary and not over_size_limit:
646 646 full_content = safe_unicode(_content)
647 647
648 648 file_data.update({
649 649 "content": full_content,
650 650 })
651 651
652 652 except RepositoryError:
653 653 log.exception("Exception in get_node")
654 654 raise
655 655
656 656 return file_data
657 657
658 658 def get_fts_data(self, repo_name, commit_id, root_path='/'):
659 659 """
660 660 Fetch node tree for usage in full text search
661 661 """
662 662
663 663 tree_info = list()
664 664
665 665 try:
666 666 _repo = self._get_repo(repo_name)
667 667 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
668 668 root_path = root_path.lstrip('/')
669 669 for __, dirs, files in commit.walk(root_path):
670 670
671 671 for f in files:
672 672 is_binary, md5, size, _content = f.metadata_uncached()
673 673 _data = {
674 674 "name": f.unicode_path,
675 675 "md5": md5,
676 676 "extension": f.extension,
677 677 "binary": is_binary,
678 678 "size": size
679 679 }
680 680
681 681 tree_info.append(_data)
682 682
683 683 except RepositoryError:
684 684 log.exception("Exception in get_nodes")
685 685 raise
686 686
687 687 return tree_info
688 688
689 689 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
690 690 author=None, trigger_push_hook=True):
691 691 """
692 692 Commits given multiple nodes into repo
693 693
694 694 :param user: RhodeCode User object or user_id, the commiter
695 695 :param repo: RhodeCode Repository object
696 696 :param message: commit message
697 697 :param nodes: mapping {filename:{'content':content},...}
698 698 :param parent_commit: parent commit, can be empty than it's
699 699 initial commit
700 700 :param author: author of commit, cna be different that commiter
701 701 only for git
702 702 :param trigger_push_hook: trigger push hooks
703 703
704 704 :returns: new commited commit
705 705 """
706 706
707 707 user = self._get_user(user)
708 708 scm_instance = repo.scm_instance(cache=False)
709 709
710 710 processed_nodes = []
711 711 for f_path in nodes:
712 712 f_path = self._sanitize_path(f_path)
713 713 content = nodes[f_path]['content']
714 714 f_path = safe_str(f_path)
715 715 # decoding here will force that we have proper encoded values
716 716 # in any other case this will throw exceptions and deny commit
717 717 if isinstance(content, (basestring,)):
718 718 content = safe_str(content)
719 719 elif isinstance(content, (file, cStringIO.OutputType,)):
720 720 content = content.read()
721 721 else:
722 722 raise Exception('Content is of unrecognized type %s' % (
723 723 type(content)
724 724 ))
725 725 processed_nodes.append((f_path, content))
726 726
727 727 message = safe_unicode(message)
728 728 commiter = user.full_contact
729 729 author = safe_unicode(author) if author else commiter
730 730
731 731 imc = scm_instance.in_memory_commit
732 732
733 733 if not parent_commit:
734 734 parent_commit = EmptyCommit(alias=scm_instance.alias)
735 735
736 736 if isinstance(parent_commit, EmptyCommit):
737 737 # EmptyCommit means we we're editing empty repository
738 738 parents = None
739 739 else:
740 740 parents = [parent_commit]
741 741 # add multiple nodes
742 742 for path, content in processed_nodes:
743 743 imc.add(FileNode(path, content=content))
744 744 # TODO: handle pre push scenario
745 745 tip = imc.commit(message=message,
746 746 author=author,
747 747 parents=parents,
748 748 branch=parent_commit.branch)
749 749
750 750 self.mark_for_invalidation(repo.repo_name)
751 751 if trigger_push_hook:
752 752 hooks_utils.trigger_post_push_hook(
753 753 username=user.username, action='push_local',
754 754 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
755 755 hook_type='post_push',
756 756 commit_ids=[tip.raw_id])
757 757 return tip
758 758
759 759 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
760 760 author=None, trigger_push_hook=True):
761 761 user = self._get_user(user)
762 762 scm_instance = repo.scm_instance(cache=False)
763 763
764 764 message = safe_unicode(message)
765 765 commiter = user.full_contact
766 766 author = safe_unicode(author) if author else commiter
767 767
768 768 imc = scm_instance.in_memory_commit
769 769
770 770 if not parent_commit:
771 771 parent_commit = EmptyCommit(alias=scm_instance.alias)
772 772
773 773 if isinstance(parent_commit, EmptyCommit):
774 774 # EmptyCommit means we we're editing empty repository
775 775 parents = None
776 776 else:
777 777 parents = [parent_commit]
778 778
779 779 # add multiple nodes
780 780 for _filename, data in nodes.items():
781 781 # new filename, can be renamed from the old one, also sanitaze
782 782 # the path for any hack around relative paths like ../../ etc.
783 783 filename = self._sanitize_path(data['filename'])
784 784 old_filename = self._sanitize_path(_filename)
785 785 content = data['content']
786 786 file_mode = data.get('mode')
787 787 filenode = FileNode(old_filename, content=content, mode=file_mode)
788 788 op = data['op']
789 789 if op == 'add':
790 790 imc.add(filenode)
791 791 elif op == 'del':
792 792 imc.remove(filenode)
793 793 elif op == 'mod':
794 794 if filename != old_filename:
795 795 # TODO: handle renames more efficient, needs vcs lib changes
796 796 imc.remove(filenode)
797 797 imc.add(FileNode(filename, content=content, mode=file_mode))
798 798 else:
799 799 imc.change(filenode)
800 800
801 801 try:
802 802 # TODO: handle pre push scenario commit changes
803 803 tip = imc.commit(message=message,
804 804 author=author,
805 805 parents=parents,
806 806 branch=parent_commit.branch)
807 807 except NodeNotChangedError:
808 808 raise
809 809 except Exception as e:
810 810 log.exception("Unexpected exception during call to imc.commit")
811 811 raise IMCCommitError(str(e))
812 812 finally:
813 813 # always clear caches, if commit fails we want fresh object also
814 814 self.mark_for_invalidation(repo.repo_name)
815 815
816 816 if trigger_push_hook:
817 817 hooks_utils.trigger_post_push_hook(
818 818 username=user.username, action='push_local', hook_type='post_push',
819 819 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
820 820 commit_ids=[tip.raw_id])
821 821
822 822 return tip
823 823
824 824 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
825 825 author=None, trigger_push_hook=True):
826 826 """
827 827 Deletes given multiple nodes into `repo`
828 828
829 829 :param user: RhodeCode User object or user_id, the committer
830 830 :param repo: RhodeCode Repository object
831 831 :param message: commit message
832 832 :param nodes: mapping {filename:{'content':content},...}
833 833 :param parent_commit: parent commit, can be empty than it's initial
834 834 commit
835 835 :param author: author of commit, cna be different that commiter only
836 836 for git
837 837 :param trigger_push_hook: trigger push hooks
838 838
839 839 :returns: new commit after deletion
840 840 """
841 841
842 842 user = self._get_user(user)
843 843 scm_instance = repo.scm_instance(cache=False)
844 844
845 845 processed_nodes = []
846 846 for f_path in nodes:
847 847 f_path = self._sanitize_path(f_path)
848 848 # content can be empty but for compatabilty it allows same dicts
849 849 # structure as add_nodes
850 850 content = nodes[f_path].get('content')
851 851 processed_nodes.append((f_path, content))
852 852
853 853 message = safe_unicode(message)
854 854 commiter = user.full_contact
855 855 author = safe_unicode(author) if author else commiter
856 856
857 857 imc = scm_instance.in_memory_commit
858 858
859 859 if not parent_commit:
860 860 parent_commit = EmptyCommit(alias=scm_instance.alias)
861 861
862 862 if isinstance(parent_commit, EmptyCommit):
863 863 # EmptyCommit means we we're editing empty repository
864 864 parents = None
865 865 else:
866 866 parents = [parent_commit]
867 867 # add multiple nodes
868 868 for path, content in processed_nodes:
869 869 imc.remove(FileNode(path, content=content))
870 870
871 871 # TODO: handle pre push scenario
872 872 tip = imc.commit(message=message,
873 873 author=author,
874 874 parents=parents,
875 875 branch=parent_commit.branch)
876 876
877 877 self.mark_for_invalidation(repo.repo_name)
878 878 if trigger_push_hook:
879 879 hooks_utils.trigger_post_push_hook(
880 880 username=user.username, action='push_local', hook_type='post_push',
881 881 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
882 882 commit_ids=[tip.raw_id])
883 883 return tip
884 884
885 885 def strip(self, repo, commit_id, branch):
886 886 scm_instance = repo.scm_instance(cache=False)
887 887 scm_instance.config.clear_section('hooks')
888 888 scm_instance.strip(commit_id, branch)
889 889 self.mark_for_invalidation(repo.repo_name)
890 890
891 891 def get_unread_journal(self):
892 892 return self.sa.query(UserLog).count()
893 893
894 @classmethod
895 def backend_landing_ref(cls, repo_type):
896 """
897 Return a default landing ref based on a repository type.
898 """
899
900 landing_ref = {
901 'hg': ('branch:default', 'default'),
902 'git': ('branch:master', 'master'),
903 'svn': ('rev:tip', 'latest tip'),
904 'default': ('rev:tip', 'latest tip'),
905 }
906
907 return landing_ref.get(repo_type) or landing_ref['default']
908
894 909 def get_repo_landing_revs(self, translator, repo=None):
895 910 """
896 911 Generates select option with tags branches and bookmarks (for hg only)
897 912 grouped by type
898 913
899 914 :param repo:
900 915 """
901 916 _ = translator
902 917 repo = self._get_repo(repo)
903 918
904 hist_l = [
905 ['rev:tip', _('latest tip')]
919 if repo:
920 repo_type = repo.repo_type
921 else:
922 repo_type = 'default'
923
924 default_landing_ref, landing_ref_lbl = self.backend_landing_ref(repo_type)
925
926 default_ref_options = [
927 [default_landing_ref, landing_ref_lbl]
906 928 ]
907 choices = [
908 'rev:tip'
929 default_choices = [
930 default_landing_ref
909 931 ]
910 932
911 933 if not repo:
912 return choices, hist_l
934 return default_choices, default_ref_options
913 935
914 936 repo = repo.scm_instance()
915 937
916 branches_group = (
917 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
918 for b in repo.branches],
919 _("Branches"))
920 hist_l.append(branches_group)
938 ref_options = [('rev:tip', 'latest tip')]
939 choices = ['rev:tip']
940
941 # branches
942 branch_group = [(u'branch:%s' % safe_unicode(b), safe_unicode(b)) for b in repo.branches]
943 if not branch_group:
944 # new repo, or without maybe a branch?
945 branch_group = default_ref_options
946
947 branches_group = (branch_group, _("Branches"))
948 ref_options.append(branches_group)
921 949 choices.extend([x[0] for x in branches_group[0]])
922 950
951 # bookmarks for HG
923 952 if repo.alias == 'hg':
924 953 bookmarks_group = (
925 954 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
926 955 for b in repo.bookmarks],
927 956 _("Bookmarks"))
928 hist_l.append(bookmarks_group)
957 ref_options.append(bookmarks_group)
929 958 choices.extend([x[0] for x in bookmarks_group[0]])
930 959
960 # tags
931 961 tags_group = (
932 962 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
933 963 for t in repo.tags],
934 964 _("Tags"))
935 hist_l.append(tags_group)
965 ref_options.append(tags_group)
936 966 choices.extend([x[0] for x in tags_group[0]])
937 967
938 return choices, hist_l
968 return choices, ref_options
939 969
940 970 def get_server_info(self, environ=None):
941 971 server_info = get_system_info(environ)
942 972 return server_info
@@ -1,430 +1,441 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 colander
22 22 import deform.widget
23 23
24 24 from rhodecode.translation import _
25 25 from rhodecode.model.validation_schema.utils import convert_to_optgroup
26 26 from rhodecode.model.validation_schema import validators, preparers, types
27 27
28 28 DEFAULT_LANDING_REF = 'rev:tip'
29 DEFAULT_BACKEND_LANDING_REF = {
30 'hg': 'branch:default',
31 'git': 'branch:master',
32 'svn': 'rev:tip',
33 }
29 34
30 35
31 36 def get_group_and_repo(repo_name):
32 37 from rhodecode.model.repo_group import RepoGroupModel
33 38 return RepoGroupModel()._get_group_name_and_parent(
34 39 repo_name, get_object=True)
35 40
36 41
37 42 def get_repo_group(repo_group_id):
38 43 from rhodecode.model.repo_group import RepoGroup
39 44 return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR
40 45
41 46
42 47 @colander.deferred
43 48 def deferred_repo_type_validator(node, kw):
44 49 options = kw.get('repo_type_options', [])
45 50 return colander.OneOf([x for x in options])
46 51
47 52
48 53 @colander.deferred
49 54 def deferred_repo_owner_validator(node, kw):
50 55
51 56 def repo_owner_validator(node, value):
52 57 from rhodecode.model.db import User
53 58 existing = User.get_by_username(value)
54 59 if not existing:
55 60 msg = _(u'Repo owner with id `{}` does not exists').format(value)
56 61 raise colander.Invalid(node, msg)
57 62
58 63 return repo_owner_validator
59 64
60 65
61 66 @colander.deferred
62 67 def deferred_landing_ref_validator(node, kw):
63 68 options = kw.get(
64 69 'repo_ref_options', [DEFAULT_LANDING_REF])
65 70 return colander.OneOf([x for x in options])
66 71
67 72
68 73 @colander.deferred
69 74 def deferred_sync_uri_validator(node, kw):
70 75 repo_type = kw.get('repo_type')
71 76 validator = validators.CloneUriValidator(repo_type)
72 77 return validator
73 78
74 79
75 80 @colander.deferred
76 81 def deferred_landing_ref_widget(node, kw):
77 items = kw.get(
78 'repo_ref_items', [(DEFAULT_LANDING_REF, DEFAULT_LANDING_REF)])
82 repo_type = kw.get('repo_type')
83 default_opts = []
84 if repo_type:
85 default_opts.append(
86 (DEFAULT_BACKEND_LANDING_REF[repo_type],
87 DEFAULT_BACKEND_LANDING_REF[repo_type]))
88
89 items = kw.get('repo_ref_items', default_opts)
79 90 items = convert_to_optgroup(items)
80 91 return deform.widget.Select2Widget(values=items)
81 92
82 93
83 94 @colander.deferred
84 95 def deferred_fork_of_validator(node, kw):
85 96 old_values = kw.get('old_values') or {}
86 97
87 98 def fork_of_validator(node, value):
88 99 from rhodecode.model.db import Repository, RepoGroup
89 100 existing = Repository.get_by_repo_name(value)
90 101 if not existing:
91 102 msg = _(u'Fork with id `{}` does not exists').format(value)
92 103 raise colander.Invalid(node, msg)
93 104 elif old_values['repo_name'] == existing.repo_name:
94 105 msg = _(u'Cannot set fork of '
95 106 u'parameter of this repository to itself').format(value)
96 107 raise colander.Invalid(node, msg)
97 108
98 109 return fork_of_validator
99 110
100 111
101 112 @colander.deferred
102 113 def deferred_can_write_to_group_validator(node, kw):
103 114 request_user = kw.get('user')
104 115 old_values = kw.get('old_values') or {}
105 116
106 117 def can_write_to_group_validator(node, value):
107 118 """
108 119 Checks if given repo path is writable by user. This includes checks if
109 120 user is allowed to create repositories under root path or under
110 121 repo group paths
111 122 """
112 123
113 124 from rhodecode.lib.auth import (
114 125 HasPermissionAny, HasRepoGroupPermissionAny)
115 126 from rhodecode.model.repo_group import RepoGroupModel
116 127
117 128 messages = {
118 129 'invalid_repo_group':
119 130 _(u"Repository group `{}` does not exist"),
120 131 # permissions denied we expose as not existing, to prevent
121 132 # resource discovery
122 133 'permission_denied':
123 134 _(u"Repository group `{}` does not exist"),
124 135 'permission_denied_root':
125 136 _(u"You do not have the permission to store "
126 137 u"repositories in the root location.")
127 138 }
128 139
129 140 value = value['repo_group_name']
130 141
131 142 is_root_location = value is types.RootLocation
132 143 # NOT initialized validators, we must call them
133 144 can_create_repos_at_root = HasPermissionAny(
134 145 'hg.admin', 'hg.create.repository')
135 146
136 147 # if values is root location, we simply need to check if we can write
137 148 # to root location !
138 149 if is_root_location:
139 150 if can_create_repos_at_root(user=request_user):
140 151 # we can create repo group inside tool-level. No more checks
141 152 # are required
142 153 return
143 154 else:
144 155 # "fake" node name as repo_name, otherwise we oddly report
145 156 # the error as if it was coming form repo_group
146 157 # however repo_group is empty when using root location.
147 158 node.name = 'repo_name'
148 159 raise colander.Invalid(node, messages['permission_denied_root'])
149 160
150 161 # parent group not exists ? throw an error
151 162 repo_group = RepoGroupModel().get_by_group_name(value)
152 163 if value and not repo_group:
153 164 raise colander.Invalid(
154 165 node, messages['invalid_repo_group'].format(value))
155 166
156 167 gr_name = repo_group.group_name
157 168
158 169 # create repositories with write permission on group is set to true
159 170 create_on_write = HasPermissionAny(
160 171 'hg.create.write_on_repogroup.true')(user=request_user)
161 172
162 173 group_admin = HasRepoGroupPermissionAny('group.admin')(
163 174 gr_name, 'can write into group validator', user=request_user)
164 175 group_write = HasRepoGroupPermissionAny('group.write')(
165 176 gr_name, 'can write into group validator', user=request_user)
166 177
167 178 forbidden = not (group_admin or (group_write and create_on_write))
168 179
169 180 # TODO: handling of old values, and detecting no-change in path
170 181 # to skip permission checks in such cases. This only needs to be
171 182 # implemented if we use this schema in forms as well
172 183
173 184 # gid = (old_data['repo_group'].get('group_id')
174 185 # if (old_data and 'repo_group' in old_data) else None)
175 186 # value_changed = gid != safe_int(value)
176 187 # new = not old_data
177 188
178 189 # do check if we changed the value, there's a case that someone got
179 190 # revoked write permissions to a repository, he still created, we
180 191 # don't need to check permission if he didn't change the value of
181 192 # groups in form box
182 193 # if value_changed or new:
183 194 # # parent group need to be existing
184 195 # TODO: ENDS HERE
185 196
186 197 if repo_group and forbidden:
187 198 msg = messages['permission_denied'].format(value)
188 199 raise colander.Invalid(node, msg)
189 200
190 201 return can_write_to_group_validator
191 202
192 203
193 204 @colander.deferred
194 205 def deferred_unique_name_validator(node, kw):
195 206 request_user = kw.get('user')
196 207 old_values = kw.get('old_values') or {}
197 208
198 209 def unique_name_validator(node, value):
199 210 from rhodecode.model.db import Repository, RepoGroup
200 211 name_changed = value != old_values.get('repo_name')
201 212
202 213 existing = Repository.get_by_repo_name(value)
203 214 if name_changed and existing:
204 215 msg = _(u'Repository with name `{}` already exists').format(value)
205 216 raise colander.Invalid(node, msg)
206 217
207 218 existing_group = RepoGroup.get_by_group_name(value)
208 219 if name_changed and existing_group:
209 220 msg = _(u'Repository group with name `{}` already exists').format(
210 221 value)
211 222 raise colander.Invalid(node, msg)
212 223 return unique_name_validator
213 224
214 225
215 226 @colander.deferred
216 227 def deferred_repo_name_validator(node, kw):
217 228 def no_git_suffix_validator(node, value):
218 229 if value.endswith('.git'):
219 230 msg = _('Repository name cannot end with .git')
220 231 raise colander.Invalid(node, msg)
221 232 return colander.All(
222 233 no_git_suffix_validator, validators.valid_name_validator)
223 234
224 235
225 236 @colander.deferred
226 237 def deferred_repo_group_validator(node, kw):
227 238 options = kw.get(
228 239 'repo_repo_group_options')
229 240 return colander.OneOf([x for x in options])
230 241
231 242
232 243 @colander.deferred
233 244 def deferred_repo_group_widget(node, kw):
234 245 items = kw.get('repo_repo_group_items')
235 246 return deform.widget.Select2Widget(values=items)
236 247
237 248
238 249 class GroupType(colander.Mapping):
239 250 def _validate(self, node, value):
240 251 try:
241 252 return dict(repo_group_name=value)
242 253 except Exception as e:
243 254 raise colander.Invalid(
244 255 node, '"${val}" is not a mapping type: ${err}'.format(
245 256 val=value, err=e))
246 257
247 258 def deserialize(self, node, cstruct):
248 259 if cstruct is colander.null:
249 260 return cstruct
250 261
251 262 appstruct = super(GroupType, self).deserialize(node, cstruct)
252 263 validated_name = appstruct['repo_group_name']
253 264
254 265 # inject group based on once deserialized data
255 266 (repo_name_without_group,
256 267 parent_group_name,
257 268 parent_group) = get_group_and_repo(validated_name)
258 269
259 270 appstruct['repo_name_with_group'] = validated_name
260 271 appstruct['repo_name_without_group'] = repo_name_without_group
261 272 appstruct['repo_group_name'] = parent_group_name or types.RootLocation
262 273
263 274 if parent_group:
264 275 appstruct['repo_group_id'] = parent_group.group_id
265 276
266 277 return appstruct
267 278
268 279
269 280 class GroupSchema(colander.SchemaNode):
270 281 schema_type = GroupType
271 282 validator = deferred_can_write_to_group_validator
272 283 missing = colander.null
273 284
274 285
275 286 class RepoGroup(GroupSchema):
276 287 repo_group_name = colander.SchemaNode(
277 288 types.GroupNameType())
278 289 repo_group_id = colander.SchemaNode(
279 290 colander.String(), missing=None)
280 291 repo_name_without_group = colander.SchemaNode(
281 292 colander.String(), missing=None)
282 293
283 294
284 295 class RepoGroupAccessSchema(colander.MappingSchema):
285 296 repo_group = RepoGroup()
286 297
287 298
288 299 class RepoNameUniqueSchema(colander.MappingSchema):
289 300 unique_repo_name = colander.SchemaNode(
290 301 colander.String(),
291 302 validator=deferred_unique_name_validator)
292 303
293 304
294 305 class RepoSchema(colander.MappingSchema):
295 306
296 307 repo_name = colander.SchemaNode(
297 308 types.RepoNameType(),
298 309 validator=deferred_repo_name_validator)
299 310
300 311 repo_type = colander.SchemaNode(
301 312 colander.String(),
302 313 validator=deferred_repo_type_validator)
303 314
304 315 repo_owner = colander.SchemaNode(
305 316 colander.String(),
306 317 validator=deferred_repo_owner_validator,
307 318 widget=deform.widget.TextInputWidget())
308 319
309 320 repo_description = colander.SchemaNode(
310 321 colander.String(), missing='',
311 322 widget=deform.widget.TextAreaWidget())
312 323
313 324 repo_landing_commit_ref = colander.SchemaNode(
314 325 colander.String(),
315 326 validator=deferred_landing_ref_validator,
316 327 preparers=[preparers.strip_preparer],
317 328 missing=DEFAULT_LANDING_REF,
318 329 widget=deferred_landing_ref_widget)
319 330
320 331 repo_clone_uri = colander.SchemaNode(
321 332 colander.String(),
322 333 validator=deferred_sync_uri_validator,
323 334 preparers=[preparers.strip_preparer],
324 335 missing='')
325 336
326 337 repo_push_uri = colander.SchemaNode(
327 338 colander.String(),
328 339 validator=deferred_sync_uri_validator,
329 340 preparers=[preparers.strip_preparer],
330 341 missing='')
331 342
332 343 repo_fork_of = colander.SchemaNode(
333 344 colander.String(),
334 345 validator=deferred_fork_of_validator,
335 346 missing=None)
336 347
337 348 repo_private = colander.SchemaNode(
338 349 types.StringBooleanType(),
339 350 missing=False, widget=deform.widget.CheckboxWidget())
340 351 repo_copy_permissions = colander.SchemaNode(
341 352 types.StringBooleanType(),
342 353 missing=False, widget=deform.widget.CheckboxWidget())
343 354 repo_enable_statistics = colander.SchemaNode(
344 355 types.StringBooleanType(),
345 356 missing=False, widget=deform.widget.CheckboxWidget())
346 357 repo_enable_downloads = colander.SchemaNode(
347 358 types.StringBooleanType(),
348 359 missing=False, widget=deform.widget.CheckboxWidget())
349 360 repo_enable_locking = colander.SchemaNode(
350 361 types.StringBooleanType(),
351 362 missing=False, widget=deform.widget.CheckboxWidget())
352 363
353 364 def deserialize(self, cstruct):
354 365 """
355 366 Custom deserialize that allows to chain validation, and verify
356 367 permissions, and as last step uniqueness
357 368 """
358 369
359 370 # first pass, to validate given data
360 371 appstruct = super(RepoSchema, self).deserialize(cstruct)
361 372 validated_name = appstruct['repo_name']
362 373
363 374 # second pass to validate permissions to repo_group
364 375 second = RepoGroupAccessSchema().bind(**self.bindings)
365 376 appstruct_second = second.deserialize({'repo_group': validated_name})
366 377 # save result
367 378 appstruct['repo_group'] = appstruct_second['repo_group']
368 379
369 380 # thirds to validate uniqueness
370 381 third = RepoNameUniqueSchema().bind(**self.bindings)
371 382 third.deserialize({'unique_repo_name': validated_name})
372 383
373 384 return appstruct
374 385
375 386
376 387 class RepoSettingsSchema(RepoSchema):
377 388 repo_group = colander.SchemaNode(
378 389 colander.Integer(),
379 390 validator=deferred_repo_group_validator,
380 391 widget=deferred_repo_group_widget,
381 392 missing='')
382 393
383 394 repo_clone_uri_change = colander.SchemaNode(
384 395 colander.String(),
385 396 missing='NEW')
386 397
387 398 repo_clone_uri = colander.SchemaNode(
388 399 colander.String(),
389 400 preparers=[preparers.strip_preparer],
390 401 validator=deferred_sync_uri_validator,
391 402 missing='')
392 403
393 404 repo_push_uri_change = colander.SchemaNode(
394 405 colander.String(),
395 406 missing='NEW')
396 407
397 408 repo_push_uri = colander.SchemaNode(
398 409 colander.String(),
399 410 preparers=[preparers.strip_preparer],
400 411 validator=deferred_sync_uri_validator,
401 412 missing='')
402 413
403 414 def deserialize(self, cstruct):
404 415 """
405 416 Custom deserialize that allows to chain validation, and verify
406 417 permissions, and as last step uniqueness
407 418 """
408 419
409 420 # first pass, to validate given data
410 421 appstruct = super(RepoSchema, self).deserialize(cstruct)
411 422 validated_name = appstruct['repo_name']
412 423 # because of repoSchema adds repo-group as an ID, we inject it as
413 424 # full name here because validators require it, it's unwrapped later
414 425 # so it's safe to use and final name is going to be without group anyway
415 426
416 427 group, separator = get_repo_group(appstruct['repo_group'])
417 428 if group:
418 429 validated_name = separator.join([group.group_name, validated_name])
419 430
420 431 # second pass to validate permissions to repo_group
421 432 second = RepoGroupAccessSchema().bind(**self.bindings)
422 433 appstruct_second = second.deserialize({'repo_group': validated_name})
423 434 # save result
424 435 appstruct['repo_group'] = appstruct_second['repo_group']
425 436
426 437 # thirds to validate uniqueness
427 438 third = RepoNameUniqueSchema().bind(**self.bindings)
428 439 third.deserialize({'unique_repo_name': validated_name})
429 440
430 441 return appstruct
@@ -1,164 +1,151 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ${h.secure_form(h.route_path('repo_create'), request=request)}
4 4 <div class="form">
5 5 <!-- fields -->
6 6 <div class="fields">
7 7 <div class="field">
8 8 <div class="label">
9 9 <label for="repo_name">${_('Repository name')}:</label>
10 10 </div>
11 11 <div class="input">
12 12 ${h.text('repo_name', class_="medium")}
13 13 <div class="info-block">
14 14 <a id="remote_clone_toggle" href="#">${_('Import Existing Repository ?')}</a>
15 15 </div>
16 16 %if not c.rhodecode_user.is_admin:
17 17 ${h.hidden('user_created',True)}
18 18 %endif
19 19 </div>
20 20 </div>
21 21 <div id="remote_clone" class="field" style="display: none;">
22 22 <div class="label">
23 23 <label for="clone_uri">${_('Clone from')}:</label>
24 24 </div>
25 25 <div class="input">
26 26 ${h.text('clone_uri', class_="medium")}
27 27 <span class="help-block">
28 28 <pre>
29 29 - The repository must be accessible over http:// or https://
30 30 - For Git projects it's recommended appending .git to the end of clone url.
31 31 - Make sure to select proper repository type from the below selector before importing it.
32 32 - If your HTTP[S] repository is not publicly accessible,
33 33 add authentication information to the URL: https://username:password@server.company.com/repo-name.
34 34 - The Git LFS/Mercurial Largefiles objects will not be imported.
35 35 - For very large repositories, it's recommended to manually copy them into the
36 36 RhodeCode <a href="${h.route_path('admin_settings_vcs', _anchor='vcs-storage-options')}">storage location</a> and run <a href="${h.route_path('admin_settings_mapping')}">Remap and Rescan</a>.
37 37 </pre>
38 38 </span>
39 39 </div>
40 40 </div>
41 41 <div class="field">
42 <div class="label">
43 <label for="repo_type">${_('Type')}:</label>
44 </div>
45 <div class="select">
46 ${h.select('repo_type','hg',c.backends)}
47 <span class="help-block">${_('Set the type of repository to create.')}</span>
48 </div>
49 </div>
50 <div class="field">
51 42 <div class="label">
52 43 <label for="repo_group">${_('Repository group')}:</label>
53 44 </div>
54 45 <div class="select">
55 46 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
56 47 % if c.personal_repo_group:
57 48 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
58 49 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
59 50 </a>
60 51 % endif
61 52 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
62 53 </div>
63 54 </div>
64 55 <div class="field">
65 56 <div class="label">
57 <label for="repo_type">${_('Type')}:</label>
58 </div>
59 <div class="select">
60 ${h.select('repo_type','hg',c.backends)}
61 <span class="help-block">${_('Set the type of repository to create.')}</span>
62 </div>
63 </div>
64 <div class="field">
65 <div class="label">
66 66 <label for="repo_description">${_('Description')}:</label>
67 67 </div>
68 68 <div class="textarea editor">
69 69 ${h.textarea('repo_description',cols=23,rows=5,class_="medium")}
70 70 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
71 71 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
72 72 <span id="meta-tags-desc" style="display: none">
73 73 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
74 74 ${dt.metatags_help()}
75 75 </span>
76 76 </div>
77 77 </div>
78 <div class="field">
79 <div class="label">
80 <label for="repo_landing_rev">${_('Landing commit')}:</label>
81 </div>
82 <div class="select">
83 ${h.select('repo_landing_rev','',c.landing_revs,class_="medium")}
84 <span class="help-block">${_('The default commit for file pages, downloads, full text search index, and README generation.')}</span>
85 </div>
86 </div>
87 78 <div id="copy_perms" class="field">
88 79 <div class="label label-checkbox">
89 80 <label for="repo_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
90 81 </div>
91 82 <div class="checkboxes">
92 83 ${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
93 84 <span class="help-block">${_('Copy permissions from parent repository group.')}</span>
94 85 </div>
95 86 </div>
96 87 <div class="field">
97 88 <div class="label label-checkbox">
98 89 <label for="repo_private">${_('Private Repository')}:</label>
99 90 </div>
100 91 <div class="checkboxes">
101 92 ${h.checkbox('repo_private',value="True")}
102 93 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
103 94 </div>
104 95 </div>
105 96 <div class="buttons">
106 97 ${h.submit('save',_('Save'),class_="btn")}
107 98 </div>
108 99 </div>
109 100 </div>
110 101 <script>
111 102 $(document).ready(function(){
112 103 var setCopyPermsOption = function(group_val){
113 104 if(group_val != "-1"){
114 105 $('#copy_perms').show()
115 106 }
116 107 else{
117 108 $('#copy_perms').hide();
118 109 }
119 110 };
120 111
121 112 $('#remote_clone_toggle').on('click', function(e){
122 113 $('#remote_clone').show();
123 114 e.preventDefault();
124 115 });
125 116
126 117 if($('#remote_clone input').hasClass('error')){
127 118 $('#remote_clone').show();
128 119 }
129 120 if($('#remote_clone input').val()){
130 121 $('#remote_clone').show();
131 122 }
132 123
133 124 $("#repo_group").select2({
134 125 'containerCssClass': "drop-menu",
135 126 'dropdownCssClass': "drop-menu-dropdown",
136 127 'dropdownAutoWidth': true,
137 128 'width': "resolve"
138 129 });
139 130
140 131 setCopyPermsOption($('#repo_group').val());
141 132 $("#repo_group").on("change", function(e) {
142 133 setCopyPermsOption(e.val)
143 134 });
144 135
145 136 $("#repo_type").select2({
146 137 'containerCssClass': "drop-menu",
147 138 'dropdownCssClass': "drop-menu-dropdown",
148 139 'minimumResultsForSearch': -1,
149 140 });
150 $("#repo_landing_rev").select2({
151 'containerCssClass': "drop-menu",
152 'dropdownCssClass': "drop-menu-dropdown",
153 'minimumResultsForSearch': -1,
154 });
141
155 142 $('#repo_name').focus();
156 143
157 144 $('#select_my_group').on('click', function(e){
158 145 e.preventDefault();
159 146 $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
160 147 })
161 148
162 149 })
163 150 </script>
164 151 ${h.end_form()}
@@ -1,127 +1,117 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Fork repository %s') % c.repo_name}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()"></%def>
12 12
13 13 <%def name="menu_bar_nav()">
14 14 ${self.menu_items(active='repositories')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='options')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 ${h.secure_form(h.route_path('repo_fork_create',repo_name=c.rhodecode_db_repo.repo_name), request=request)}
24 24 <div class="form">
25 25 <!-- fields -->
26 26 <div class="fields">
27 27
28 28 <div class="field">
29 29 <div class="label">
30 30 <label for="repo_name">${_('Fork name')}:</label>
31 31 </div>
32 32 <div class="input">
33 33 ${h.text('repo_name', class_="medium")}
34 34 ${h.hidden('repo_type',c.rhodecode_db_repo.repo_type)}
35 35 ${h.hidden('fork_parent_id',c.rhodecode_db_repo.repo_id)}
36 36 </div>
37 37 </div>
38 38
39 39 <div class="field">
40 40 <div class="label">
41 41 <label for="repo_group">${_('Repository group')}:</label>
42 42 </div>
43 43 <div class="select">
44 44 ${h.select('repo_group','',c.repo_groups,class_="medium")}
45 45 % if c.personal_repo_group:
46 46 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
47 47 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
48 48 </a>
49 49 % endif
50 50 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
51 51 </div>
52 52 </div>
53 53
54 54 <div class="field">
55 55 <div class="label label-textarea">
56 56 <label for="description">${_('Description')}:</label>
57 57 </div>
58 58 <div class="textarea editor">
59 59 ${h.textarea('description',cols=23,rows=5,class_="medium")}
60 60 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
61 61 <span class="help-block">${_('Plain text format with support of {metatags}. Add a README file for longer descriptions').format(metatags=metatags_url)|n}</span>
62 62 <span id="meta-tags-desc" style="display: none">
63 63 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
64 64 ${dt.metatags_help()}
65 65 </span>
66 66 </div>
67 67 </div>
68 68
69 69 <div class="field">
70 <div class="label">
71 <label for="landing_rev">${_('Landing commit')}:</label>
72 </div>
73 <div class="select">
74 ${h.select('landing_rev','',c.landing_revs,class_="medium")}
75 <span class="help-block">${_('The default commit for file pages, downloads, full text search index, and README generation.')}</span>
76 </div>
77 </div>
78
79 <div class="field">
80 70 <div class="label label-checkbox">
81 71 <label for="private">${_('Copy permissions')}:</label>
82 72 </div>
83 73 <div class="checkboxes">
84 74 ${h.checkbox('copy_permissions',value="True", checked="checked")}
85 75 <span class="help-block">${_('Copy permissions from parent repository.')}</span>
86 76 </div>
87 77 </div>
88 78
89 79 <div class="field">
90 80 <div class="label label-checkbox">
91 81 <label for="private">${_('Private')}:</label>
92 82 </div>
93 83 <div class="checkboxes">
94 84 ${h.checkbox('private',value="True")}
95 85 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
96 86 </div>
97 87 </div>
98 88
99 89 <div class="buttons">
100 90 ${h.submit('',_('Fork this Repository'),class_="btn")}
101 91 </div>
102 92 </div>
103 93 </div>
104 94 ${h.end_form()}
105 95 </div>
106 96 <script>
107 97 $(document).ready(function(){
108 98 $("#repo_group").select2({
109 99 'dropdownAutoWidth': true,
110 100 'containerCssClass': "drop-menu",
111 101 'dropdownCssClass': "drop-menu-dropdown",
112 102 'width': "resolve"
113 103 });
114 104 $("#landing_rev").select2({
115 105 'containerCssClass': "drop-menu",
116 106 'dropdownCssClass': "drop-menu-dropdown",
117 107 'minimumResultsForSearch': -1
118 108 });
119 109 $('#repo_name').focus();
120 110
121 111 $('#select_my_group').on('click', function(e){
122 112 e.preventDefault();
123 113 $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
124 114 })
125 115 })
126 116 </script>
127 117 </%def>
General Comments 0
You need to be logged in to leave comments. Login now