##// END OF EJS Templates
api: expose new functions for FTS...
marcink -
r3460:866fba74 default
parent child Browse files
Show More
@@ -1,2166 +1,2280 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
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
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 40 from rhodecode.model.comment import CommentsModel
41 41 from rhodecode.model.db import (
42 42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
43 43 ChangesetComment)
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.scm import ScmModel, RepoList
46 46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 47 from rhodecode.model import validation_schema
48 48 from rhodecode.model.validation_schema.schemas import repo_schema
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 @jsonrpc_method()
54 54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
55 55 """
56 56 Gets an existing repository by its name or repository_id.
57 57
58 58 The members section so the output returns users groups or users
59 59 associated with that repository.
60 60
61 61 This command can only be run using an |authtoken| with admin rights,
62 62 or users with at least read rights to the |repo|.
63 63
64 64 :param apiuser: This is filled automatically from the |authtoken|.
65 65 :type apiuser: AuthUser
66 66 :param repoid: The repository name or repository id.
67 67 :type repoid: str or int
68 68 :param cache: use the cached value for last changeset
69 69 :type: cache: Optional(bool)
70 70
71 71 Example output:
72 72
73 73 .. code-block:: bash
74 74
75 75 {
76 76 "error": null,
77 77 "id": <repo_id>,
78 78 "result": {
79 79 "clone_uri": null,
80 80 "created_on": "timestamp",
81 81 "description": "repo description",
82 82 "enable_downloads": false,
83 83 "enable_locking": false,
84 84 "enable_statistics": false,
85 85 "followers": [
86 86 {
87 87 "active": true,
88 88 "admin": false,
89 89 "api_key": "****************************************",
90 90 "api_keys": [
91 91 "****************************************"
92 92 ],
93 93 "email": "user@example.com",
94 94 "emails": [
95 95 "user@example.com"
96 96 ],
97 97 "extern_name": "rhodecode",
98 98 "extern_type": "rhodecode",
99 99 "firstname": "username",
100 100 "ip_addresses": [],
101 101 "language": null,
102 102 "last_login": "2015-09-16T17:16:35.854",
103 103 "lastname": "surname",
104 104 "user_id": <user_id>,
105 105 "username": "name"
106 106 }
107 107 ],
108 108 "fork_of": "parent-repo",
109 109 "landing_rev": [
110 110 "rev",
111 111 "tip"
112 112 ],
113 113 "last_changeset": {
114 114 "author": "User <user@example.com>",
115 115 "branch": "default",
116 116 "date": "timestamp",
117 117 "message": "last commit message",
118 118 "parents": [
119 119 {
120 120 "raw_id": "commit-id"
121 121 }
122 122 ],
123 123 "raw_id": "commit-id",
124 124 "revision": <revision number>,
125 125 "short_id": "short id"
126 126 },
127 127 "lock_reason": null,
128 128 "locked_by": null,
129 129 "locked_date": null,
130 130 "owner": "owner-name",
131 131 "permissions": [
132 132 {
133 133 "name": "super-admin-name",
134 134 "origin": "super-admin",
135 135 "permission": "repository.admin",
136 136 "type": "user"
137 137 },
138 138 {
139 139 "name": "owner-name",
140 140 "origin": "owner",
141 141 "permission": "repository.admin",
142 142 "type": "user"
143 143 },
144 144 {
145 145 "name": "user-group-name",
146 146 "origin": "permission",
147 147 "permission": "repository.write",
148 148 "type": "user_group"
149 149 }
150 150 ],
151 151 "private": true,
152 152 "repo_id": 676,
153 153 "repo_name": "user-group/repo-name",
154 154 "repo_type": "hg"
155 155 }
156 156 }
157 157 """
158 158
159 159 repo = get_repo_or_error(repoid)
160 160 cache = Optional.extract(cache)
161 161
162 162 include_secrets = False
163 163 if has_superadmin_permission(apiuser):
164 164 include_secrets = True
165 165 else:
166 166 # check if we have at least read permission for this repo !
167 167 _perms = (
168 168 'repository.admin', 'repository.write', 'repository.read',)
169 169 validate_repo_permissions(apiuser, repoid, repo, _perms)
170 170
171 171 permissions = []
172 172 for _user in repo.permissions():
173 173 user_data = {
174 174 'name': _user.username,
175 175 'permission': _user.permission,
176 176 'origin': get_origin(_user),
177 177 'type': "user",
178 178 }
179 179 permissions.append(user_data)
180 180
181 181 for _user_group in repo.permission_user_groups():
182 182 user_group_data = {
183 183 'name': _user_group.users_group_name,
184 184 'permission': _user_group.permission,
185 185 'origin': get_origin(_user_group),
186 186 'type': "user_group",
187 187 }
188 188 permissions.append(user_group_data)
189 189
190 190 following_users = [
191 191 user.user.get_api_data(include_secrets=include_secrets)
192 192 for user in repo.followers]
193 193
194 194 if not cache:
195 195 repo.update_commit_cache()
196 196 data = repo.get_api_data(include_secrets=include_secrets)
197 197 data['permissions'] = permissions
198 198 data['followers'] = following_users
199 199 return data
200 200
201 201
202 202 @jsonrpc_method()
203 203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
204 204 """
205 205 Lists all existing repositories.
206 206
207 207 This command can only be run using an |authtoken| with admin rights,
208 208 or users with at least read rights to |repos|.
209 209
210 210 :param apiuser: This is filled automatically from the |authtoken|.
211 211 :type apiuser: AuthUser
212 212 :param root: specify root repository group to fetch repositories.
213 213 filters the returned repositories to be members of given root group.
214 214 :type root: Optional(None)
215 215 :param traverse: traverse given root into subrepositories. With this flag
216 216 set to False, it will only return top-level repositories from `root`.
217 217 if root is empty it will return just top-level repositories.
218 218 :type traverse: Optional(True)
219 219
220 220
221 221 Example output:
222 222
223 223 .. code-block:: bash
224 224
225 225 id : <id_given_in_input>
226 226 result: [
227 227 {
228 228 "repo_id" : "<repo_id>",
229 229 "repo_name" : "<reponame>"
230 230 "repo_type" : "<repo_type>",
231 231 "clone_uri" : "<clone_uri>",
232 232 "private": : "<bool>",
233 233 "created_on" : "<datetimecreated>",
234 234 "description" : "<description>",
235 235 "landing_rev": "<landing_rev>",
236 236 "owner": "<repo_owner>",
237 237 "fork_of": "<name_of_fork_parent>",
238 238 "enable_downloads": "<bool>",
239 239 "enable_locking": "<bool>",
240 240 "enable_statistics": "<bool>",
241 241 },
242 242 ...
243 243 ]
244 244 error: null
245 245 """
246 246
247 247 include_secrets = has_superadmin_permission(apiuser)
248 248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
249 249 extras = {'user': apiuser}
250 250
251 251 root = Optional.extract(root)
252 252 traverse = Optional.extract(traverse, binary=True)
253 253
254 254 if root:
255 255 # verify parent existance, if it's empty return an error
256 256 parent = RepoGroup.get_by_group_name(root)
257 257 if not parent:
258 258 raise JSONRPCError(
259 259 'Root repository group `{}` does not exist'.format(root))
260 260
261 261 if traverse:
262 262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
263 263 else:
264 264 repos = RepoModel().get_repos_for_root(root=parent)
265 265 else:
266 266 if traverse:
267 267 repos = RepoModel().get_all()
268 268 else:
269 269 # return just top-level
270 270 repos = RepoModel().get_repos_for_root(root=None)
271 271
272 272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
273 273 return [repo.get_api_data(include_secrets=include_secrets)
274 274 for repo in repo_list]
275 275
276 276
277 277 @jsonrpc_method()
278 278 def get_repo_changeset(request, apiuser, repoid, revision,
279 279 details=Optional('basic')):
280 280 """
281 281 Returns information about a changeset.
282 282
283 283 Additionally parameters define the amount of details returned by
284 284 this function.
285 285
286 286 This command can only be run using an |authtoken| with admin rights,
287 287 or users with at least read rights to the |repo|.
288 288
289 289 :param apiuser: This is filled automatically from the |authtoken|.
290 290 :type apiuser: AuthUser
291 291 :param repoid: The repository name or repository id
292 292 :type repoid: str or int
293 293 :param revision: revision for which listing should be done
294 294 :type revision: str
295 295 :param details: details can be 'basic|extended|full' full gives diff
296 296 info details like the diff itself, and number of changed files etc.
297 297 :type details: Optional(str)
298 298
299 299 """
300 300 repo = get_repo_or_error(repoid)
301 301 if not has_superadmin_permission(apiuser):
302 302 _perms = (
303 303 'repository.admin', 'repository.write', 'repository.read',)
304 304 validate_repo_permissions(apiuser, repoid, repo, _perms)
305 305
306 306 changes_details = Optional.extract(details)
307 307 _changes_details_types = ['basic', 'extended', 'full']
308 308 if changes_details not in _changes_details_types:
309 309 raise JSONRPCError(
310 310 'ret_type must be one of %s' % (
311 311 ','.join(_changes_details_types)))
312 312
313 313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
314 314 'status', '_commit', '_file_paths']
315 315
316 316 try:
317 317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
318 318 except TypeError as e:
319 319 raise JSONRPCError(safe_str(e))
320 320 _cs_json = cs.__json__()
321 321 _cs_json['diff'] = build_commit_data(cs, changes_details)
322 322 if changes_details == 'full':
323 323 _cs_json['refs'] = cs._get_refs()
324 324 return _cs_json
325 325
326 326
327 327 @jsonrpc_method()
328 328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
329 329 details=Optional('basic')):
330 330 """
331 331 Returns a set of commits limited by the number starting
332 332 from the `start_rev` option.
333 333
334 334 Additional parameters define the amount of details returned by this
335 335 function.
336 336
337 337 This command can only be run using an |authtoken| with admin rights,
338 338 or users with at least read rights to |repos|.
339 339
340 340 :param apiuser: This is filled automatically from the |authtoken|.
341 341 :type apiuser: AuthUser
342 342 :param repoid: The repository name or repository ID.
343 343 :type repoid: str or int
344 344 :param start_rev: The starting revision from where to get changesets.
345 345 :type start_rev: str
346 346 :param limit: Limit the number of commits to this amount
347 347 :type limit: str or int
348 348 :param details: Set the level of detail returned. Valid option are:
349 349 ``basic``, ``extended`` and ``full``.
350 350 :type details: Optional(str)
351 351
352 352 .. note::
353 353
354 354 Setting the parameter `details` to the value ``full`` is extensive
355 355 and returns details like the diff itself, and the number
356 356 of changed files.
357 357
358 358 """
359 359 repo = get_repo_or_error(repoid)
360 360 if not has_superadmin_permission(apiuser):
361 361 _perms = (
362 362 'repository.admin', 'repository.write', 'repository.read',)
363 363 validate_repo_permissions(apiuser, repoid, repo, _perms)
364 364
365 365 changes_details = Optional.extract(details)
366 366 _changes_details_types = ['basic', 'extended', 'full']
367 367 if changes_details not in _changes_details_types:
368 368 raise JSONRPCError(
369 369 'ret_type must be one of %s' % (
370 370 ','.join(_changes_details_types)))
371 371
372 372 limit = int(limit)
373 373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
374 374 'status', '_commit', '_file_paths']
375 375
376 376 vcs_repo = repo.scm_instance()
377 377 # SVN needs a special case to distinguish its index and commit id
378 378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
379 379 start_rev = vcs_repo.commit_ids[0]
380 380
381 381 try:
382 382 commits = vcs_repo.get_commits(
383 383 start_id=start_rev, pre_load=pre_load)
384 384 except TypeError as e:
385 385 raise JSONRPCError(safe_str(e))
386 386 except Exception:
387 387 log.exception('Fetching of commits failed')
388 388 raise JSONRPCError('Error occurred during commit fetching')
389 389
390 390 ret = []
391 391 for cnt, commit in enumerate(commits):
392 392 if cnt >= limit != -1:
393 393 break
394 394 _cs_json = commit.__json__()
395 395 _cs_json['diff'] = build_commit_data(commit, changes_details)
396 396 if changes_details == 'full':
397 397 _cs_json['refs'] = {
398 398 'branches': [commit.branch],
399 399 'bookmarks': getattr(commit, 'bookmarks', []),
400 400 'tags': commit.tags
401 401 }
402 402 ret.append(_cs_json)
403 403 return ret
404 404
405 405
406 406 @jsonrpc_method()
407 407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
408 408 ret_type=Optional('all'), details=Optional('basic'),
409 409 max_file_bytes=Optional(None)):
410 410 """
411 411 Returns a list of nodes and children in a flat list for a given
412 412 path at given revision.
413 413
414 414 It's possible to specify ret_type to show only `files` or `dirs`.
415 415
416 416 This command can only be run using an |authtoken| with admin rights,
417 417 or users with at least read rights to |repos|.
418 418
419 419 :param apiuser: This is filled automatically from the |authtoken|.
420 420 :type apiuser: AuthUser
421 421 :param repoid: The repository name or repository ID.
422 422 :type repoid: str or int
423 423 :param revision: The revision for which listing should be done.
424 424 :type revision: str
425 425 :param root_path: The path from which to start displaying.
426 426 :type root_path: str
427 427 :param ret_type: Set the return type. Valid options are
428 428 ``all`` (default), ``files`` and ``dirs``.
429 429 :type ret_type: Optional(str)
430 430 :param details: Returns extended information about nodes, such as
431 md5, binary, and or content. The valid options are ``basic`` and
432 ``full``.
431 md5, binary, and or content.
432 The valid options are ``basic`` and ``full``.
433 433 :type details: Optional(str)
434 434 :param max_file_bytes: Only return file content under this file size bytes
435 435 :type details: Optional(int)
436 436
437 437 Example output:
438 438
439 439 .. code-block:: bash
440 440
441 441 id : <id_given_in_input>
442 442 result: [
443 {
444 "name" : "<name>"
445 "type" : "<type>",
446 "binary": "<true|false>" (only in extended mode)
447 "md5" : "<md5 of file content>" (only in extended mode)
448 },
443 {
444 "binary": false,
445 "content": "File line\nLine2\n",
446 "extension": "md",
447 "lines": 2,
448 "md5": "059fa5d29b19c0657e384749480f6422",
449 "mimetype": "text/x-minidsrc",
450 "name": "file.md",
451 "size": 580,
452 "type": "file"
453 },
449 454 ...
450 455 ]
451 456 error: null
452 457 """
453 458
454 459 repo = get_repo_or_error(repoid)
455 460 if not has_superadmin_permission(apiuser):
456 _perms = (
457 'repository.admin', 'repository.write', 'repository.read',)
461 _perms = ('repository.admin', 'repository.write', 'repository.read',)
458 462 validate_repo_permissions(apiuser, repoid, repo, _perms)
459 463
460 464 ret_type = Optional.extract(ret_type)
461 465 details = Optional.extract(details)
462 466 _extended_types = ['basic', 'full']
463 467 if details not in _extended_types:
464 raise JSONRPCError(
465 'ret_type must be one of %s' % (','.join(_extended_types)))
468 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
466 469 extended_info = False
467 470 content = False
468 471 if details == 'basic':
469 472 extended_info = True
470 473
471 474 if details == 'full':
472 475 extended_info = content = True
473 476
474 477 _map = {}
475 478 try:
476 479 # check if repo is not empty by any chance, skip quicker if it is.
477 480 _scm = repo.scm_instance()
478 481 if _scm.is_empty():
479 482 return []
480 483
481 484 _d, _f = ScmModel().get_nodes(
482 485 repo, revision, root_path, flat=False,
483 486 extended_info=extended_info, content=content,
484 487 max_file_bytes=max_file_bytes)
485 488 _map = {
486 489 'all': _d + _f,
487 490 'files': _f,
488 491 'dirs': _d,
489 492 }
490 493 return _map[ret_type]
491 494 except KeyError:
492 495 raise JSONRPCError(
493 496 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
494 497 except Exception:
495 498 log.exception("Exception occurred while trying to get repo nodes")
496 499 raise JSONRPCError(
497 500 'failed to get repo: `%s` nodes' % repo.repo_name
498 501 )
499 502
500 503
501 504 @jsonrpc_method()
505 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
506 max_file_bytes=Optional(None), details=Optional('basic')):
507 """
508 Returns a single file from repository at given revision.
509
510 This command can only be run using an |authtoken| with admin rights,
511 or users with at least read rights to |repos|.
512
513 :param apiuser: This is filled automatically from the |authtoken|.
514 :type apiuser: AuthUser
515 :param repoid: The repository name or repository ID.
516 :type repoid: str or int
517 :param commit_id: The revision for which listing should be done.
518 :type commit_id: str
519 :param file_path: The path from which to start displaying.
520 :type file_path: str
521 :param details: Returns different set of information about nodes.
522 The valid options are ``minimal`` ``basic`` and ``full``.
523 :type details: Optional(str)
524 :param max_file_bytes: Only return file content under this file size bytes
525 :type details: Optional(int)
526
527 Example output:
528
529 .. code-block:: bash
530
531 id : <id_given_in_input>
532 result: {
533 "binary": false,
534 "extension": "py",
535 "lines": 35,
536 "content": "....",
537 "md5": "76318336366b0f17ee249e11b0c99c41",
538 "mimetype": "text/x-python",
539 "name": "python.py",
540 "size": 817,
541 "type": "file",
542 }
543 error: null
544 """
545
546 repo = get_repo_or_error(repoid)
547 if not has_superadmin_permission(apiuser):
548 _perms = ('repository.admin', 'repository.write', 'repository.read',)
549 validate_repo_permissions(apiuser, repoid, repo, _perms)
550
551 details = Optional.extract(details)
552 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
553 if details not in _extended_types:
554 raise JSONRPCError(
555 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
556 extended_info = False
557 content = False
558
559 if details == 'minimal':
560 extended_info = False
561
562 elif details == 'basic':
563 extended_info = True
564
565 elif details == 'full':
566 extended_info = content = True
567
568 try:
569 # check if repo is not empty by any chance, skip quicker if it is.
570 _scm = repo.scm_instance()
571 if _scm.is_empty():
572 return None
573
574 node = ScmModel().get_node(
575 repo, commit_id, file_path, extended_info=extended_info,
576 content=content, max_file_bytes=max_file_bytes)
577
578 except Exception:
579 log.exception("Exception occurred while trying to get repo node")
580 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
581
582 return node
583
584
585 @jsonrpc_method()
586 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
587 """
588 Returns a list of tree nodes for path at given revision. This api is built
589 strictly for usage in full text search building, and shouldn't be consumed
590
591 This command can only be run using an |authtoken| with admin rights,
592 or users with at least read rights to |repos|.
593
594 """
595
596 repo = get_repo_or_error(repoid)
597 if not has_superadmin_permission(apiuser):
598 _perms = ('repository.admin', 'repository.write', 'repository.read',)
599 validate_repo_permissions(apiuser, repoid, repo, _perms)
600
601 try:
602 # check if repo is not empty by any chance, skip quicker if it is.
603 _scm = repo.scm_instance()
604 if _scm.is_empty():
605 return []
606
607 tree_files = ScmModel().get_fts_data(repo, commit_id, root_path)
608 return tree_files
609
610 except Exception:
611 log.exception("Exception occurred while trying to get repo nodes")
612 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
613
614
615 @jsonrpc_method()
502 616 def get_repo_refs(request, apiuser, repoid):
503 617 """
504 618 Returns a dictionary of current references. It returns
505 619 bookmarks, branches, closed_branches, and tags for given repository
506 620
507 621 It's possible to specify ret_type to show only `files` or `dirs`.
508 622
509 623 This command can only be run using an |authtoken| with admin rights,
510 624 or users with at least read rights to |repos|.
511 625
512 626 :param apiuser: This is filled automatically from the |authtoken|.
513 627 :type apiuser: AuthUser
514 628 :param repoid: The repository name or repository ID.
515 629 :type repoid: str or int
516 630
517 631 Example output:
518 632
519 633 .. code-block:: bash
520 634
521 635 id : <id_given_in_input>
522 636 "result": {
523 637 "bookmarks": {
524 638 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
525 639 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
526 640 },
527 641 "branches": {
528 642 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
529 643 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
530 644 },
531 645 "branches_closed": {},
532 646 "tags": {
533 647 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
534 648 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
535 649 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
536 650 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
537 651 }
538 652 }
539 653 error: null
540 654 """
541 655
542 656 repo = get_repo_or_error(repoid)
543 657 if not has_superadmin_permission(apiuser):
544 658 _perms = ('repository.admin', 'repository.write', 'repository.read',)
545 659 validate_repo_permissions(apiuser, repoid, repo, _perms)
546 660
547 661 try:
548 662 # check if repo is not empty by any chance, skip quicker if it is.
549 663 vcs_instance = repo.scm_instance()
550 664 refs = vcs_instance.refs()
551 665 return refs
552 666 except Exception:
553 667 log.exception("Exception occurred while trying to get repo refs")
554 668 raise JSONRPCError(
555 669 'failed to get repo: `%s` references' % repo.repo_name
556 670 )
557 671
558 672
559 673 @jsonrpc_method()
560 674 def create_repo(
561 675 request, apiuser, repo_name, repo_type,
562 676 owner=Optional(OAttr('apiuser')),
563 677 description=Optional(''),
564 678 private=Optional(False),
565 679 clone_uri=Optional(None),
566 680 push_uri=Optional(None),
567 681 landing_rev=Optional('rev:tip'),
568 682 enable_statistics=Optional(False),
569 683 enable_locking=Optional(False),
570 684 enable_downloads=Optional(False),
571 685 copy_permissions=Optional(False)):
572 686 """
573 687 Creates a repository.
574 688
575 689 * If the repository name contains "/", repository will be created inside
576 690 a repository group or nested repository groups
577 691
578 692 For example "foo/bar/repo1" will create |repo| called "repo1" inside
579 693 group "foo/bar". You have to have permissions to access and write to
580 694 the last repository group ("bar" in this example)
581 695
582 696 This command can only be run using an |authtoken| with at least
583 697 permissions to create repositories, or write permissions to
584 698 parent repository groups.
585 699
586 700 :param apiuser: This is filled automatically from the |authtoken|.
587 701 :type apiuser: AuthUser
588 702 :param repo_name: Set the repository name.
589 703 :type repo_name: str
590 704 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
591 705 :type repo_type: str
592 706 :param owner: user_id or username
593 707 :type owner: Optional(str)
594 708 :param description: Set the repository description.
595 709 :type description: Optional(str)
596 710 :param private: set repository as private
597 711 :type private: bool
598 712 :param clone_uri: set clone_uri
599 713 :type clone_uri: str
600 714 :param push_uri: set push_uri
601 715 :type push_uri: str
602 716 :param landing_rev: <rev_type>:<rev>
603 717 :type landing_rev: str
604 718 :param enable_locking:
605 719 :type enable_locking: bool
606 720 :param enable_downloads:
607 721 :type enable_downloads: bool
608 722 :param enable_statistics:
609 723 :type enable_statistics: bool
610 724 :param copy_permissions: Copy permission from group in which the
611 725 repository is being created.
612 726 :type copy_permissions: bool
613 727
614 728
615 729 Example output:
616 730
617 731 .. code-block:: bash
618 732
619 733 id : <id_given_in_input>
620 734 result: {
621 735 "msg": "Created new repository `<reponame>`",
622 736 "success": true,
623 737 "task": "<celery task id or None if done sync>"
624 738 }
625 739 error: null
626 740
627 741
628 742 Example error output:
629 743
630 744 .. code-block:: bash
631 745
632 746 id : <id_given_in_input>
633 747 result : null
634 748 error : {
635 749 'failed to create repository `<repo_name>`'
636 750 }
637 751
638 752 """
639 753
640 754 owner = validate_set_owner_permissions(apiuser, owner)
641 755
642 756 description = Optional.extract(description)
643 757 copy_permissions = Optional.extract(copy_permissions)
644 758 clone_uri = Optional.extract(clone_uri)
645 759 push_uri = Optional.extract(push_uri)
646 760 landing_commit_ref = Optional.extract(landing_rev)
647 761
648 762 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
649 763 if isinstance(private, Optional):
650 764 private = defs.get('repo_private') or Optional.extract(private)
651 765 if isinstance(repo_type, Optional):
652 766 repo_type = defs.get('repo_type')
653 767 if isinstance(enable_statistics, Optional):
654 768 enable_statistics = defs.get('repo_enable_statistics')
655 769 if isinstance(enable_locking, Optional):
656 770 enable_locking = defs.get('repo_enable_locking')
657 771 if isinstance(enable_downloads, Optional):
658 772 enable_downloads = defs.get('repo_enable_downloads')
659 773
660 774 schema = repo_schema.RepoSchema().bind(
661 775 repo_type_options=rhodecode.BACKENDS.keys(),
662 776 repo_type=repo_type,
663 777 # user caller
664 778 user=apiuser)
665 779
666 780 try:
667 781 schema_data = schema.deserialize(dict(
668 782 repo_name=repo_name,
669 783 repo_type=repo_type,
670 784 repo_owner=owner.username,
671 785 repo_description=description,
672 786 repo_landing_commit_ref=landing_commit_ref,
673 787 repo_clone_uri=clone_uri,
674 788 repo_push_uri=push_uri,
675 789 repo_private=private,
676 790 repo_copy_permissions=copy_permissions,
677 791 repo_enable_statistics=enable_statistics,
678 792 repo_enable_downloads=enable_downloads,
679 793 repo_enable_locking=enable_locking))
680 794 except validation_schema.Invalid as err:
681 795 raise JSONRPCValidationError(colander_exc=err)
682 796
683 797 try:
684 798 data = {
685 799 'owner': owner,
686 800 'repo_name': schema_data['repo_group']['repo_name_without_group'],
687 801 'repo_name_full': schema_data['repo_name'],
688 802 'repo_group': schema_data['repo_group']['repo_group_id'],
689 803 'repo_type': schema_data['repo_type'],
690 804 'repo_description': schema_data['repo_description'],
691 805 'repo_private': schema_data['repo_private'],
692 806 'clone_uri': schema_data['repo_clone_uri'],
693 807 'push_uri': schema_data['repo_push_uri'],
694 808 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
695 809 'enable_statistics': schema_data['repo_enable_statistics'],
696 810 'enable_locking': schema_data['repo_enable_locking'],
697 811 'enable_downloads': schema_data['repo_enable_downloads'],
698 812 'repo_copy_permissions': schema_data['repo_copy_permissions'],
699 813 }
700 814
701 815 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
702 816 task_id = get_task_id(task)
703 817 # no commit, it's done in RepoModel, or async via celery
704 818 return {
705 819 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
706 820 'success': True, # cannot return the repo data here since fork
707 821 # can be done async
708 822 'task': task_id
709 823 }
710 824 except Exception:
711 825 log.exception(
712 826 u"Exception while trying to create the repository %s",
713 827 schema_data['repo_name'])
714 828 raise JSONRPCError(
715 829 'failed to create repository `%s`' % (schema_data['repo_name'],))
716 830
717 831
718 832 @jsonrpc_method()
719 833 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
720 834 description=Optional('')):
721 835 """
722 836 Adds an extra field to a repository.
723 837
724 838 This command can only be run using an |authtoken| with at least
725 839 write permissions to the |repo|.
726 840
727 841 :param apiuser: This is filled automatically from the |authtoken|.
728 842 :type apiuser: AuthUser
729 843 :param repoid: Set the repository name or repository id.
730 844 :type repoid: str or int
731 845 :param key: Create a unique field key for this repository.
732 846 :type key: str
733 847 :param label:
734 848 :type label: Optional(str)
735 849 :param description:
736 850 :type description: Optional(str)
737 851 """
738 852 repo = get_repo_or_error(repoid)
739 853 if not has_superadmin_permission(apiuser):
740 854 _perms = ('repository.admin',)
741 855 validate_repo_permissions(apiuser, repoid, repo, _perms)
742 856
743 857 label = Optional.extract(label) or key
744 858 description = Optional.extract(description)
745 859
746 860 field = RepositoryField.get_by_key_name(key, repo)
747 861 if field:
748 862 raise JSONRPCError('Field with key '
749 863 '`%s` exists for repo `%s`' % (key, repoid))
750 864
751 865 try:
752 866 RepoModel().add_repo_field(repo, key, field_label=label,
753 867 field_desc=description)
754 868 Session().commit()
755 869 return {
756 870 'msg': "Added new repository field `%s`" % (key,),
757 871 'success': True,
758 872 }
759 873 except Exception:
760 874 log.exception("Exception occurred while trying to add field to repo")
761 875 raise JSONRPCError(
762 876 'failed to create new field for repository `%s`' % (repoid,))
763 877
764 878
765 879 @jsonrpc_method()
766 880 def remove_field_from_repo(request, apiuser, repoid, key):
767 881 """
768 882 Removes an extra field from a repository.
769 883
770 884 This command can only be run using an |authtoken| with at least
771 885 write permissions to the |repo|.
772 886
773 887 :param apiuser: This is filled automatically from the |authtoken|.
774 888 :type apiuser: AuthUser
775 889 :param repoid: Set the repository name or repository ID.
776 890 :type repoid: str or int
777 891 :param key: Set the unique field key for this repository.
778 892 :type key: str
779 893 """
780 894
781 895 repo = get_repo_or_error(repoid)
782 896 if not has_superadmin_permission(apiuser):
783 897 _perms = ('repository.admin',)
784 898 validate_repo_permissions(apiuser, repoid, repo, _perms)
785 899
786 900 field = RepositoryField.get_by_key_name(key, repo)
787 901 if not field:
788 902 raise JSONRPCError('Field with key `%s` does not '
789 903 'exists for repo `%s`' % (key, repoid))
790 904
791 905 try:
792 906 RepoModel().delete_repo_field(repo, field_key=key)
793 907 Session().commit()
794 908 return {
795 909 'msg': "Deleted repository field `%s`" % (key,),
796 910 'success': True,
797 911 }
798 912 except Exception:
799 913 log.exception(
800 914 "Exception occurred while trying to delete field from repo")
801 915 raise JSONRPCError(
802 916 'failed to delete field for repository `%s`' % (repoid,))
803 917
804 918
805 919 @jsonrpc_method()
806 920 def update_repo(
807 921 request, apiuser, repoid, repo_name=Optional(None),
808 922 owner=Optional(OAttr('apiuser')), description=Optional(''),
809 923 private=Optional(False),
810 924 clone_uri=Optional(None), push_uri=Optional(None),
811 925 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
812 926 enable_statistics=Optional(False),
813 927 enable_locking=Optional(False),
814 928 enable_downloads=Optional(False), fields=Optional('')):
815 929 """
816 930 Updates a repository with the given information.
817 931
818 932 This command can only be run using an |authtoken| with at least
819 933 admin permissions to the |repo|.
820 934
821 935 * If the repository name contains "/", repository will be updated
822 936 accordingly with a repository group or nested repository groups
823 937
824 938 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
825 939 called "repo-test" and place it inside group "foo/bar".
826 940 You have to have permissions to access and write to the last repository
827 941 group ("bar" in this example)
828 942
829 943 :param apiuser: This is filled automatically from the |authtoken|.
830 944 :type apiuser: AuthUser
831 945 :param repoid: repository name or repository ID.
832 946 :type repoid: str or int
833 947 :param repo_name: Update the |repo| name, including the
834 948 repository group it's in.
835 949 :type repo_name: str
836 950 :param owner: Set the |repo| owner.
837 951 :type owner: str
838 952 :param fork_of: Set the |repo| as fork of another |repo|.
839 953 :type fork_of: str
840 954 :param description: Update the |repo| description.
841 955 :type description: str
842 956 :param private: Set the |repo| as private. (True | False)
843 957 :type private: bool
844 958 :param clone_uri: Update the |repo| clone URI.
845 959 :type clone_uri: str
846 960 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
847 961 :type landing_rev: str
848 962 :param enable_statistics: Enable statistics on the |repo|, (True | False).
849 963 :type enable_statistics: bool
850 964 :param enable_locking: Enable |repo| locking.
851 965 :type enable_locking: bool
852 966 :param enable_downloads: Enable downloads from the |repo|, (True | False).
853 967 :type enable_downloads: bool
854 968 :param fields: Add extra fields to the |repo|. Use the following
855 969 example format: ``field_key=field_val,field_key2=fieldval2``.
856 970 Escape ', ' with \,
857 971 :type fields: str
858 972 """
859 973
860 974 repo = get_repo_or_error(repoid)
861 975
862 976 include_secrets = False
863 977 if not has_superadmin_permission(apiuser):
864 978 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
865 979 else:
866 980 include_secrets = True
867 981
868 982 updates = dict(
869 983 repo_name=repo_name
870 984 if not isinstance(repo_name, Optional) else repo.repo_name,
871 985
872 986 fork_id=fork_of
873 987 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
874 988
875 989 user=owner
876 990 if not isinstance(owner, Optional) else repo.user.username,
877 991
878 992 repo_description=description
879 993 if not isinstance(description, Optional) else repo.description,
880 994
881 995 repo_private=private
882 996 if not isinstance(private, Optional) else repo.private,
883 997
884 998 clone_uri=clone_uri
885 999 if not isinstance(clone_uri, Optional) else repo.clone_uri,
886 1000
887 1001 push_uri=push_uri
888 1002 if not isinstance(push_uri, Optional) else repo.push_uri,
889 1003
890 1004 repo_landing_rev=landing_rev
891 1005 if not isinstance(landing_rev, Optional) else repo._landing_revision,
892 1006
893 1007 repo_enable_statistics=enable_statistics
894 1008 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
895 1009
896 1010 repo_enable_locking=enable_locking
897 1011 if not isinstance(enable_locking, Optional) else repo.enable_locking,
898 1012
899 1013 repo_enable_downloads=enable_downloads
900 1014 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
901 1015
902 1016 ref_choices, _labels = ScmModel().get_repo_landing_revs(
903 1017 request.translate, repo=repo)
904 1018
905 1019 old_values = repo.get_api_data()
906 1020 repo_type = repo.repo_type
907 1021 schema = repo_schema.RepoSchema().bind(
908 1022 repo_type_options=rhodecode.BACKENDS.keys(),
909 1023 repo_ref_options=ref_choices,
910 1024 repo_type=repo_type,
911 1025 # user caller
912 1026 user=apiuser,
913 1027 old_values=old_values)
914 1028 try:
915 1029 schema_data = schema.deserialize(dict(
916 1030 # we save old value, users cannot change type
917 1031 repo_type=repo_type,
918 1032
919 1033 repo_name=updates['repo_name'],
920 1034 repo_owner=updates['user'],
921 1035 repo_description=updates['repo_description'],
922 1036 repo_clone_uri=updates['clone_uri'],
923 1037 repo_push_uri=updates['push_uri'],
924 1038 repo_fork_of=updates['fork_id'],
925 1039 repo_private=updates['repo_private'],
926 1040 repo_landing_commit_ref=updates['repo_landing_rev'],
927 1041 repo_enable_statistics=updates['repo_enable_statistics'],
928 1042 repo_enable_downloads=updates['repo_enable_downloads'],
929 1043 repo_enable_locking=updates['repo_enable_locking']))
930 1044 except validation_schema.Invalid as err:
931 1045 raise JSONRPCValidationError(colander_exc=err)
932 1046
933 1047 # save validated data back into the updates dict
934 1048 validated_updates = dict(
935 1049 repo_name=schema_data['repo_group']['repo_name_without_group'],
936 1050 repo_group=schema_data['repo_group']['repo_group_id'],
937 1051
938 1052 user=schema_data['repo_owner'],
939 1053 repo_description=schema_data['repo_description'],
940 1054 repo_private=schema_data['repo_private'],
941 1055 clone_uri=schema_data['repo_clone_uri'],
942 1056 push_uri=schema_data['repo_push_uri'],
943 1057 repo_landing_rev=schema_data['repo_landing_commit_ref'],
944 1058 repo_enable_statistics=schema_data['repo_enable_statistics'],
945 1059 repo_enable_locking=schema_data['repo_enable_locking'],
946 1060 repo_enable_downloads=schema_data['repo_enable_downloads'],
947 1061 )
948 1062
949 1063 if schema_data['repo_fork_of']:
950 1064 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
951 1065 validated_updates['fork_id'] = fork_repo.repo_id
952 1066
953 1067 # extra fields
954 1068 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
955 1069 if fields:
956 1070 validated_updates.update(fields)
957 1071
958 1072 try:
959 1073 RepoModel().update(repo, **validated_updates)
960 1074 audit_logger.store_api(
961 1075 'repo.edit', action_data={'old_data': old_values},
962 1076 user=apiuser, repo=repo)
963 1077 Session().commit()
964 1078 return {
965 1079 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
966 1080 'repository': repo.get_api_data(include_secrets=include_secrets)
967 1081 }
968 1082 except Exception:
969 1083 log.exception(
970 1084 u"Exception while trying to update the repository %s",
971 1085 repoid)
972 1086 raise JSONRPCError('failed to update repo `%s`' % repoid)
973 1087
974 1088
975 1089 @jsonrpc_method()
976 1090 def fork_repo(request, apiuser, repoid, fork_name,
977 1091 owner=Optional(OAttr('apiuser')),
978 1092 description=Optional(''),
979 1093 private=Optional(False),
980 1094 clone_uri=Optional(None),
981 1095 landing_rev=Optional('rev:tip'),
982 1096 copy_permissions=Optional(False)):
983 1097 """
984 1098 Creates a fork of the specified |repo|.
985 1099
986 1100 * If the fork_name contains "/", fork will be created inside
987 1101 a repository group or nested repository groups
988 1102
989 1103 For example "foo/bar/fork-repo" will create fork called "fork-repo"
990 1104 inside group "foo/bar". You have to have permissions to access and
991 1105 write to the last repository group ("bar" in this example)
992 1106
993 1107 This command can only be run using an |authtoken| with minimum
994 1108 read permissions of the forked repo, create fork permissions for an user.
995 1109
996 1110 :param apiuser: This is filled automatically from the |authtoken|.
997 1111 :type apiuser: AuthUser
998 1112 :param repoid: Set repository name or repository ID.
999 1113 :type repoid: str or int
1000 1114 :param fork_name: Set the fork name, including it's repository group membership.
1001 1115 :type fork_name: str
1002 1116 :param owner: Set the fork owner.
1003 1117 :type owner: str
1004 1118 :param description: Set the fork description.
1005 1119 :type description: str
1006 1120 :param copy_permissions: Copy permissions from parent |repo|. The
1007 1121 default is False.
1008 1122 :type copy_permissions: bool
1009 1123 :param private: Make the fork private. The default is False.
1010 1124 :type private: bool
1011 1125 :param landing_rev: Set the landing revision. The default is tip.
1012 1126
1013 1127 Example output:
1014 1128
1015 1129 .. code-block:: bash
1016 1130
1017 1131 id : <id_for_response>
1018 1132 api_key : "<api_key>"
1019 1133 args: {
1020 1134 "repoid" : "<reponame or repo_id>",
1021 1135 "fork_name": "<forkname>",
1022 1136 "owner": "<username or user_id = Optional(=apiuser)>",
1023 1137 "description": "<description>",
1024 1138 "copy_permissions": "<bool>",
1025 1139 "private": "<bool>",
1026 1140 "landing_rev": "<landing_rev>"
1027 1141 }
1028 1142
1029 1143 Example error output:
1030 1144
1031 1145 .. code-block:: bash
1032 1146
1033 1147 id : <id_given_in_input>
1034 1148 result: {
1035 1149 "msg": "Created fork of `<reponame>` as `<forkname>`",
1036 1150 "success": true,
1037 1151 "task": "<celery task id or None if done sync>"
1038 1152 }
1039 1153 error: null
1040 1154
1041 1155 """
1042 1156
1043 1157 repo = get_repo_or_error(repoid)
1044 1158 repo_name = repo.repo_name
1045 1159
1046 1160 if not has_superadmin_permission(apiuser):
1047 1161 # check if we have at least read permission for
1048 1162 # this repo that we fork !
1049 1163 _perms = (
1050 1164 'repository.admin', 'repository.write', 'repository.read')
1051 1165 validate_repo_permissions(apiuser, repoid, repo, _perms)
1052 1166
1053 1167 # check if the regular user has at least fork permissions as well
1054 1168 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1055 1169 raise JSONRPCForbidden()
1056 1170
1057 1171 # check if user can set owner parameter
1058 1172 owner = validate_set_owner_permissions(apiuser, owner)
1059 1173
1060 1174 description = Optional.extract(description)
1061 1175 copy_permissions = Optional.extract(copy_permissions)
1062 1176 clone_uri = Optional.extract(clone_uri)
1063 1177 landing_commit_ref = Optional.extract(landing_rev)
1064 1178 private = Optional.extract(private)
1065 1179
1066 1180 schema = repo_schema.RepoSchema().bind(
1067 1181 repo_type_options=rhodecode.BACKENDS.keys(),
1068 1182 repo_type=repo.repo_type,
1069 1183 # user caller
1070 1184 user=apiuser)
1071 1185
1072 1186 try:
1073 1187 schema_data = schema.deserialize(dict(
1074 1188 repo_name=fork_name,
1075 1189 repo_type=repo.repo_type,
1076 1190 repo_owner=owner.username,
1077 1191 repo_description=description,
1078 1192 repo_landing_commit_ref=landing_commit_ref,
1079 1193 repo_clone_uri=clone_uri,
1080 1194 repo_private=private,
1081 1195 repo_copy_permissions=copy_permissions))
1082 1196 except validation_schema.Invalid as err:
1083 1197 raise JSONRPCValidationError(colander_exc=err)
1084 1198
1085 1199 try:
1086 1200 data = {
1087 1201 'fork_parent_id': repo.repo_id,
1088 1202
1089 1203 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1090 1204 'repo_name_full': schema_data['repo_name'],
1091 1205 'repo_group': schema_data['repo_group']['repo_group_id'],
1092 1206 'repo_type': schema_data['repo_type'],
1093 1207 'description': schema_data['repo_description'],
1094 1208 'private': schema_data['repo_private'],
1095 1209 'copy_permissions': schema_data['repo_copy_permissions'],
1096 1210 'landing_rev': schema_data['repo_landing_commit_ref'],
1097 1211 }
1098 1212
1099 1213 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1100 1214 # no commit, it's done in RepoModel, or async via celery
1101 1215 task_id = get_task_id(task)
1102 1216
1103 1217 return {
1104 1218 'msg': 'Created fork of `%s` as `%s`' % (
1105 1219 repo.repo_name, schema_data['repo_name']),
1106 1220 'success': True, # cannot return the repo data here since fork
1107 1221 # can be done async
1108 1222 'task': task_id
1109 1223 }
1110 1224 except Exception:
1111 1225 log.exception(
1112 1226 u"Exception while trying to create fork %s",
1113 1227 schema_data['repo_name'])
1114 1228 raise JSONRPCError(
1115 1229 'failed to fork repository `%s` as `%s`' % (
1116 1230 repo_name, schema_data['repo_name']))
1117 1231
1118 1232
1119 1233 @jsonrpc_method()
1120 1234 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1121 1235 """
1122 1236 Deletes a repository.
1123 1237
1124 1238 * When the `forks` parameter is set it's possible to detach or delete
1125 1239 forks of deleted repository.
1126 1240
1127 1241 This command can only be run using an |authtoken| with admin
1128 1242 permissions on the |repo|.
1129 1243
1130 1244 :param apiuser: This is filled automatically from the |authtoken|.
1131 1245 :type apiuser: AuthUser
1132 1246 :param repoid: Set the repository name or repository ID.
1133 1247 :type repoid: str or int
1134 1248 :param forks: Set to `detach` or `delete` forks from the |repo|.
1135 1249 :type forks: Optional(str)
1136 1250
1137 1251 Example error output:
1138 1252
1139 1253 .. code-block:: bash
1140 1254
1141 1255 id : <id_given_in_input>
1142 1256 result: {
1143 1257 "msg": "Deleted repository `<reponame>`",
1144 1258 "success": true
1145 1259 }
1146 1260 error: null
1147 1261 """
1148 1262
1149 1263 repo = get_repo_or_error(repoid)
1150 1264 repo_name = repo.repo_name
1151 1265 if not has_superadmin_permission(apiuser):
1152 1266 _perms = ('repository.admin',)
1153 1267 validate_repo_permissions(apiuser, repoid, repo, _perms)
1154 1268
1155 1269 try:
1156 1270 handle_forks = Optional.extract(forks)
1157 1271 _forks_msg = ''
1158 1272 _forks = [f for f in repo.forks]
1159 1273 if handle_forks == 'detach':
1160 1274 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1161 1275 elif handle_forks == 'delete':
1162 1276 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1163 1277 elif _forks:
1164 1278 raise JSONRPCError(
1165 1279 'Cannot delete `%s` it still contains attached forks' %
1166 1280 (repo.repo_name,)
1167 1281 )
1168 1282 old_data = repo.get_api_data()
1169 1283 RepoModel().delete(repo, forks=forks)
1170 1284
1171 1285 repo = audit_logger.RepoWrap(repo_id=None,
1172 1286 repo_name=repo.repo_name)
1173 1287
1174 1288 audit_logger.store_api(
1175 1289 'repo.delete', action_data={'old_data': old_data},
1176 1290 user=apiuser, repo=repo)
1177 1291
1178 1292 ScmModel().mark_for_invalidation(repo_name, delete=True)
1179 1293 Session().commit()
1180 1294 return {
1181 1295 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1182 1296 'success': True
1183 1297 }
1184 1298 except Exception:
1185 1299 log.exception("Exception occurred while trying to delete repo")
1186 1300 raise JSONRPCError(
1187 1301 'failed to delete repository `%s`' % (repo_name,)
1188 1302 )
1189 1303
1190 1304
1191 1305 #TODO: marcink, change name ?
1192 1306 @jsonrpc_method()
1193 1307 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1194 1308 """
1195 1309 Invalidates the cache for the specified repository.
1196 1310
1197 1311 This command can only be run using an |authtoken| with admin rights to
1198 1312 the specified repository.
1199 1313
1200 1314 This command takes the following options:
1201 1315
1202 1316 :param apiuser: This is filled automatically from |authtoken|.
1203 1317 :type apiuser: AuthUser
1204 1318 :param repoid: Sets the repository name or repository ID.
1205 1319 :type repoid: str or int
1206 1320 :param delete_keys: This deletes the invalidated keys instead of
1207 1321 just flagging them.
1208 1322 :type delete_keys: Optional(``True`` | ``False``)
1209 1323
1210 1324 Example output:
1211 1325
1212 1326 .. code-block:: bash
1213 1327
1214 1328 id : <id_given_in_input>
1215 1329 result : {
1216 1330 'msg': Cache for repository `<repository name>` was invalidated,
1217 1331 'repository': <repository name>
1218 1332 }
1219 1333 error : null
1220 1334
1221 1335 Example error output:
1222 1336
1223 1337 .. code-block:: bash
1224 1338
1225 1339 id : <id_given_in_input>
1226 1340 result : null
1227 1341 error : {
1228 1342 'Error occurred during cache invalidation action'
1229 1343 }
1230 1344
1231 1345 """
1232 1346
1233 1347 repo = get_repo_or_error(repoid)
1234 1348 if not has_superadmin_permission(apiuser):
1235 1349 _perms = ('repository.admin', 'repository.write',)
1236 1350 validate_repo_permissions(apiuser, repoid, repo, _perms)
1237 1351
1238 1352 delete = Optional.extract(delete_keys)
1239 1353 try:
1240 1354 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1241 1355 return {
1242 1356 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1243 1357 'repository': repo.repo_name
1244 1358 }
1245 1359 except Exception:
1246 1360 log.exception(
1247 1361 "Exception occurred while trying to invalidate repo cache")
1248 1362 raise JSONRPCError(
1249 1363 'Error occurred during cache invalidation action'
1250 1364 )
1251 1365
1252 1366
1253 1367 #TODO: marcink, change name ?
1254 1368 @jsonrpc_method()
1255 1369 def lock(request, apiuser, repoid, locked=Optional(None),
1256 1370 userid=Optional(OAttr('apiuser'))):
1257 1371 """
1258 1372 Sets the lock state of the specified |repo| by the given user.
1259 1373 From more information, see :ref:`repo-locking`.
1260 1374
1261 1375 * If the ``userid`` option is not set, the repository is locked to the
1262 1376 user who called the method.
1263 1377 * If the ``locked`` parameter is not set, the current lock state of the
1264 1378 repository is displayed.
1265 1379
1266 1380 This command can only be run using an |authtoken| with admin rights to
1267 1381 the specified repository.
1268 1382
1269 1383 This command takes the following options:
1270 1384
1271 1385 :param apiuser: This is filled automatically from the |authtoken|.
1272 1386 :type apiuser: AuthUser
1273 1387 :param repoid: Sets the repository name or repository ID.
1274 1388 :type repoid: str or int
1275 1389 :param locked: Sets the lock state.
1276 1390 :type locked: Optional(``True`` | ``False``)
1277 1391 :param userid: Set the repository lock to this user.
1278 1392 :type userid: Optional(str or int)
1279 1393
1280 1394 Example error output:
1281 1395
1282 1396 .. code-block:: bash
1283 1397
1284 1398 id : <id_given_in_input>
1285 1399 result : {
1286 1400 'repo': '<reponame>',
1287 1401 'locked': <bool: lock state>,
1288 1402 'locked_since': <int: lock timestamp>,
1289 1403 'locked_by': <username of person who made the lock>,
1290 1404 'lock_reason': <str: reason for locking>,
1291 1405 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1292 1406 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1293 1407 or
1294 1408 'msg': 'Repo `<repository name>` not locked.'
1295 1409 or
1296 1410 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1297 1411 }
1298 1412 error : null
1299 1413
1300 1414 Example error output:
1301 1415
1302 1416 .. code-block:: bash
1303 1417
1304 1418 id : <id_given_in_input>
1305 1419 result : null
1306 1420 error : {
1307 1421 'Error occurred locking repository `<reponame>`'
1308 1422 }
1309 1423 """
1310 1424
1311 1425 repo = get_repo_or_error(repoid)
1312 1426 if not has_superadmin_permission(apiuser):
1313 1427 # check if we have at least write permission for this repo !
1314 1428 _perms = ('repository.admin', 'repository.write',)
1315 1429 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316 1430
1317 1431 # make sure normal user does not pass someone else userid,
1318 1432 # he is not allowed to do that
1319 1433 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1320 1434 raise JSONRPCError('userid is not the same as your user')
1321 1435
1322 1436 if isinstance(userid, Optional):
1323 1437 userid = apiuser.user_id
1324 1438
1325 1439 user = get_user_or_error(userid)
1326 1440
1327 1441 if isinstance(locked, Optional):
1328 1442 lockobj = repo.locked
1329 1443
1330 1444 if lockobj[0] is None:
1331 1445 _d = {
1332 1446 'repo': repo.repo_name,
1333 1447 'locked': False,
1334 1448 'locked_since': None,
1335 1449 'locked_by': None,
1336 1450 'lock_reason': None,
1337 1451 'lock_state_changed': False,
1338 1452 'msg': 'Repo `%s` not locked.' % repo.repo_name
1339 1453 }
1340 1454 return _d
1341 1455 else:
1342 1456 _user_id, _time, _reason = lockobj
1343 1457 lock_user = get_user_or_error(userid)
1344 1458 _d = {
1345 1459 'repo': repo.repo_name,
1346 1460 'locked': True,
1347 1461 'locked_since': _time,
1348 1462 'locked_by': lock_user.username,
1349 1463 'lock_reason': _reason,
1350 1464 'lock_state_changed': False,
1351 1465 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1352 1466 % (repo.repo_name, lock_user.username,
1353 1467 json.dumps(time_to_datetime(_time))))
1354 1468 }
1355 1469 return _d
1356 1470
1357 1471 # force locked state through a flag
1358 1472 else:
1359 1473 locked = str2bool(locked)
1360 1474 lock_reason = Repository.LOCK_API
1361 1475 try:
1362 1476 if locked:
1363 1477 lock_time = time.time()
1364 1478 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1365 1479 else:
1366 1480 lock_time = None
1367 1481 Repository.unlock(repo)
1368 1482 _d = {
1369 1483 'repo': repo.repo_name,
1370 1484 'locked': locked,
1371 1485 'locked_since': lock_time,
1372 1486 'locked_by': user.username,
1373 1487 'lock_reason': lock_reason,
1374 1488 'lock_state_changed': True,
1375 1489 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1376 1490 % (user.username, repo.repo_name, locked))
1377 1491 }
1378 1492 return _d
1379 1493 except Exception:
1380 1494 log.exception(
1381 1495 "Exception occurred while trying to lock repository")
1382 1496 raise JSONRPCError(
1383 1497 'Error occurred locking repository `%s`' % repo.repo_name
1384 1498 )
1385 1499
1386 1500
1387 1501 @jsonrpc_method()
1388 1502 def comment_commit(
1389 1503 request, apiuser, repoid, commit_id, message, status=Optional(None),
1390 1504 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1391 1505 resolves_comment_id=Optional(None),
1392 1506 userid=Optional(OAttr('apiuser'))):
1393 1507 """
1394 1508 Set a commit comment, and optionally change the status of the commit.
1395 1509
1396 1510 :param apiuser: This is filled automatically from the |authtoken|.
1397 1511 :type apiuser: AuthUser
1398 1512 :param repoid: Set the repository name or repository ID.
1399 1513 :type repoid: str or int
1400 1514 :param commit_id: Specify the commit_id for which to set a comment.
1401 1515 :type commit_id: str
1402 1516 :param message: The comment text.
1403 1517 :type message: str
1404 1518 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1405 1519 'approved', 'rejected', 'under_review'
1406 1520 :type status: str
1407 1521 :param comment_type: Comment type, one of: 'note', 'todo'
1408 1522 :type comment_type: Optional(str), default: 'note'
1409 1523 :param userid: Set the user name of the comment creator.
1410 1524 :type userid: Optional(str or int)
1411 1525
1412 1526 Example error output:
1413 1527
1414 1528 .. code-block:: bash
1415 1529
1416 1530 {
1417 1531 "id" : <id_given_in_input>,
1418 1532 "result" : {
1419 1533 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1420 1534 "status_change": null or <status>,
1421 1535 "success": true
1422 1536 },
1423 1537 "error" : null
1424 1538 }
1425 1539
1426 1540 """
1427 1541 repo = get_repo_or_error(repoid)
1428 1542 if not has_superadmin_permission(apiuser):
1429 1543 _perms = ('repository.read', 'repository.write', 'repository.admin')
1430 1544 validate_repo_permissions(apiuser, repoid, repo, _perms)
1431 1545
1432 1546 try:
1433 1547 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1434 1548 except Exception as e:
1435 1549 log.exception('Failed to fetch commit')
1436 1550 raise JSONRPCError(safe_str(e))
1437 1551
1438 1552 if isinstance(userid, Optional):
1439 1553 userid = apiuser.user_id
1440 1554
1441 1555 user = get_user_or_error(userid)
1442 1556 status = Optional.extract(status)
1443 1557 comment_type = Optional.extract(comment_type)
1444 1558 resolves_comment_id = Optional.extract(resolves_comment_id)
1445 1559
1446 1560 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1447 1561 if status and status not in allowed_statuses:
1448 1562 raise JSONRPCError('Bad status, must be on '
1449 1563 'of %s got %s' % (allowed_statuses, status,))
1450 1564
1451 1565 if resolves_comment_id:
1452 1566 comment = ChangesetComment.get(resolves_comment_id)
1453 1567 if not comment:
1454 1568 raise JSONRPCError(
1455 1569 'Invalid resolves_comment_id `%s` for this commit.'
1456 1570 % resolves_comment_id)
1457 1571 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1458 1572 raise JSONRPCError(
1459 1573 'Comment `%s` is wrong type for setting status to resolved.'
1460 1574 % resolves_comment_id)
1461 1575
1462 1576 try:
1463 1577 rc_config = SettingsModel().get_all_settings()
1464 1578 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1465 1579 status_change_label = ChangesetStatus.get_status_lbl(status)
1466 1580 comment = CommentsModel().create(
1467 1581 message, repo, user, commit_id=commit_id,
1468 1582 status_change=status_change_label,
1469 1583 status_change_type=status,
1470 1584 renderer=renderer,
1471 1585 comment_type=comment_type,
1472 1586 resolves_comment_id=resolves_comment_id,
1473 1587 auth_user=apiuser
1474 1588 )
1475 1589 if status:
1476 1590 # also do a status change
1477 1591 try:
1478 1592 ChangesetStatusModel().set_status(
1479 1593 repo, status, user, comment, revision=commit_id,
1480 1594 dont_allow_on_closed_pull_request=True
1481 1595 )
1482 1596 except StatusChangeOnClosedPullRequestError:
1483 1597 log.exception(
1484 1598 "Exception occurred while trying to change repo commit status")
1485 1599 msg = ('Changing status on a changeset associated with '
1486 1600 'a closed pull request is not allowed')
1487 1601 raise JSONRPCError(msg)
1488 1602
1489 1603 Session().commit()
1490 1604 return {
1491 1605 'msg': (
1492 1606 'Commented on commit `%s` for repository `%s`' % (
1493 1607 comment.revision, repo.repo_name)),
1494 1608 'status_change': status,
1495 1609 'success': True,
1496 1610 }
1497 1611 except JSONRPCError:
1498 1612 # catch any inside errors, and re-raise them to prevent from
1499 1613 # below global catch to silence them
1500 1614 raise
1501 1615 except Exception:
1502 1616 log.exception("Exception occurred while trying to comment on commit")
1503 1617 raise JSONRPCError(
1504 1618 'failed to set comment on repository `%s`' % (repo.repo_name,)
1505 1619 )
1506 1620
1507 1621
1508 1622 @jsonrpc_method()
1509 1623 def get_repo_comments(request, apiuser, repoid,
1510 1624 commit_id=Optional(None), comment_type=Optional(None),
1511 1625 userid=Optional(None)):
1512 1626 """
1513 1627 Get all comments for a repository
1514 1628
1515 1629 :param apiuser: This is filled automatically from the |authtoken|.
1516 1630 :type apiuser: AuthUser
1517 1631 :param repoid: Set the repository name or repository ID.
1518 1632 :type repoid: str or int
1519 1633 :param commit_id: Optionally filter the comments by the commit_id
1520 1634 :type commit_id: Optional(str), default: None
1521 1635 :param comment_type: Optionally filter the comments by the comment_type
1522 1636 one of: 'note', 'todo'
1523 1637 :type comment_type: Optional(str), default: None
1524 1638 :param userid: Optionally filter the comments by the author of comment
1525 1639 :type userid: Optional(str or int), Default: None
1526 1640
1527 1641 Example error output:
1528 1642
1529 1643 .. code-block:: bash
1530 1644
1531 1645 {
1532 1646 "id" : <id_given_in_input>,
1533 1647 "result" : [
1534 1648 {
1535 1649 "comment_author": <USER_DETAILS>,
1536 1650 "comment_created_on": "2017-02-01T14:38:16.309",
1537 1651 "comment_f_path": "file.txt",
1538 1652 "comment_id": 282,
1539 1653 "comment_lineno": "n1",
1540 1654 "comment_resolved_by": null,
1541 1655 "comment_status": [],
1542 1656 "comment_text": "This file needs a header",
1543 1657 "comment_type": "todo"
1544 1658 }
1545 1659 ],
1546 1660 "error" : null
1547 1661 }
1548 1662
1549 1663 """
1550 1664 repo = get_repo_or_error(repoid)
1551 1665 if not has_superadmin_permission(apiuser):
1552 1666 _perms = ('repository.read', 'repository.write', 'repository.admin')
1553 1667 validate_repo_permissions(apiuser, repoid, repo, _perms)
1554 1668
1555 1669 commit_id = Optional.extract(commit_id)
1556 1670
1557 1671 userid = Optional.extract(userid)
1558 1672 if userid:
1559 1673 user = get_user_or_error(userid)
1560 1674 else:
1561 1675 user = None
1562 1676
1563 1677 comment_type = Optional.extract(comment_type)
1564 1678 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1565 1679 raise JSONRPCError(
1566 1680 'comment_type must be one of `{}` got {}'.format(
1567 1681 ChangesetComment.COMMENT_TYPES, comment_type)
1568 1682 )
1569 1683
1570 1684 comments = CommentsModel().get_repository_comments(
1571 1685 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1572 1686 return comments
1573 1687
1574 1688
1575 1689 @jsonrpc_method()
1576 1690 def grant_user_permission(request, apiuser, repoid, userid, perm):
1577 1691 """
1578 1692 Grant permissions for the specified user on the given repository,
1579 1693 or update existing permissions if found.
1580 1694
1581 1695 This command can only be run using an |authtoken| with admin
1582 1696 permissions on the |repo|.
1583 1697
1584 1698 :param apiuser: This is filled automatically from the |authtoken|.
1585 1699 :type apiuser: AuthUser
1586 1700 :param repoid: Set the repository name or repository ID.
1587 1701 :type repoid: str or int
1588 1702 :param userid: Set the user name.
1589 1703 :type userid: str
1590 1704 :param perm: Set the user permissions, using the following format
1591 1705 ``(repository.(none|read|write|admin))``
1592 1706 :type perm: str
1593 1707
1594 1708 Example output:
1595 1709
1596 1710 .. code-block:: bash
1597 1711
1598 1712 id : <id_given_in_input>
1599 1713 result: {
1600 1714 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1601 1715 "success": true
1602 1716 }
1603 1717 error: null
1604 1718 """
1605 1719
1606 1720 repo = get_repo_or_error(repoid)
1607 1721 user = get_user_or_error(userid)
1608 1722 perm = get_perm_or_error(perm)
1609 1723 if not has_superadmin_permission(apiuser):
1610 1724 _perms = ('repository.admin',)
1611 1725 validate_repo_permissions(apiuser, repoid, repo, _perms)
1612 1726
1613 1727 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1614 1728 try:
1615 1729 changes = RepoModel().update_permissions(
1616 1730 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1617 1731
1618 1732 action_data = {
1619 1733 'added': changes['added'],
1620 1734 'updated': changes['updated'],
1621 1735 'deleted': changes['deleted'],
1622 1736 }
1623 1737 audit_logger.store_api(
1624 1738 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1625 1739
1626 1740 Session().commit()
1627 1741 return {
1628 1742 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1629 1743 perm.permission_name, user.username, repo.repo_name
1630 1744 ),
1631 1745 'success': True
1632 1746 }
1633 1747 except Exception:
1634 1748 log.exception("Exception occurred while trying edit permissions for repo")
1635 1749 raise JSONRPCError(
1636 1750 'failed to edit permission for user: `%s` in repo: `%s`' % (
1637 1751 userid, repoid
1638 1752 )
1639 1753 )
1640 1754
1641 1755
1642 1756 @jsonrpc_method()
1643 1757 def revoke_user_permission(request, apiuser, repoid, userid):
1644 1758 """
1645 1759 Revoke permission for a user on the specified repository.
1646 1760
1647 1761 This command can only be run using an |authtoken| with admin
1648 1762 permissions on the |repo|.
1649 1763
1650 1764 :param apiuser: This is filled automatically from the |authtoken|.
1651 1765 :type apiuser: AuthUser
1652 1766 :param repoid: Set the repository name or repository ID.
1653 1767 :type repoid: str or int
1654 1768 :param userid: Set the user name of revoked user.
1655 1769 :type userid: str or int
1656 1770
1657 1771 Example error output:
1658 1772
1659 1773 .. code-block:: bash
1660 1774
1661 1775 id : <id_given_in_input>
1662 1776 result: {
1663 1777 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1664 1778 "success": true
1665 1779 }
1666 1780 error: null
1667 1781 """
1668 1782
1669 1783 repo = get_repo_or_error(repoid)
1670 1784 user = get_user_or_error(userid)
1671 1785 if not has_superadmin_permission(apiuser):
1672 1786 _perms = ('repository.admin',)
1673 1787 validate_repo_permissions(apiuser, repoid, repo, _perms)
1674 1788
1675 1789 perm_deletions = [[user.user_id, None, "user"]]
1676 1790 try:
1677 1791 changes = RepoModel().update_permissions(
1678 1792 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1679 1793
1680 1794 action_data = {
1681 1795 'added': changes['added'],
1682 1796 'updated': changes['updated'],
1683 1797 'deleted': changes['deleted'],
1684 1798 }
1685 1799 audit_logger.store_api(
1686 1800 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1687 1801
1688 1802 Session().commit()
1689 1803 return {
1690 1804 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1691 1805 user.username, repo.repo_name
1692 1806 ),
1693 1807 'success': True
1694 1808 }
1695 1809 except Exception:
1696 1810 log.exception("Exception occurred while trying revoke permissions to repo")
1697 1811 raise JSONRPCError(
1698 1812 'failed to edit permission for user: `%s` in repo: `%s`' % (
1699 1813 userid, repoid
1700 1814 )
1701 1815 )
1702 1816
1703 1817
1704 1818 @jsonrpc_method()
1705 1819 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1706 1820 """
1707 1821 Grant permission for a user group on the specified repository,
1708 1822 or update existing permissions.
1709 1823
1710 1824 This command can only be run using an |authtoken| with admin
1711 1825 permissions on the |repo|.
1712 1826
1713 1827 :param apiuser: This is filled automatically from the |authtoken|.
1714 1828 :type apiuser: AuthUser
1715 1829 :param repoid: Set the repository name or repository ID.
1716 1830 :type repoid: str or int
1717 1831 :param usergroupid: Specify the ID of the user group.
1718 1832 :type usergroupid: str or int
1719 1833 :param perm: Set the user group permissions using the following
1720 1834 format: (repository.(none|read|write|admin))
1721 1835 :type perm: str
1722 1836
1723 1837 Example output:
1724 1838
1725 1839 .. code-block:: bash
1726 1840
1727 1841 id : <id_given_in_input>
1728 1842 result : {
1729 1843 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1730 1844 "success": true
1731 1845
1732 1846 }
1733 1847 error : null
1734 1848
1735 1849 Example error output:
1736 1850
1737 1851 .. code-block:: bash
1738 1852
1739 1853 id : <id_given_in_input>
1740 1854 result : null
1741 1855 error : {
1742 1856 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1743 1857 }
1744 1858
1745 1859 """
1746 1860
1747 1861 repo = get_repo_or_error(repoid)
1748 1862 perm = get_perm_or_error(perm)
1749 1863 if not has_superadmin_permission(apiuser):
1750 1864 _perms = ('repository.admin',)
1751 1865 validate_repo_permissions(apiuser, repoid, repo, _perms)
1752 1866
1753 1867 user_group = get_user_group_or_error(usergroupid)
1754 1868 if not has_superadmin_permission(apiuser):
1755 1869 # check if we have at least read permission for this user group !
1756 1870 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1757 1871 if not HasUserGroupPermissionAnyApi(*_perms)(
1758 1872 user=apiuser, user_group_name=user_group.users_group_name):
1759 1873 raise JSONRPCError(
1760 1874 'user group `%s` does not exist' % (usergroupid,))
1761 1875
1762 1876 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1763 1877 try:
1764 1878 changes = RepoModel().update_permissions(
1765 1879 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1766 1880 action_data = {
1767 1881 'added': changes['added'],
1768 1882 'updated': changes['updated'],
1769 1883 'deleted': changes['deleted'],
1770 1884 }
1771 1885 audit_logger.store_api(
1772 1886 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1773 1887
1774 1888 Session().commit()
1775 1889 return {
1776 1890 'msg': 'Granted perm: `%s` for user group: `%s` in '
1777 1891 'repo: `%s`' % (
1778 1892 perm.permission_name, user_group.users_group_name,
1779 1893 repo.repo_name
1780 1894 ),
1781 1895 'success': True
1782 1896 }
1783 1897 except Exception:
1784 1898 log.exception(
1785 1899 "Exception occurred while trying change permission on repo")
1786 1900 raise JSONRPCError(
1787 1901 'failed to edit permission for user group: `%s` in '
1788 1902 'repo: `%s`' % (
1789 1903 usergroupid, repo.repo_name
1790 1904 )
1791 1905 )
1792 1906
1793 1907
1794 1908 @jsonrpc_method()
1795 1909 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1796 1910 """
1797 1911 Revoke the permissions of a user group on a given repository.
1798 1912
1799 1913 This command can only be run using an |authtoken| with admin
1800 1914 permissions on the |repo|.
1801 1915
1802 1916 :param apiuser: This is filled automatically from the |authtoken|.
1803 1917 :type apiuser: AuthUser
1804 1918 :param repoid: Set the repository name or repository ID.
1805 1919 :type repoid: str or int
1806 1920 :param usergroupid: Specify the user group ID.
1807 1921 :type usergroupid: str or int
1808 1922
1809 1923 Example output:
1810 1924
1811 1925 .. code-block:: bash
1812 1926
1813 1927 id : <id_given_in_input>
1814 1928 result: {
1815 1929 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1816 1930 "success": true
1817 1931 }
1818 1932 error: null
1819 1933 """
1820 1934
1821 1935 repo = get_repo_or_error(repoid)
1822 1936 if not has_superadmin_permission(apiuser):
1823 1937 _perms = ('repository.admin',)
1824 1938 validate_repo_permissions(apiuser, repoid, repo, _perms)
1825 1939
1826 1940 user_group = get_user_group_or_error(usergroupid)
1827 1941 if not has_superadmin_permission(apiuser):
1828 1942 # check if we have at least read permission for this user group !
1829 1943 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1830 1944 if not HasUserGroupPermissionAnyApi(*_perms)(
1831 1945 user=apiuser, user_group_name=user_group.users_group_name):
1832 1946 raise JSONRPCError(
1833 1947 'user group `%s` does not exist' % (usergroupid,))
1834 1948
1835 1949 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1836 1950 try:
1837 1951 changes = RepoModel().update_permissions(
1838 1952 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1839 1953 action_data = {
1840 1954 'added': changes['added'],
1841 1955 'updated': changes['updated'],
1842 1956 'deleted': changes['deleted'],
1843 1957 }
1844 1958 audit_logger.store_api(
1845 1959 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1846 1960
1847 1961 Session().commit()
1848 1962 return {
1849 1963 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1850 1964 user_group.users_group_name, repo.repo_name
1851 1965 ),
1852 1966 'success': True
1853 1967 }
1854 1968 except Exception:
1855 1969 log.exception("Exception occurred while trying revoke "
1856 1970 "user group permission on repo")
1857 1971 raise JSONRPCError(
1858 1972 'failed to edit permission for user group: `%s` in '
1859 1973 'repo: `%s`' % (
1860 1974 user_group.users_group_name, repo.repo_name
1861 1975 )
1862 1976 )
1863 1977
1864 1978
1865 1979 @jsonrpc_method()
1866 1980 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1867 1981 """
1868 1982 Triggers a pull on the given repository from a remote location. You
1869 1983 can use this to keep remote repositories up-to-date.
1870 1984
1871 1985 This command can only be run using an |authtoken| with admin
1872 1986 rights to the specified repository. For more information,
1873 1987 see :ref:`config-token-ref`.
1874 1988
1875 1989 This command takes the following options:
1876 1990
1877 1991 :param apiuser: This is filled automatically from the |authtoken|.
1878 1992 :type apiuser: AuthUser
1879 1993 :param repoid: The repository name or repository ID.
1880 1994 :type repoid: str or int
1881 1995 :param remote_uri: Optional remote URI to pass in for pull
1882 1996 :type remote_uri: str
1883 1997
1884 1998 Example output:
1885 1999
1886 2000 .. code-block:: bash
1887 2001
1888 2002 id : <id_given_in_input>
1889 2003 result : {
1890 2004 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1891 2005 "repository": "<repository name>"
1892 2006 }
1893 2007 error : null
1894 2008
1895 2009 Example error output:
1896 2010
1897 2011 .. code-block:: bash
1898 2012
1899 2013 id : <id_given_in_input>
1900 2014 result : null
1901 2015 error : {
1902 2016 "Unable to push changes from `<remote_url>`"
1903 2017 }
1904 2018
1905 2019 """
1906 2020
1907 2021 repo = get_repo_or_error(repoid)
1908 2022 remote_uri = Optional.extract(remote_uri)
1909 2023 remote_uri_display = remote_uri or repo.clone_uri_hidden
1910 2024 if not has_superadmin_permission(apiuser):
1911 2025 _perms = ('repository.admin',)
1912 2026 validate_repo_permissions(apiuser, repoid, repo, _perms)
1913 2027
1914 2028 try:
1915 2029 ScmModel().pull_changes(
1916 2030 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1917 2031 return {
1918 2032 'msg': 'Pulled from url `%s` on repo `%s`' % (
1919 2033 remote_uri_display, repo.repo_name),
1920 2034 'repository': repo.repo_name
1921 2035 }
1922 2036 except Exception:
1923 2037 log.exception("Exception occurred while trying to "
1924 2038 "pull changes from remote location")
1925 2039 raise JSONRPCError(
1926 2040 'Unable to pull changes from `%s`' % remote_uri_display
1927 2041 )
1928 2042
1929 2043
1930 2044 @jsonrpc_method()
1931 2045 def strip(request, apiuser, repoid, revision, branch):
1932 2046 """
1933 2047 Strips the given revision from the specified repository.
1934 2048
1935 2049 * This will remove the revision and all of its decendants.
1936 2050
1937 2051 This command can only be run using an |authtoken| with admin rights to
1938 2052 the specified repository.
1939 2053
1940 2054 This command takes the following options:
1941 2055
1942 2056 :param apiuser: This is filled automatically from the |authtoken|.
1943 2057 :type apiuser: AuthUser
1944 2058 :param repoid: The repository name or repository ID.
1945 2059 :type repoid: str or int
1946 2060 :param revision: The revision you wish to strip.
1947 2061 :type revision: str
1948 2062 :param branch: The branch from which to strip the revision.
1949 2063 :type branch: str
1950 2064
1951 2065 Example output:
1952 2066
1953 2067 .. code-block:: bash
1954 2068
1955 2069 id : <id_given_in_input>
1956 2070 result : {
1957 2071 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1958 2072 "repository": "<repository name>"
1959 2073 }
1960 2074 error : null
1961 2075
1962 2076 Example error output:
1963 2077
1964 2078 .. code-block:: bash
1965 2079
1966 2080 id : <id_given_in_input>
1967 2081 result : null
1968 2082 error : {
1969 2083 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1970 2084 }
1971 2085
1972 2086 """
1973 2087
1974 2088 repo = get_repo_or_error(repoid)
1975 2089 if not has_superadmin_permission(apiuser):
1976 2090 _perms = ('repository.admin',)
1977 2091 validate_repo_permissions(apiuser, repoid, repo, _perms)
1978 2092
1979 2093 try:
1980 2094 ScmModel().strip(repo, revision, branch)
1981 2095 audit_logger.store_api(
1982 2096 'repo.commit.strip', action_data={'commit_id': revision},
1983 2097 repo=repo,
1984 2098 user=apiuser, commit=True)
1985 2099
1986 2100 return {
1987 2101 'msg': 'Stripped commit %s from repo `%s`' % (
1988 2102 revision, repo.repo_name),
1989 2103 'repository': repo.repo_name
1990 2104 }
1991 2105 except Exception:
1992 2106 log.exception("Exception while trying to strip")
1993 2107 raise JSONRPCError(
1994 2108 'Unable to strip commit %s from repo `%s`' % (
1995 2109 revision, repo.repo_name)
1996 2110 )
1997 2111
1998 2112
1999 2113 @jsonrpc_method()
2000 2114 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2001 2115 """
2002 2116 Returns all settings for a repository. If key is given it only returns the
2003 2117 setting identified by the key or null.
2004 2118
2005 2119 :param apiuser: This is filled automatically from the |authtoken|.
2006 2120 :type apiuser: AuthUser
2007 2121 :param repoid: The repository name or repository id.
2008 2122 :type repoid: str or int
2009 2123 :param key: Key of the setting to return.
2010 2124 :type: key: Optional(str)
2011 2125
2012 2126 Example output:
2013 2127
2014 2128 .. code-block:: bash
2015 2129
2016 2130 {
2017 2131 "error": null,
2018 2132 "id": 237,
2019 2133 "result": {
2020 2134 "extensions_largefiles": true,
2021 2135 "extensions_evolve": true,
2022 2136 "hooks_changegroup_push_logger": true,
2023 2137 "hooks_changegroup_repo_size": false,
2024 2138 "hooks_outgoing_pull_logger": true,
2025 2139 "phases_publish": "True",
2026 2140 "rhodecode_hg_use_rebase_for_merging": true,
2027 2141 "rhodecode_pr_merge_enabled": true,
2028 2142 "rhodecode_use_outdated_comments": true
2029 2143 }
2030 2144 }
2031 2145 """
2032 2146
2033 2147 # Restrict access to this api method to admins only.
2034 2148 if not has_superadmin_permission(apiuser):
2035 2149 raise JSONRPCForbidden()
2036 2150
2037 2151 try:
2038 2152 repo = get_repo_or_error(repoid)
2039 2153 settings_model = VcsSettingsModel(repo=repo)
2040 2154 settings = settings_model.get_global_settings()
2041 2155 settings.update(settings_model.get_repo_settings())
2042 2156
2043 2157 # If only a single setting is requested fetch it from all settings.
2044 2158 key = Optional.extract(key)
2045 2159 if key is not None:
2046 2160 settings = settings.get(key, None)
2047 2161 except Exception:
2048 2162 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2049 2163 log.exception(msg)
2050 2164 raise JSONRPCError(msg)
2051 2165
2052 2166 return settings
2053 2167
2054 2168
2055 2169 @jsonrpc_method()
2056 2170 def set_repo_settings(request, apiuser, repoid, settings):
2057 2171 """
2058 2172 Update repository settings. Returns true on success.
2059 2173
2060 2174 :param apiuser: This is filled automatically from the |authtoken|.
2061 2175 :type apiuser: AuthUser
2062 2176 :param repoid: The repository name or repository id.
2063 2177 :type repoid: str or int
2064 2178 :param settings: The new settings for the repository.
2065 2179 :type: settings: dict
2066 2180
2067 2181 Example output:
2068 2182
2069 2183 .. code-block:: bash
2070 2184
2071 2185 {
2072 2186 "error": null,
2073 2187 "id": 237,
2074 2188 "result": true
2075 2189 }
2076 2190 """
2077 2191 # Restrict access to this api method to admins only.
2078 2192 if not has_superadmin_permission(apiuser):
2079 2193 raise JSONRPCForbidden()
2080 2194
2081 2195 if type(settings) is not dict:
2082 2196 raise JSONRPCError('Settings have to be a JSON Object.')
2083 2197
2084 2198 try:
2085 2199 settings_model = VcsSettingsModel(repo=repoid)
2086 2200
2087 2201 # Merge global, repo and incoming settings.
2088 2202 new_settings = settings_model.get_global_settings()
2089 2203 new_settings.update(settings_model.get_repo_settings())
2090 2204 new_settings.update(settings)
2091 2205
2092 2206 # Update the settings.
2093 2207 inherit_global_settings = new_settings.get(
2094 2208 'inherit_global_settings', False)
2095 2209 settings_model.create_or_update_repo_settings(
2096 2210 new_settings, inherit_global_settings=inherit_global_settings)
2097 2211 Session().commit()
2098 2212 except Exception:
2099 2213 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2100 2214 log.exception(msg)
2101 2215 raise JSONRPCError(msg)
2102 2216
2103 2217 # Indicate success.
2104 2218 return True
2105 2219
2106 2220
2107 2221 @jsonrpc_method()
2108 2222 def maintenance(request, apiuser, repoid):
2109 2223 """
2110 2224 Triggers a maintenance on the given repository.
2111 2225
2112 2226 This command can only be run using an |authtoken| with admin
2113 2227 rights to the specified repository. For more information,
2114 2228 see :ref:`config-token-ref`.
2115 2229
2116 2230 This command takes the following options:
2117 2231
2118 2232 :param apiuser: This is filled automatically from the |authtoken|.
2119 2233 :type apiuser: AuthUser
2120 2234 :param repoid: The repository name or repository ID.
2121 2235 :type repoid: str or int
2122 2236
2123 2237 Example output:
2124 2238
2125 2239 .. code-block:: bash
2126 2240
2127 2241 id : <id_given_in_input>
2128 2242 result : {
2129 2243 "msg": "executed maintenance command",
2130 2244 "executed_actions": [
2131 2245 <action_message>, <action_message2>...
2132 2246 ],
2133 2247 "repository": "<repository name>"
2134 2248 }
2135 2249 error : null
2136 2250
2137 2251 Example error output:
2138 2252
2139 2253 .. code-block:: bash
2140 2254
2141 2255 id : <id_given_in_input>
2142 2256 result : null
2143 2257 error : {
2144 2258 "Unable to execute maintenance on `<reponame>`"
2145 2259 }
2146 2260
2147 2261 """
2148 2262
2149 2263 repo = get_repo_or_error(repoid)
2150 2264 if not has_superadmin_permission(apiuser):
2151 2265 _perms = ('repository.admin',)
2152 2266 validate_repo_permissions(apiuser, repoid, repo, _perms)
2153 2267
2154 2268 try:
2155 2269 maintenance = repo_maintenance.RepoMaintenance()
2156 2270 executed_actions = maintenance.execute(repo)
2157 2271
2158 2272 return {
2159 2273 'msg': 'executed maintenance command',
2160 2274 'executed_actions': executed_actions,
2161 2275 'repository': repo.repo_name
2162 2276 }
2163 2277 except Exception:
2164 2278 log.exception("Exception occurred while trying to run maintenance")
2165 2279 raise JSONRPCError(
2166 2280 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,821 +1,837 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-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 Module holding everything related to vcs nodes, with vcs2 architecture.
23 23 """
24 24
25 25 import os
26 26 import stat
27 27
28 28 from zope.cachedescriptors.property import Lazy as LazyProperty
29 29
30 30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 31 from rhodecode.lib.utils import safe_unicode, safe_str
32 32 from rhodecode.lib.utils2 import md5
33 33 from rhodecode.lib.vcs import path as vcspath
34 34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37 37
38 38 LARGEFILE_PREFIX = '.hglf'
39 39
40 40
41 41 class NodeKind:
42 42 SUBMODULE = -1
43 43 DIR = 1
44 44 FILE = 2
45 45 LARGEFILE = 3
46 46
47 47
48 48 class NodeState:
49 49 ADDED = u'added'
50 50 CHANGED = u'changed'
51 51 NOT_CHANGED = u'not changed'
52 52 REMOVED = u'removed'
53 53
54 54
55 55 class NodeGeneratorBase(object):
56 56 """
57 57 Base class for removed added and changed filenodes, it's a lazy generator
58 58 class that will create filenodes only on iteration or call
59 59
60 60 The len method doesn't need to create filenodes at all
61 61 """
62 62
63 63 def __init__(self, current_paths, cs):
64 64 self.cs = cs
65 65 self.current_paths = current_paths
66 66
67 67 def __call__(self):
68 68 return [n for n in self]
69 69
70 70 def __getslice__(self, i, j):
71 71 for p in self.current_paths[i:j]:
72 72 yield self.cs.get_node(p)
73 73
74 74 def __len__(self):
75 75 return len(self.current_paths)
76 76
77 77 def __iter__(self):
78 78 for p in self.current_paths:
79 79 yield self.cs.get_node(p)
80 80
81 81
82 82 class AddedFileNodesGenerator(NodeGeneratorBase):
83 83 """
84 84 Class holding added files for current commit
85 85 """
86 86
87 87
88 88 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 89 """
90 90 Class holding changed files for current commit
91 91 """
92 92
93 93
94 94 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 95 """
96 96 Class holding removed files for current commit
97 97 """
98 98 def __iter__(self):
99 99 for p in self.current_paths:
100 100 yield RemovedFileNode(path=p)
101 101
102 102 def __getslice__(self, i, j):
103 103 for p in self.current_paths[i:j]:
104 104 yield RemovedFileNode(path=p)
105 105
106 106
107 107 class Node(object):
108 108 """
109 109 Simplest class representing file or directory on repository. SCM backends
110 110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 111 directly.
112 112
113 113 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 114 only. Moreover, every single node is identified by the ``path`` attribute,
115 115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 116 """
117 117 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
118 118 # security attacks could be used with this
119 119 commit = None
120 120
121 121 def __init__(self, path, kind):
122 122 self._validate_path(path) # can throw exception if path is invalid
123 123 self.path = safe_str(path.rstrip('/')) # we store paths as str
124 124 if path == '' and kind != NodeKind.DIR:
125 125 raise NodeError("Only DirNode and its subclasses may be "
126 126 "initialized with empty path")
127 127 self.kind = kind
128 128
129 129 if self.is_root() and not self.is_dir():
130 130 raise NodeError("Root node cannot be FILE kind")
131 131
132 132 def _validate_path(self, path):
133 133 if path.startswith('/'):
134 134 raise NodeError(
135 135 "Cannot initialize Node objects with slash at "
136 136 "the beginning as only relative paths are supported. "
137 137 "Got %s" % (path,))
138 138
139 139 @LazyProperty
140 140 def parent(self):
141 141 parent_path = self.get_parent_path()
142 142 if parent_path:
143 143 if self.commit:
144 144 return self.commit.get_node(parent_path)
145 145 return DirNode(parent_path)
146 146 return None
147 147
148 148 @LazyProperty
149 149 def unicode_path(self):
150 150 return safe_unicode(self.path)
151 151
152 152 @LazyProperty
153 153 def has_rtlo(self):
154 154 """Detects if a path has right-to-left-override marker"""
155 155 return self.RTLO_MARKER in self.unicode_path
156 156
157 157 @LazyProperty
158 158 def unicode_path_safe(self):
159 159 """
160 160 Special SAFE representation of path without the right-to-left-override.
161 161 This should be only used for "showing" the file, cannot be used for any
162 162 urls etc.
163 163 """
164 164 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
165 165
166 166 @LazyProperty
167 167 def dir_path(self):
168 168 """
169 169 Returns name of the directory from full path of this vcs node. Empty
170 170 string is returned if there's no directory in the path
171 171 """
172 172 _parts = self.path.rstrip('/').rsplit('/', 1)
173 173 if len(_parts) == 2:
174 174 return safe_unicode(_parts[0])
175 175 return u''
176 176
177 177 @LazyProperty
178 178 def name(self):
179 179 """
180 180 Returns name of the node so if its path
181 181 then only last part is returned.
182 182 """
183 183 return safe_unicode(self.path.rstrip('/').split('/')[-1])
184 184
185 185 @property
186 186 def kind(self):
187 187 return self._kind
188 188
189 189 @kind.setter
190 190 def kind(self, kind):
191 191 if hasattr(self, '_kind'):
192 192 raise NodeError("Cannot change node's kind")
193 193 else:
194 194 self._kind = kind
195 195 # Post setter check (path's trailing slash)
196 196 if self.path.endswith('/'):
197 197 raise NodeError("Node's path cannot end with slash")
198 198
199 199 def __cmp__(self, other):
200 200 """
201 201 Comparator using name of the node, needed for quick list sorting.
202 202 """
203 203 kind_cmp = cmp(self.kind, other.kind)
204 204 if kind_cmp:
205 205 return kind_cmp
206 206 return cmp(self.name, other.name)
207 207
208 208 def __eq__(self, other):
209 209 for attr in ['name', 'path', 'kind']:
210 210 if getattr(self, attr) != getattr(other, attr):
211 211 return False
212 212 if self.is_file():
213 213 if self.content != other.content:
214 214 return False
215 215 else:
216 216 # For DirNode's check without entering each dir
217 217 self_nodes_paths = list(sorted(n.path for n in self.nodes))
218 218 other_nodes_paths = list(sorted(n.path for n in self.nodes))
219 219 if self_nodes_paths != other_nodes_paths:
220 220 return False
221 221 return True
222 222
223 223 def __ne__(self, other):
224 224 return not self.__eq__(other)
225 225
226 226 def __repr__(self):
227 227 return '<%s %r>' % (self.__class__.__name__, self.path)
228 228
229 229 def __str__(self):
230 230 return self.__repr__()
231 231
232 232 def __unicode__(self):
233 233 return self.name
234 234
235 235 def get_parent_path(self):
236 236 """
237 237 Returns node's parent path or empty string if node is root.
238 238 """
239 239 if self.is_root():
240 240 return ''
241 241 return vcspath.dirname(self.path.rstrip('/')) + '/'
242 242
243 243 def is_file(self):
244 244 """
245 245 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
246 246 otherwise.
247 247 """
248 248 return self.kind == NodeKind.FILE
249 249
250 250 def is_dir(self):
251 251 """
252 252 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
253 253 otherwise.
254 254 """
255 255 return self.kind == NodeKind.DIR
256 256
257 257 def is_root(self):
258 258 """
259 259 Returns ``True`` if node is a root node and ``False`` otherwise.
260 260 """
261 261 return self.kind == NodeKind.DIR and self.path == ''
262 262
263 263 def is_submodule(self):
264 264 """
265 265 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
266 266 otherwise.
267 267 """
268 268 return self.kind == NodeKind.SUBMODULE
269 269
270 270 def is_largefile(self):
271 271 """
272 272 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
273 273 otherwise
274 274 """
275 275 return self.kind == NodeKind.LARGEFILE
276 276
277 277 def is_link(self):
278 278 if self.commit:
279 279 return self.commit.is_link(self.path)
280 280 return False
281 281
282 282 @LazyProperty
283 283 def added(self):
284 284 return self.state is NodeState.ADDED
285 285
286 286 @LazyProperty
287 287 def changed(self):
288 288 return self.state is NodeState.CHANGED
289 289
290 290 @LazyProperty
291 291 def not_changed(self):
292 292 return self.state is NodeState.NOT_CHANGED
293 293
294 294 @LazyProperty
295 295 def removed(self):
296 296 return self.state is NodeState.REMOVED
297 297
298 298
299 299 class FileNode(Node):
300 300 """
301 301 Class representing file nodes.
302 302
303 303 :attribute: path: path to the node, relative to repository's root
304 304 :attribute: content: if given arbitrary sets content of the file
305 305 :attribute: commit: if given, first time content is accessed, callback
306 306 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
307 307 """
308 308 _filter_pre_load = []
309 309
310 310 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
311 311 """
312 312 Only one of ``content`` and ``commit`` may be given. Passing both
313 313 would raise ``NodeError`` exception.
314 314
315 315 :param path: relative path to the node
316 316 :param content: content may be passed to constructor
317 317 :param commit: if given, will use it to lazily fetch content
318 318 :param mode: ST_MODE (i.e. 0100644)
319 319 """
320 320 if content and commit:
321 321 raise NodeError("Cannot use both content and commit")
322 322 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
323 323 self.commit = commit
324 324 self._content = content
325 325 self._mode = mode or FILEMODE_DEFAULT
326 326
327 327 self._set_bulk_properties(pre_load)
328 328
329 329 def _set_bulk_properties(self, pre_load):
330 330 if not pre_load:
331 331 return
332 332 pre_load = [entry for entry in pre_load
333 333 if entry not in self._filter_pre_load]
334 334 if not pre_load:
335 335 return
336 336
337 337 for attr_name in pre_load:
338 338 result = getattr(self, attr_name)
339 339 if callable(result):
340 340 result = result()
341 341 self.__dict__[attr_name] = result
342 342
343 343 @LazyProperty
344 344 def mode(self):
345 345 """
346 346 Returns lazily mode of the FileNode. If `commit` is not set, would
347 347 use value given at initialization or `FILEMODE_DEFAULT` (default).
348 348 """
349 349 if self.commit:
350 350 mode = self.commit.get_file_mode(self.path)
351 351 else:
352 352 mode = self._mode
353 353 return mode
354 354
355 355 @LazyProperty
356 356 def raw_bytes(self):
357 357 """
358 358 Returns lazily the raw bytes of the FileNode.
359 359 """
360 360 if self.commit:
361 361 if self._content is None:
362 362 self._content = self.commit.get_file_content(self.path)
363 363 content = self._content
364 364 else:
365 365 content = self._content
366 366 return content
367 367
368 368 @LazyProperty
369 369 def md5(self):
370 370 """
371 371 Returns md5 of the file node.
372 372 """
373 373 return md5(self.raw_bytes)
374 374
375 def metadata_uncached(self):
376 """
377 Returns md5, binary flag of the file node, without any cache usage.
378 """
379
380 if self.commit:
381 content = self.commit.get_file_content(self.path)
382 else:
383 content = self._content
384
385 is_binary = content and '\0' in content
386 size = 0
387 if content:
388 size = len(content)
389 return is_binary, md5(content), size
390
375 391 @LazyProperty
376 392 def content(self):
377 393 """
378 394 Returns lazily content of the FileNode. If possible, would try to
379 395 decode content from UTF-8.
380 396 """
381 397 content = self.raw_bytes
382 398
383 399 if self.is_binary:
384 400 return content
385 401 return safe_unicode(content)
386 402
387 403 @LazyProperty
388 404 def size(self):
389 405 if self.commit:
390 406 return self.commit.get_file_size(self.path)
391 407 raise NodeError(
392 408 "Cannot retrieve size of the file without related "
393 409 "commit attribute")
394 410
395 411 @LazyProperty
396 412 def message(self):
397 413 if self.commit:
398 414 return self.last_commit.message
399 415 raise NodeError(
400 416 "Cannot retrieve message of the file without related "
401 417 "commit attribute")
402 418
403 419 @LazyProperty
404 420 def last_commit(self):
405 421 if self.commit:
406 422 pre_load = ["author", "date", "message"]
407 423 return self.commit.get_path_commit(self.path, pre_load=pre_load)
408 424 raise NodeError(
409 425 "Cannot retrieve last commit of the file without "
410 426 "related commit attribute")
411 427
412 428 def get_mimetype(self):
413 429 """
414 430 Mimetype is calculated based on the file's content. If ``_mimetype``
415 431 attribute is available, it will be returned (backends which store
416 432 mimetypes or can easily recognize them, should set this private
417 433 attribute to indicate that type should *NOT* be calculated).
418 434 """
419 435
420 436 if hasattr(self, '_mimetype'):
421 437 if (isinstance(self._mimetype, (tuple, list,)) and
422 438 len(self._mimetype) == 2):
423 439 return self._mimetype
424 440 else:
425 441 raise NodeError('given _mimetype attribute must be an 2 '
426 442 'element list or tuple')
427 443
428 444 db = get_mimetypes_db()
429 445 mtype, encoding = db.guess_type(self.name)
430 446
431 447 if mtype is None:
432 448 if self.is_binary:
433 449 mtype = 'application/octet-stream'
434 450 encoding = None
435 451 else:
436 452 mtype = 'text/plain'
437 453 encoding = None
438 454
439 455 # try with pygments
440 456 try:
441 457 from pygments.lexers import get_lexer_for_filename
442 458 mt = get_lexer_for_filename(self.name).mimetypes
443 459 except Exception:
444 460 mt = None
445 461
446 462 if mt:
447 463 mtype = mt[0]
448 464
449 465 return mtype, encoding
450 466
451 467 @LazyProperty
452 468 def mimetype(self):
453 469 """
454 470 Wrapper around full mimetype info. It returns only type of fetched
455 471 mimetype without the encoding part. use get_mimetype function to fetch
456 472 full set of (type,encoding)
457 473 """
458 474 return self.get_mimetype()[0]
459 475
460 476 @LazyProperty
461 477 def mimetype_main(self):
462 478 return self.mimetype.split('/')[0]
463 479
464 480 @classmethod
465 481 def get_lexer(cls, filename, content=None):
466 482 from pygments import lexers
467 483
468 484 extension = filename.split('.')[-1]
469 485 lexer = None
470 486
471 487 try:
472 488 lexer = lexers.guess_lexer_for_filename(
473 489 filename, content, stripnl=False)
474 490 except lexers.ClassNotFound:
475 491 lexer = None
476 492
477 493 # try our EXTENSION_MAP
478 494 if not lexer:
479 495 try:
480 496 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
481 497 if lexer_class:
482 498 lexer = lexers.get_lexer_by_name(lexer_class[0])
483 499 except lexers.ClassNotFound:
484 500 lexer = None
485 501
486 502 if not lexer:
487 503 lexer = lexers.TextLexer(stripnl=False)
488 504
489 505 return lexer
490 506
491 507 @LazyProperty
492 508 def lexer(self):
493 509 """
494 510 Returns pygment's lexer class. Would try to guess lexer taking file's
495 511 content, name and mimetype.
496 512 """
497 513 return self.get_lexer(self.name, self.content)
498 514
499 515 @LazyProperty
500 516 def lexer_alias(self):
501 517 """
502 518 Returns first alias of the lexer guessed for this file.
503 519 """
504 520 return self.lexer.aliases[0]
505 521
506 522 @LazyProperty
507 523 def history(self):
508 524 """
509 525 Returns a list of commit for this file in which the file was changed
510 526 """
511 527 if self.commit is None:
512 528 raise NodeError('Unable to get commit for this FileNode')
513 529 return self.commit.get_path_history(self.path)
514 530
515 531 @LazyProperty
516 532 def annotate(self):
517 533 """
518 534 Returns a list of three element tuples with lineno, commit and line
519 535 """
520 536 if self.commit is None:
521 537 raise NodeError('Unable to get commit for this FileNode')
522 538 pre_load = ["author", "date", "message"]
523 539 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
524 540
525 541 @LazyProperty
526 542 def state(self):
527 543 if not self.commit:
528 544 raise NodeError(
529 545 "Cannot check state of the node if it's not "
530 546 "linked with commit")
531 547 elif self.path in (node.path for node in self.commit.added):
532 548 return NodeState.ADDED
533 549 elif self.path in (node.path for node in self.commit.changed):
534 550 return NodeState.CHANGED
535 551 else:
536 552 return NodeState.NOT_CHANGED
537 553
538 554 @LazyProperty
539 555 def is_binary(self):
540 556 """
541 557 Returns True if file has binary content.
542 558 """
543 559 _bin = self.raw_bytes and '\0' in self.raw_bytes
544 560 return _bin
545 561
546 562 @LazyProperty
547 563 def extension(self):
548 564 """Returns filenode extension"""
549 565 return self.name.split('.')[-1]
550 566
551 567 @property
552 568 def is_executable(self):
553 569 """
554 570 Returns ``True`` if file has executable flag turned on.
555 571 """
556 572 return bool(self.mode & stat.S_IXUSR)
557 573
558 574 def get_largefile_node(self):
559 575 """
560 576 Try to return a Mercurial FileNode from this node. It does internal
561 577 checks inside largefile store, if that file exist there it will
562 578 create special instance of LargeFileNode which can get content from
563 579 LF store.
564 580 """
565 581 if self.commit:
566 582 return self.commit.get_largefile_node(self.path)
567 583
568 584 def lines(self, count_empty=False):
569 585 all_lines, empty_lines = 0, 0
570 586
571 587 if not self.is_binary:
572 588 content = self.content
573 589 if count_empty:
574 590 all_lines = 0
575 591 empty_lines = 0
576 592 for line in content.splitlines(True):
577 593 if line == '\n':
578 594 empty_lines += 1
579 595 all_lines += 1
580 596
581 597 return all_lines, all_lines - empty_lines
582 598 else:
583 599 # fast method
584 600 empty_lines = all_lines = content.count('\n')
585 601 if all_lines == 0 and content:
586 602 # one-line without a newline
587 603 empty_lines = all_lines = 1
588 604
589 605 return all_lines, empty_lines
590 606
591 607 def __repr__(self):
592 608 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
593 609 getattr(self.commit, 'short_id', ''))
594 610
595 611
596 612 class RemovedFileNode(FileNode):
597 613 """
598 614 Dummy FileNode class - trying to access any public attribute except path,
599 615 name, kind or state (or methods/attributes checking those two) would raise
600 616 RemovedFileNodeError.
601 617 """
602 618 ALLOWED_ATTRIBUTES = [
603 619 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
604 620 'added', 'changed', 'not_changed', 'removed'
605 621 ]
606 622
607 623 def __init__(self, path):
608 624 """
609 625 :param path: relative path to the node
610 626 """
611 627 super(RemovedFileNode, self).__init__(path=path)
612 628
613 629 def __getattribute__(self, attr):
614 630 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
615 631 return super(RemovedFileNode, self).__getattribute__(attr)
616 632 raise RemovedFileNodeError(
617 633 "Cannot access attribute %s on RemovedFileNode" % attr)
618 634
619 635 @LazyProperty
620 636 def state(self):
621 637 return NodeState.REMOVED
622 638
623 639
624 640 class DirNode(Node):
625 641 """
626 642 DirNode stores list of files and directories within this node.
627 643 Nodes may be used standalone but within repository context they
628 644 lazily fetch data within same repositorty's commit.
629 645 """
630 646
631 647 def __init__(self, path, nodes=(), commit=None):
632 648 """
633 649 Only one of ``nodes`` and ``commit`` may be given. Passing both
634 650 would raise ``NodeError`` exception.
635 651
636 652 :param path: relative path to the node
637 653 :param nodes: content may be passed to constructor
638 654 :param commit: if given, will use it to lazily fetch content
639 655 """
640 656 if nodes and commit:
641 657 raise NodeError("Cannot use both nodes and commit")
642 658 super(DirNode, self).__init__(path, NodeKind.DIR)
643 659 self.commit = commit
644 660 self._nodes = nodes
645 661
646 662 @LazyProperty
647 663 def content(self):
648 664 raise NodeError(
649 665 "%s represents a dir and has no `content` attribute" % self)
650 666
651 667 @LazyProperty
652 668 def nodes(self):
653 669 if self.commit:
654 670 nodes = self.commit.get_nodes(self.path)
655 671 else:
656 672 nodes = self._nodes
657 673 self._nodes_dict = dict((node.path, node) for node in nodes)
658 674 return sorted(nodes)
659 675
660 676 @LazyProperty
661 677 def files(self):
662 678 return sorted((node for node in self.nodes if node.is_file()))
663 679
664 680 @LazyProperty
665 681 def dirs(self):
666 682 return sorted((node for node in self.nodes if node.is_dir()))
667 683
668 684 def __iter__(self):
669 685 for node in self.nodes:
670 686 yield node
671 687
672 688 def get_node(self, path):
673 689 """
674 690 Returns node from within this particular ``DirNode``, so it is now
675 691 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
676 692 'docs'. In order to access deeper nodes one must fetch nodes between
677 693 them first - this would work::
678 694
679 695 docs = root.get_node('docs')
680 696 docs.get_node('api').get_node('index.rst')
681 697
682 698 :param: path - relative to the current node
683 699
684 700 .. note::
685 701 To access lazily (as in example above) node have to be initialized
686 702 with related commit object - without it node is out of
687 703 context and may know nothing about anything else than nearest
688 704 (located at same level) nodes.
689 705 """
690 706 try:
691 707 path = path.rstrip('/')
692 708 if path == '':
693 709 raise NodeError("Cannot retrieve node without path")
694 710 self.nodes # access nodes first in order to set _nodes_dict
695 711 paths = path.split('/')
696 712 if len(paths) == 1:
697 713 if not self.is_root():
698 714 path = '/'.join((self.path, paths[0]))
699 715 else:
700 716 path = paths[0]
701 717 return self._nodes_dict[path]
702 718 elif len(paths) > 1:
703 719 if self.commit is None:
704 720 raise NodeError(
705 721 "Cannot access deeper nodes without commit")
706 722 else:
707 723 path1, path2 = paths[0], '/'.join(paths[1:])
708 724 return self.get_node(path1).get_node(path2)
709 725 else:
710 726 raise KeyError
711 727 except KeyError:
712 728 raise NodeError("Node does not exist at %s" % path)
713 729
714 730 @LazyProperty
715 731 def state(self):
716 732 raise NodeError("Cannot access state of DirNode")
717 733
718 734 @LazyProperty
719 735 def size(self):
720 736 size = 0
721 737 for root, dirs, files in self.commit.walk(self.path):
722 738 for f in files:
723 739 size += f.size
724 740
725 741 return size
726 742
727 743 @LazyProperty
728 744 def last_commit(self):
729 745 if self.commit:
730 746 pre_load = ["author", "date", "message"]
731 747 return self.commit.get_path_commit(self.path, pre_load=pre_load)
732 748 raise NodeError(
733 749 "Cannot retrieve last commit of the file without "
734 750 "related commit attribute")
735 751
736 752 def __repr__(self):
737 753 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
738 754 getattr(self.commit, 'short_id', ''))
739 755
740 756
741 757 class RootNode(DirNode):
742 758 """
743 759 DirNode being the root node of the repository.
744 760 """
745 761
746 762 def __init__(self, nodes=(), commit=None):
747 763 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
748 764
749 765 def __repr__(self):
750 766 return '<%s>' % self.__class__.__name__
751 767
752 768
753 769 class SubModuleNode(Node):
754 770 """
755 771 represents a SubModule of Git or SubRepo of Mercurial
756 772 """
757 773 is_binary = False
758 774 size = 0
759 775
760 776 def __init__(self, name, url=None, commit=None, alias=None):
761 777 self.path = name
762 778 self.kind = NodeKind.SUBMODULE
763 779 self.alias = alias
764 780
765 781 # we have to use EmptyCommit here since this can point to svn/git/hg
766 782 # submodules we cannot get from repository
767 783 self.commit = EmptyCommit(str(commit), alias=alias)
768 784 self.url = url or self._extract_submodule_url()
769 785
770 786 def __repr__(self):
771 787 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
772 788 getattr(self.commit, 'short_id', ''))
773 789
774 790 def _extract_submodule_url(self):
775 791 # TODO: find a way to parse gits submodule file and extract the
776 792 # linking URL
777 793 return self.path
778 794
779 795 @LazyProperty
780 796 def name(self):
781 797 """
782 798 Returns name of the node so if its path
783 799 then only last part is returned.
784 800 """
785 801 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
786 802 return u'%s @ %s' % (org, self.commit.short_id)
787 803
788 804
789 805 class LargeFileNode(FileNode):
790 806
791 807 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
792 808 self.path = path
793 809 self.org_path = org_path
794 810 self.kind = NodeKind.LARGEFILE
795 811 self.alias = alias
796 812
797 813 def _validate_path(self, path):
798 814 """
799 815 we override check since the LargeFileNode path is system absolute
800 816 """
801 817 pass
802 818
803 819 def __repr__(self):
804 820 return '<%s %r>' % (self.__class__.__name__, self.path)
805 821
806 822 @LazyProperty
807 823 def size(self):
808 824 return os.stat(self.path).st_size
809 825
810 826 @LazyProperty
811 827 def raw_bytes(self):
812 828 with open(self.path, 'rb') as f:
813 829 content = f.read()
814 830 return content
815 831
816 832 @LazyProperty
817 833 def name(self):
818 834 """
819 835 Overwrites name to be the org lf path
820 836 """
821 837 return self.org_path
@@ -1,832 +1,914 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 klass = get_backend(path[0])
230 230 repos[name] = klass(path[1], config=config)
231 231 except OSError:
232 232 continue
233 233 log.debug('found %s paths with repositories', len(repos))
234 234 return repos
235 235
236 236 def get_repos(self, all_repos=None, sort_key=None):
237 237 """
238 238 Get all repositories from db and for each repo create it's
239 239 backend instance and fill that backed with information from database
240 240
241 241 :param all_repos: list of repository names as strings
242 242 give specific repositories list, good for filtering
243 243
244 244 :param sort_key: initial sorting of repositories
245 245 """
246 246 if all_repos is None:
247 247 all_repos = self.sa.query(Repository)\
248 248 .filter(Repository.group_id == None)\
249 249 .order_by(func.lower(Repository.repo_name)).all()
250 250 repo_iter = SimpleCachedRepoList(
251 251 all_repos, repos_path=self.repos_path, order_by=sort_key)
252 252 return repo_iter
253 253
254 254 def get_repo_groups(self, all_groups=None):
255 255 if all_groups is None:
256 256 all_groups = RepoGroup.query()\
257 257 .filter(RepoGroup.group_parent_id == None).all()
258 258 return [x for x in RepoGroupList(all_groups)]
259 259
260 260 def mark_for_invalidation(self, repo_name, delete=False):
261 261 """
262 262 Mark caches of this repo invalid in the database. `delete` flag
263 263 removes the cache entries
264 264
265 265 :param repo_name: the repo_name for which caches should be marked
266 266 invalid, or deleted
267 267 :param delete: delete the entry keys instead of setting bool
268 268 flag on them, and also purge caches used by the dogpile
269 269 """
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271
272 272 if repo:
273 273 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
274 274 repo_id=repo.repo_id)
275 275 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
276 276
277 277 repo_id = repo.repo_id
278 278 config = repo._config
279 279 config.set('extensions', 'largefiles', '')
280 280 repo.update_commit_cache(config=config, cs_cache=None)
281 281 if delete:
282 282 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
283 283 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
284 284
285 285 def toggle_following_repo(self, follow_repo_id, user_id):
286 286
287 287 f = self.sa.query(UserFollowing)\
288 288 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
289 289 .filter(UserFollowing.user_id == user_id).scalar()
290 290
291 291 if f is not None:
292 292 try:
293 293 self.sa.delete(f)
294 294 return
295 295 except Exception:
296 296 log.error(traceback.format_exc())
297 297 raise
298 298
299 299 try:
300 300 f = UserFollowing()
301 301 f.user_id = user_id
302 302 f.follows_repo_id = follow_repo_id
303 303 self.sa.add(f)
304 304 except Exception:
305 305 log.error(traceback.format_exc())
306 306 raise
307 307
308 308 def toggle_following_user(self, follow_user_id, user_id):
309 309 f = self.sa.query(UserFollowing)\
310 310 .filter(UserFollowing.follows_user_id == follow_user_id)\
311 311 .filter(UserFollowing.user_id == user_id).scalar()
312 312
313 313 if f is not None:
314 314 try:
315 315 self.sa.delete(f)
316 316 return
317 317 except Exception:
318 318 log.error(traceback.format_exc())
319 319 raise
320 320
321 321 try:
322 322 f = UserFollowing()
323 323 f.user_id = user_id
324 324 f.follows_user_id = follow_user_id
325 325 self.sa.add(f)
326 326 except Exception:
327 327 log.error(traceback.format_exc())
328 328 raise
329 329
330 330 def is_following_repo(self, repo_name, user_id, cache=False):
331 331 r = self.sa.query(Repository)\
332 332 .filter(Repository.repo_name == repo_name).scalar()
333 333
334 334 f = self.sa.query(UserFollowing)\
335 335 .filter(UserFollowing.follows_repository == r)\
336 336 .filter(UserFollowing.user_id == user_id).scalar()
337 337
338 338 return f is not None
339 339
340 340 def is_following_user(self, username, user_id, cache=False):
341 341 u = User.get_by_username(username)
342 342
343 343 f = self.sa.query(UserFollowing)\
344 344 .filter(UserFollowing.follows_user == u)\
345 345 .filter(UserFollowing.user_id == user_id).scalar()
346 346
347 347 return f is not None
348 348
349 349 def get_followers(self, repo):
350 350 repo = self._get_repo(repo)
351 351
352 352 return self.sa.query(UserFollowing)\
353 353 .filter(UserFollowing.follows_repository == repo).count()
354 354
355 355 def get_forks(self, repo):
356 356 repo = self._get_repo(repo)
357 357 return self.sa.query(Repository)\
358 358 .filter(Repository.fork == repo).count()
359 359
360 360 def get_pull_requests(self, repo):
361 361 repo = self._get_repo(repo)
362 362 return self.sa.query(PullRequest)\
363 363 .filter(PullRequest.target_repo == repo)\
364 364 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
365 365
366 366 def mark_as_fork(self, repo, fork, user):
367 367 repo = self._get_repo(repo)
368 368 fork = self._get_repo(fork)
369 369 if fork and repo.repo_id == fork.repo_id:
370 370 raise Exception("Cannot set repository as fork of itself")
371 371
372 372 if fork and repo.repo_type != fork.repo_type:
373 373 raise RepositoryError(
374 374 "Cannot set repository as fork of repository with other type")
375 375
376 376 repo.fork = fork
377 377 self.sa.add(repo)
378 378 return repo
379 379
380 380 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
381 381 dbrepo = self._get_repo(repo)
382 382 remote_uri = remote_uri or dbrepo.clone_uri
383 383 if not remote_uri:
384 384 raise Exception("This repository doesn't have a clone uri")
385 385
386 386 repo = dbrepo.scm_instance(cache=False)
387 387 repo.config.clear_section('hooks')
388 388
389 389 try:
390 390 # NOTE(marcink): add extra validation so we skip invalid urls
391 391 # this is due this tasks can be executed via scheduler without
392 392 # proper validation of remote_uri
393 393 if validate_uri:
394 394 config = make_db_config(clear_session=False)
395 395 url_validator(remote_uri, dbrepo.repo_type, config)
396 396 except InvalidCloneUrl:
397 397 raise
398 398
399 399 repo_name = dbrepo.repo_name
400 400 try:
401 401 # TODO: we need to make sure those operations call proper hooks !
402 402 repo.fetch(remote_uri)
403 403
404 404 self.mark_for_invalidation(repo_name)
405 405 except Exception:
406 406 log.error(traceback.format_exc())
407 407 raise
408 408
409 409 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
410 410 dbrepo = self._get_repo(repo)
411 411 remote_uri = remote_uri or dbrepo.push_uri
412 412 if not remote_uri:
413 413 raise Exception("This repository doesn't have a clone uri")
414 414
415 415 repo = dbrepo.scm_instance(cache=False)
416 416 repo.config.clear_section('hooks')
417 417
418 418 try:
419 419 # NOTE(marcink): add extra validation so we skip invalid urls
420 420 # this is due this tasks can be executed via scheduler without
421 421 # proper validation of remote_uri
422 422 if validate_uri:
423 423 config = make_db_config(clear_session=False)
424 424 url_validator(remote_uri, dbrepo.repo_type, config)
425 425 except InvalidCloneUrl:
426 426 raise
427 427
428 428 try:
429 429 repo.push(remote_uri)
430 430 except Exception:
431 431 log.error(traceback.format_exc())
432 432 raise
433 433
434 434 def commit_change(self, repo, repo_name, commit, user, author, message,
435 435 content, f_path):
436 436 """
437 437 Commits changes
438 438
439 439 :param repo: SCM instance
440 440
441 441 """
442 442 user = self._get_user(user)
443 443
444 444 # decoding here will force that we have proper encoded values
445 445 # in any other case this will throw exceptions and deny commit
446 446 content = safe_str(content)
447 447 path = safe_str(f_path)
448 448 # message and author needs to be unicode
449 449 # proper backend should then translate that into required type
450 450 message = safe_unicode(message)
451 451 author = safe_unicode(author)
452 452 imc = repo.in_memory_commit
453 453 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
454 454 try:
455 455 # TODO: handle pre-push action !
456 456 tip = imc.commit(
457 457 message=message, author=author, parents=[commit],
458 458 branch=commit.branch)
459 459 except Exception as e:
460 460 log.error(traceback.format_exc())
461 461 raise IMCCommitError(str(e))
462 462 finally:
463 463 # always clear caches, if commit fails we want fresh object also
464 464 self.mark_for_invalidation(repo_name)
465 465
466 466 # We trigger the post-push action
467 467 hooks_utils.trigger_post_push_hook(
468 468 username=user.username, action='push_local', hook_type='post_push',
469 469 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
470 470 return tip
471 471
472 472 def _sanitize_path(self, f_path):
473 473 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
474 474 raise NonRelativePathError('%s is not an relative path' % f_path)
475 475 if f_path:
476 476 f_path = os.path.normpath(f_path)
477 477 return f_path
478 478
479 479 def get_dirnode_metadata(self, request, commit, dir_node):
480 480 if not dir_node.is_dir():
481 481 return []
482 482
483 483 data = []
484 484 for node in dir_node:
485 485 if not node.is_file():
486 486 # we skip file-nodes
487 487 continue
488 488
489 489 last_commit = node.last_commit
490 490 last_commit_date = last_commit.date
491 491 data.append({
492 492 'name': node.name,
493 493 'size': h.format_byte_size_binary(node.size),
494 494 'modified_at': h.format_date(last_commit_date),
495 495 'modified_ts': last_commit_date.isoformat(),
496 496 'revision': last_commit.revision,
497 497 'short_id': last_commit.short_id,
498 498 'message': h.escape(last_commit.message),
499 499 'author': h.escape(last_commit.author),
500 500 'user_profile': h.gravatar_with_user(
501 501 request, last_commit.author),
502 502 })
503 503
504 504 return data
505 505
506 506 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
507 507 extended_info=False, content=False, max_file_bytes=None):
508 508 """
509 509 recursive walk in root dir and return a set of all path in that dir
510 510 based on repository walk function
511 511
512 512 :param repo_name: name of repository
513 513 :param commit_id: commit id for which to list nodes
514 514 :param root_path: root path to list
515 515 :param flat: return as a list, if False returns a dict with description
516 516 :param max_file_bytes: will not return file contents over this limit
517 517
518 518 """
519 519 _files = list()
520 520 _dirs = list()
521 521 try:
522 522 _repo = self._get_repo(repo_name)
523 523 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
524 524 root_path = root_path.lstrip('/')
525 525 for __, dirs, files in commit.walk(root_path):
526 526 for f in files:
527 527 _content = None
528 528 _data = f.unicode_path
529 529 over_size_limit = (max_file_bytes is not None
530 530 and f.size > max_file_bytes)
531 531
532 532 if not flat:
533 533 _data = {
534 534 "name": h.escape(f.unicode_path),
535 535 "type": "file",
536 536 }
537 537 if extended_info:
538 538 _data.update({
539 539 "md5": f.md5,
540 540 "binary": f.is_binary,
541 541 "size": f.size,
542 542 "extension": f.extension,
543 543 "mimetype": f.mimetype,
544 544 "lines": f.lines()[0]
545 545 })
546 546
547 547 if content:
548 548 full_content = None
549 549 if not f.is_binary and not over_size_limit:
550 550 full_content = safe_str(f.content)
551 551
552 552 _data.update({
553 553 "content": full_content,
554 554 })
555 555 _files.append(_data)
556 556 for d in dirs:
557 557 _data = d.unicode_path
558 558 if not flat:
559 559 _data = {
560 560 "name": h.escape(d.unicode_path),
561 561 "type": "dir",
562 562 }
563 563 if extended_info:
564 564 _data.update({
565 565 "md5": None,
566 566 "binary": None,
567 567 "size": None,
568 568 "extension": None,
569 569 })
570 570 if content:
571 571 _data.update({
572 572 "content": None
573 573 })
574 574 _dirs.append(_data)
575 575 except RepositoryError:
576 log.debug("Exception in get_nodes", exc_info=True)
576 log.exception("Exception in get_nodes")
577 577 raise
578 578
579 579 return _dirs, _files
580 580
581 def get_node(self, repo_name, commit_id, file_path,
582 extended_info=False, content=False, max_file_bytes=None):
583 """
584 retrieve single node from commit
585 """
586 try:
587
588 _repo = self._get_repo(repo_name)
589 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
590
591 file_node = commit.get_node(file_path)
592 if file_node.is_dir():
593 raise RepositoryError('The given path is a directory')
594
595 _content = None
596 f_name = file_node.unicode_path
597
598 file_data = {
599 "name": h.escape(f_name),
600 "type": "file",
601 }
602
603 if extended_info:
604 file_data.update({
605 "md5": file_node.md5,
606 "binary": file_node.is_binary,
607 "size": file_node.size,
608 "extension": file_node.extension,
609 "mimetype": file_node.mimetype,
610 "lines": file_node.lines()[0]
611 })
612
613 if content:
614 over_size_limit = (max_file_bytes is not None
615 and file_node.size > max_file_bytes)
616 full_content = None
617 if not file_node.is_binary and not over_size_limit:
618 full_content = safe_str(file_node.content)
619
620 file_data.update({
621 "content": full_content,
622 })
623
624 except RepositoryError:
625 log.exception("Exception in get_node")
626 raise
627
628 return file_data
629
630 def get_fts_data(self, repo_name, commit_id, root_path='/'):
631 """
632 Fetch node tree for usage in full text search
633 """
634
635 tree_info = list()
636
637 try:
638 _repo = self._get_repo(repo_name)
639 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
640 root_path = root_path.lstrip('/')
641 for __, dirs, files in commit.walk(root_path):
642
643 for f in files:
644 _content = None
645 _data = f_name = f.unicode_path
646 is_binary, md5, size = f.metadata_uncached()
647 _data = {
648 "name": h.escape(f_name),
649 "md5": md5,
650 "extension": f.extension,
651 "binary": is_binary,
652 "size": size
653 }
654
655 tree_info.append(_data)
656
657 except RepositoryError:
658 log.exception("Exception in get_nodes")
659 raise
660
661 return tree_info
662
581 663 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
582 664 author=None, trigger_push_hook=True):
583 665 """
584 666 Commits given multiple nodes into repo
585 667
586 668 :param user: RhodeCode User object or user_id, the commiter
587 669 :param repo: RhodeCode Repository object
588 670 :param message: commit message
589 671 :param nodes: mapping {filename:{'content':content},...}
590 672 :param parent_commit: parent commit, can be empty than it's
591 673 initial commit
592 674 :param author: author of commit, cna be different that commiter
593 675 only for git
594 676 :param trigger_push_hook: trigger push hooks
595 677
596 678 :returns: new commited commit
597 679 """
598 680
599 681 user = self._get_user(user)
600 682 scm_instance = repo.scm_instance(cache=False)
601 683
602 684 processed_nodes = []
603 685 for f_path in nodes:
604 686 f_path = self._sanitize_path(f_path)
605 687 content = nodes[f_path]['content']
606 688 f_path = safe_str(f_path)
607 689 # decoding here will force that we have proper encoded values
608 690 # in any other case this will throw exceptions and deny commit
609 691 if isinstance(content, (basestring,)):
610 692 content = safe_str(content)
611 693 elif isinstance(content, (file, cStringIO.OutputType,)):
612 694 content = content.read()
613 695 else:
614 696 raise Exception('Content is of unrecognized type %s' % (
615 697 type(content)
616 698 ))
617 699 processed_nodes.append((f_path, content))
618 700
619 701 message = safe_unicode(message)
620 702 commiter = user.full_contact
621 703 author = safe_unicode(author) if author else commiter
622 704
623 705 imc = scm_instance.in_memory_commit
624 706
625 707 if not parent_commit:
626 708 parent_commit = EmptyCommit(alias=scm_instance.alias)
627 709
628 710 if isinstance(parent_commit, EmptyCommit):
629 711 # EmptyCommit means we we're editing empty repository
630 712 parents = None
631 713 else:
632 714 parents = [parent_commit]
633 715 # add multiple nodes
634 716 for path, content in processed_nodes:
635 717 imc.add(FileNode(path, content=content))
636 718 # TODO: handle pre push scenario
637 719 tip = imc.commit(message=message,
638 720 author=author,
639 721 parents=parents,
640 722 branch=parent_commit.branch)
641 723
642 724 self.mark_for_invalidation(repo.repo_name)
643 725 if trigger_push_hook:
644 726 hooks_utils.trigger_post_push_hook(
645 727 username=user.username, action='push_local',
646 728 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
647 729 hook_type='post_push',
648 730 commit_ids=[tip.raw_id])
649 731 return tip
650 732
651 733 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
652 734 author=None, trigger_push_hook=True):
653 735 user = self._get_user(user)
654 736 scm_instance = repo.scm_instance(cache=False)
655 737
656 738 message = safe_unicode(message)
657 739 commiter = user.full_contact
658 740 author = safe_unicode(author) if author else commiter
659 741
660 742 imc = scm_instance.in_memory_commit
661 743
662 744 if not parent_commit:
663 745 parent_commit = EmptyCommit(alias=scm_instance.alias)
664 746
665 747 if isinstance(parent_commit, EmptyCommit):
666 748 # EmptyCommit means we we're editing empty repository
667 749 parents = None
668 750 else:
669 751 parents = [parent_commit]
670 752
671 753 # add multiple nodes
672 754 for _filename, data in nodes.items():
673 755 # new filename, can be renamed from the old one, also sanitaze
674 756 # the path for any hack around relative paths like ../../ etc.
675 757 filename = self._sanitize_path(data['filename'])
676 758 old_filename = self._sanitize_path(_filename)
677 759 content = data['content']
678 760 file_mode = data.get('mode')
679 761 filenode = FileNode(old_filename, content=content, mode=file_mode)
680 762 op = data['op']
681 763 if op == 'add':
682 764 imc.add(filenode)
683 765 elif op == 'del':
684 766 imc.remove(filenode)
685 767 elif op == 'mod':
686 768 if filename != old_filename:
687 769 # TODO: handle renames more efficient, needs vcs lib changes
688 770 imc.remove(filenode)
689 771 imc.add(FileNode(filename, content=content, mode=file_mode))
690 772 else:
691 773 imc.change(filenode)
692 774
693 775 try:
694 776 # TODO: handle pre push scenario commit changes
695 777 tip = imc.commit(message=message,
696 778 author=author,
697 779 parents=parents,
698 780 branch=parent_commit.branch)
699 781 except NodeNotChangedError:
700 782 raise
701 783 except Exception as e:
702 784 log.exception("Unexpected exception during call to imc.commit")
703 785 raise IMCCommitError(str(e))
704 786 finally:
705 787 # always clear caches, if commit fails we want fresh object also
706 788 self.mark_for_invalidation(repo.repo_name)
707 789
708 790 if trigger_push_hook:
709 791 hooks_utils.trigger_post_push_hook(
710 792 username=user.username, action='push_local', hook_type='post_push',
711 793 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
712 794 commit_ids=[tip.raw_id])
713 795
714 796 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
715 797 author=None, trigger_push_hook=True):
716 798 """
717 799 Deletes given multiple nodes into `repo`
718 800
719 801 :param user: RhodeCode User object or user_id, the committer
720 802 :param repo: RhodeCode Repository object
721 803 :param message: commit message
722 804 :param nodes: mapping {filename:{'content':content},...}
723 805 :param parent_commit: parent commit, can be empty than it's initial
724 806 commit
725 807 :param author: author of commit, cna be different that commiter only
726 808 for git
727 809 :param trigger_push_hook: trigger push hooks
728 810
729 811 :returns: new commit after deletion
730 812 """
731 813
732 814 user = self._get_user(user)
733 815 scm_instance = repo.scm_instance(cache=False)
734 816
735 817 processed_nodes = []
736 818 for f_path in nodes:
737 819 f_path = self._sanitize_path(f_path)
738 820 # content can be empty but for compatabilty it allows same dicts
739 821 # structure as add_nodes
740 822 content = nodes[f_path].get('content')
741 823 processed_nodes.append((f_path, content))
742 824
743 825 message = safe_unicode(message)
744 826 commiter = user.full_contact
745 827 author = safe_unicode(author) if author else commiter
746 828
747 829 imc = scm_instance.in_memory_commit
748 830
749 831 if not parent_commit:
750 832 parent_commit = EmptyCommit(alias=scm_instance.alias)
751 833
752 834 if isinstance(parent_commit, EmptyCommit):
753 835 # EmptyCommit means we we're editing empty repository
754 836 parents = None
755 837 else:
756 838 parents = [parent_commit]
757 839 # add multiple nodes
758 840 for path, content in processed_nodes:
759 841 imc.remove(FileNode(path, content=content))
760 842
761 843 # TODO: handle pre push scenario
762 844 tip = imc.commit(message=message,
763 845 author=author,
764 846 parents=parents,
765 847 branch=parent_commit.branch)
766 848
767 849 self.mark_for_invalidation(repo.repo_name)
768 850 if trigger_push_hook:
769 851 hooks_utils.trigger_post_push_hook(
770 852 username=user.username, action='push_local', hook_type='post_push',
771 853 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
772 854 commit_ids=[tip.raw_id])
773 855 return tip
774 856
775 857 def strip(self, repo, commit_id, branch):
776 858 scm_instance = repo.scm_instance(cache=False)
777 859 scm_instance.config.clear_section('hooks')
778 860 scm_instance.strip(commit_id, branch)
779 861 self.mark_for_invalidation(repo.repo_name)
780 862
781 863 def get_unread_journal(self):
782 864 return self.sa.query(UserLog).count()
783 865
784 866 def get_repo_landing_revs(self, translator, repo=None):
785 867 """
786 868 Generates select option with tags branches and bookmarks (for hg only)
787 869 grouped by type
788 870
789 871 :param repo:
790 872 """
791 873 _ = translator
792 874 repo = self._get_repo(repo)
793 875
794 876 hist_l = [
795 877 ['rev:tip', _('latest tip')]
796 878 ]
797 879 choices = [
798 880 'rev:tip'
799 881 ]
800 882
801 883 if not repo:
802 884 return choices, hist_l
803 885
804 886 repo = repo.scm_instance()
805 887
806 888 branches_group = (
807 889 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
808 890 for b in repo.branches],
809 891 _("Branches"))
810 892 hist_l.append(branches_group)
811 893 choices.extend([x[0] for x in branches_group[0]])
812 894
813 895 if repo.alias == 'hg':
814 896 bookmarks_group = (
815 897 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
816 898 for b in repo.bookmarks],
817 899 _("Bookmarks"))
818 900 hist_l.append(bookmarks_group)
819 901 choices.extend([x[0] for x in bookmarks_group[0]])
820 902
821 903 tags_group = (
822 904 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
823 905 for t in repo.tags],
824 906 _("Tags"))
825 907 hist_l.append(tags_group)
826 908 choices.extend([x[0] for x in tags_group[0]])
827 909
828 910 return choices, hist_l
829 911
830 912 def get_server_info(self, environ=None):
831 913 server_info = get_system_info(environ)
832 914 return server_info
General Comments 0
You need to be logged in to leave comments. Login now