##// END OF EJS Templates
comments: changed signature of create method of ChangesetComment....
marcink -
r1322:50f14062 default
parent child Browse files
Show More
@@ -1,1960 +1,1960 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
33 33 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
34 34 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.model.changeset_status import ChangesetStatusModel
37 37 from rhodecode.model.comment import ChangesetCommentsModel
38 38 from rhodecode.model.db import (
39 39 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup)
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.scm import ScmModel, RepoList
42 42 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
43 43 from rhodecode.model import validation_schema
44 44 from rhodecode.model.validation_schema.schemas import repo_schema
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 @jsonrpc_method()
50 50 def get_repo(request, apiuser, repoid, cache=Optional(True)):
51 51 """
52 52 Gets an existing repository by its name or repository_id.
53 53
54 54 The members section so the output returns users groups or users
55 55 associated with that repository.
56 56
57 57 This command can only be run using an |authtoken| with admin rights,
58 58 or users with at least read rights to the |repo|.
59 59
60 60 :param apiuser: This is filled automatically from the |authtoken|.
61 61 :type apiuser: AuthUser
62 62 :param repoid: The repository name or repository id.
63 63 :type repoid: str or int
64 64 :param cache: use the cached value for last changeset
65 65 :type: cache: Optional(bool)
66 66
67 67 Example output:
68 68
69 69 .. code-block:: bash
70 70
71 71 {
72 72 "error": null,
73 73 "id": <repo_id>,
74 74 "result": {
75 75 "clone_uri": null,
76 76 "created_on": "timestamp",
77 77 "description": "repo description",
78 78 "enable_downloads": false,
79 79 "enable_locking": false,
80 80 "enable_statistics": false,
81 81 "followers": [
82 82 {
83 83 "active": true,
84 84 "admin": false,
85 85 "api_key": "****************************************",
86 86 "api_keys": [
87 87 "****************************************"
88 88 ],
89 89 "email": "user@example.com",
90 90 "emails": [
91 91 "user@example.com"
92 92 ],
93 93 "extern_name": "rhodecode",
94 94 "extern_type": "rhodecode",
95 95 "firstname": "username",
96 96 "ip_addresses": [],
97 97 "language": null,
98 98 "last_login": "2015-09-16T17:16:35.854",
99 99 "lastname": "surname",
100 100 "user_id": <user_id>,
101 101 "username": "name"
102 102 }
103 103 ],
104 104 "fork_of": "parent-repo",
105 105 "landing_rev": [
106 106 "rev",
107 107 "tip"
108 108 ],
109 109 "last_changeset": {
110 110 "author": "User <user@example.com>",
111 111 "branch": "default",
112 112 "date": "timestamp",
113 113 "message": "last commit message",
114 114 "parents": [
115 115 {
116 116 "raw_id": "commit-id"
117 117 }
118 118 ],
119 119 "raw_id": "commit-id",
120 120 "revision": <revision number>,
121 121 "short_id": "short id"
122 122 },
123 123 "lock_reason": null,
124 124 "locked_by": null,
125 125 "locked_date": null,
126 126 "members": [
127 127 {
128 128 "name": "super-admin-name",
129 129 "origin": "super-admin",
130 130 "permission": "repository.admin",
131 131 "type": "user"
132 132 },
133 133 {
134 134 "name": "owner-name",
135 135 "origin": "owner",
136 136 "permission": "repository.admin",
137 137 "type": "user"
138 138 },
139 139 {
140 140 "name": "user-group-name",
141 141 "origin": "permission",
142 142 "permission": "repository.write",
143 143 "type": "user_group"
144 144 }
145 145 ],
146 146 "owner": "owner-name",
147 147 "permissions": [
148 148 {
149 149 "name": "super-admin-name",
150 150 "origin": "super-admin",
151 151 "permission": "repository.admin",
152 152 "type": "user"
153 153 },
154 154 {
155 155 "name": "owner-name",
156 156 "origin": "owner",
157 157 "permission": "repository.admin",
158 158 "type": "user"
159 159 },
160 160 {
161 161 "name": "user-group-name",
162 162 "origin": "permission",
163 163 "permission": "repository.write",
164 164 "type": "user_group"
165 165 }
166 166 ],
167 167 "private": true,
168 168 "repo_id": 676,
169 169 "repo_name": "user-group/repo-name",
170 170 "repo_type": "hg"
171 171 }
172 172 }
173 173 """
174 174
175 175 repo = get_repo_or_error(repoid)
176 176 cache = Optional.extract(cache)
177 177
178 178 include_secrets = False
179 179 if has_superadmin_permission(apiuser):
180 180 include_secrets = True
181 181 else:
182 182 # check if we have at least read permission for this repo !
183 183 _perms = (
184 184 'repository.admin', 'repository.write', 'repository.read',)
185 185 validate_repo_permissions(apiuser, repoid, repo, _perms)
186 186
187 187 permissions = []
188 188 for _user in repo.permissions():
189 189 user_data = {
190 190 'name': _user.username,
191 191 'permission': _user.permission,
192 192 'origin': get_origin(_user),
193 193 'type': "user",
194 194 }
195 195 permissions.append(user_data)
196 196
197 197 for _user_group in repo.permission_user_groups():
198 198 user_group_data = {
199 199 'name': _user_group.users_group_name,
200 200 'permission': _user_group.permission,
201 201 'origin': get_origin(_user_group),
202 202 'type': "user_group",
203 203 }
204 204 permissions.append(user_group_data)
205 205
206 206 following_users = [
207 207 user.user.get_api_data(include_secrets=include_secrets)
208 208 for user in repo.followers]
209 209
210 210 if not cache:
211 211 repo.update_commit_cache()
212 212 data = repo.get_api_data(include_secrets=include_secrets)
213 213 data['members'] = permissions # TODO: this should be deprecated soon
214 214 data['permissions'] = permissions
215 215 data['followers'] = following_users
216 216 return data
217 217
218 218
219 219 @jsonrpc_method()
220 220 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
221 221 """
222 222 Lists all existing repositories.
223 223
224 224 This command can only be run using an |authtoken| with admin rights,
225 225 or users with at least read rights to |repos|.
226 226
227 227 :param apiuser: This is filled automatically from the |authtoken|.
228 228 :type apiuser: AuthUser
229 229 :param root: specify root repository group to fetch repositories.
230 230 filters the returned repositories to be members of given root group.
231 231 :type root: Optional(None)
232 232 :param traverse: traverse given root into subrepositories. With this flag
233 233 set to False, it will only return top-level repositories from `root`.
234 234 if root is empty it will return just top-level repositories.
235 235 :type traverse: Optional(True)
236 236
237 237
238 238 Example output:
239 239
240 240 .. code-block:: bash
241 241
242 242 id : <id_given_in_input>
243 243 result: [
244 244 {
245 245 "repo_id" : "<repo_id>",
246 246 "repo_name" : "<reponame>"
247 247 "repo_type" : "<repo_type>",
248 248 "clone_uri" : "<clone_uri>",
249 249 "private": : "<bool>",
250 250 "created_on" : "<datetimecreated>",
251 251 "description" : "<description>",
252 252 "landing_rev": "<landing_rev>",
253 253 "owner": "<repo_owner>",
254 254 "fork_of": "<name_of_fork_parent>",
255 255 "enable_downloads": "<bool>",
256 256 "enable_locking": "<bool>",
257 257 "enable_statistics": "<bool>",
258 258 },
259 259 ...
260 260 ]
261 261 error: null
262 262 """
263 263
264 264 include_secrets = has_superadmin_permission(apiuser)
265 265 _perms = ('repository.read', 'repository.write', 'repository.admin',)
266 266 extras = {'user': apiuser}
267 267
268 268 root = Optional.extract(root)
269 269 traverse = Optional.extract(traverse, binary=True)
270 270
271 271 if root:
272 272 # verify parent existance, if it's empty return an error
273 273 parent = RepoGroup.get_by_group_name(root)
274 274 if not parent:
275 275 raise JSONRPCError(
276 276 'Root repository group `{}` does not exist'.format(root))
277 277
278 278 if traverse:
279 279 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
280 280 else:
281 281 repos = RepoModel().get_repos_for_root(root=parent)
282 282 else:
283 283 if traverse:
284 284 repos = RepoModel().get_all()
285 285 else:
286 286 # return just top-level
287 287 repos = RepoModel().get_repos_for_root(root=None)
288 288
289 289 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
290 290 return [repo.get_api_data(include_secrets=include_secrets)
291 291 for repo in repo_list]
292 292
293 293
294 294 @jsonrpc_method()
295 295 def get_repo_changeset(request, apiuser, repoid, revision,
296 296 details=Optional('basic')):
297 297 """
298 298 Returns information about a changeset.
299 299
300 300 Additionally parameters define the amount of details returned by
301 301 this function.
302 302
303 303 This command can only be run using an |authtoken| with admin rights,
304 304 or users with at least read rights to the |repo|.
305 305
306 306 :param apiuser: This is filled automatically from the |authtoken|.
307 307 :type apiuser: AuthUser
308 308 :param repoid: The repository name or repository id
309 309 :type repoid: str or int
310 310 :param revision: revision for which listing should be done
311 311 :type revision: str
312 312 :param details: details can be 'basic|extended|full' full gives diff
313 313 info details like the diff itself, and number of changed files etc.
314 314 :type details: Optional(str)
315 315
316 316 """
317 317 repo = get_repo_or_error(repoid)
318 318 if not has_superadmin_permission(apiuser):
319 319 _perms = (
320 320 'repository.admin', 'repository.write', 'repository.read',)
321 321 validate_repo_permissions(apiuser, repoid, repo, _perms)
322 322
323 323 changes_details = Optional.extract(details)
324 324 _changes_details_types = ['basic', 'extended', 'full']
325 325 if changes_details not in _changes_details_types:
326 326 raise JSONRPCError(
327 327 'ret_type must be one of %s' % (
328 328 ','.join(_changes_details_types)))
329 329
330 330 pre_load = ['author', 'branch', 'date', 'message', 'parents',
331 331 'status', '_commit', '_file_paths']
332 332
333 333 try:
334 334 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
335 335 except TypeError as e:
336 336 raise JSONRPCError(e.message)
337 337 _cs_json = cs.__json__()
338 338 _cs_json['diff'] = build_commit_data(cs, changes_details)
339 339 if changes_details == 'full':
340 340 _cs_json['refs'] = {
341 341 'branches': [cs.branch],
342 342 'bookmarks': getattr(cs, 'bookmarks', []),
343 343 'tags': cs.tags
344 344 }
345 345 return _cs_json
346 346
347 347
348 348 @jsonrpc_method()
349 349 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
350 350 details=Optional('basic')):
351 351 """
352 352 Returns a set of commits limited by the number starting
353 353 from the `start_rev` option.
354 354
355 355 Additional parameters define the amount of details returned by this
356 356 function.
357 357
358 358 This command can only be run using an |authtoken| with admin rights,
359 359 or users with at least read rights to |repos|.
360 360
361 361 :param apiuser: This is filled automatically from the |authtoken|.
362 362 :type apiuser: AuthUser
363 363 :param repoid: The repository name or repository ID.
364 364 :type repoid: str or int
365 365 :param start_rev: The starting revision from where to get changesets.
366 366 :type start_rev: str
367 367 :param limit: Limit the number of commits to this amount
368 368 :type limit: str or int
369 369 :param details: Set the level of detail returned. Valid option are:
370 370 ``basic``, ``extended`` and ``full``.
371 371 :type details: Optional(str)
372 372
373 373 .. note::
374 374
375 375 Setting the parameter `details` to the value ``full`` is extensive
376 376 and returns details like the diff itself, and the number
377 377 of changed files.
378 378
379 379 """
380 380 repo = get_repo_or_error(repoid)
381 381 if not has_superadmin_permission(apiuser):
382 382 _perms = (
383 383 'repository.admin', 'repository.write', 'repository.read',)
384 384 validate_repo_permissions(apiuser, repoid, repo, _perms)
385 385
386 386 changes_details = Optional.extract(details)
387 387 _changes_details_types = ['basic', 'extended', 'full']
388 388 if changes_details not in _changes_details_types:
389 389 raise JSONRPCError(
390 390 'ret_type must be one of %s' % (
391 391 ','.join(_changes_details_types)))
392 392
393 393 limit = int(limit)
394 394 pre_load = ['author', 'branch', 'date', 'message', 'parents',
395 395 'status', '_commit', '_file_paths']
396 396
397 397 vcs_repo = repo.scm_instance()
398 398 # SVN needs a special case to distinguish its index and commit id
399 399 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
400 400 start_rev = vcs_repo.commit_ids[0]
401 401
402 402 try:
403 403 commits = vcs_repo.get_commits(
404 404 start_id=start_rev, pre_load=pre_load)
405 405 except TypeError as e:
406 406 raise JSONRPCError(e.message)
407 407 except Exception:
408 408 log.exception('Fetching of commits failed')
409 409 raise JSONRPCError('Error occurred during commit fetching')
410 410
411 411 ret = []
412 412 for cnt, commit in enumerate(commits):
413 413 if cnt >= limit != -1:
414 414 break
415 415 _cs_json = commit.__json__()
416 416 _cs_json['diff'] = build_commit_data(commit, changes_details)
417 417 if changes_details == 'full':
418 418 _cs_json['refs'] = {
419 419 'branches': [commit.branch],
420 420 'bookmarks': getattr(commit, 'bookmarks', []),
421 421 'tags': commit.tags
422 422 }
423 423 ret.append(_cs_json)
424 424 return ret
425 425
426 426
427 427 @jsonrpc_method()
428 428 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
429 429 ret_type=Optional('all'), details=Optional('basic'),
430 430 max_file_bytes=Optional(None)):
431 431 """
432 432 Returns a list of nodes and children in a flat list for a given
433 433 path at given revision.
434 434
435 435 It's possible to specify ret_type to show only `files` or `dirs`.
436 436
437 437 This command can only be run using an |authtoken| with admin rights,
438 438 or users with at least read rights to |repos|.
439 439
440 440 :param apiuser: This is filled automatically from the |authtoken|.
441 441 :type apiuser: AuthUser
442 442 :param repoid: The repository name or repository ID.
443 443 :type repoid: str or int
444 444 :param revision: The revision for which listing should be done.
445 445 :type revision: str
446 446 :param root_path: The path from which to start displaying.
447 447 :type root_path: str
448 448 :param ret_type: Set the return type. Valid options are
449 449 ``all`` (default), ``files`` and ``dirs``.
450 450 :type ret_type: Optional(str)
451 451 :param details: Returns extended information about nodes, such as
452 452 md5, binary, and or content. The valid options are ``basic`` and
453 453 ``full``.
454 454 :type details: Optional(str)
455 455 :param max_file_bytes: Only return file content under this file size bytes
456 456 :type details: Optional(int)
457 457
458 458 Example output:
459 459
460 460 .. code-block:: bash
461 461
462 462 id : <id_given_in_input>
463 463 result: [
464 464 {
465 465 "name" : "<name>"
466 466 "type" : "<type>",
467 467 "binary": "<true|false>" (only in extended mode)
468 468 "md5" : "<md5 of file content>" (only in extended mode)
469 469 },
470 470 ...
471 471 ]
472 472 error: null
473 473 """
474 474
475 475 repo = get_repo_or_error(repoid)
476 476 if not has_superadmin_permission(apiuser):
477 477 _perms = (
478 478 'repository.admin', 'repository.write', 'repository.read',)
479 479 validate_repo_permissions(apiuser, repoid, repo, _perms)
480 480
481 481 ret_type = Optional.extract(ret_type)
482 482 details = Optional.extract(details)
483 483 _extended_types = ['basic', 'full']
484 484 if details not in _extended_types:
485 485 raise JSONRPCError(
486 486 'ret_type must be one of %s' % (','.join(_extended_types)))
487 487 extended_info = False
488 488 content = False
489 489 if details == 'basic':
490 490 extended_info = True
491 491
492 492 if details == 'full':
493 493 extended_info = content = True
494 494
495 495 _map = {}
496 496 try:
497 497 # check if repo is not empty by any chance, skip quicker if it is.
498 498 _scm = repo.scm_instance()
499 499 if _scm.is_empty():
500 500 return []
501 501
502 502 _d, _f = ScmModel().get_nodes(
503 503 repo, revision, root_path, flat=False,
504 504 extended_info=extended_info, content=content,
505 505 max_file_bytes=max_file_bytes)
506 506 _map = {
507 507 'all': _d + _f,
508 508 'files': _f,
509 509 'dirs': _d,
510 510 }
511 511 return _map[ret_type]
512 512 except KeyError:
513 513 raise JSONRPCError(
514 514 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
515 515 except Exception:
516 516 log.exception("Exception occurred while trying to get repo nodes")
517 517 raise JSONRPCError(
518 518 'failed to get repo: `%s` nodes' % repo.repo_name
519 519 )
520 520
521 521
522 522 @jsonrpc_method()
523 523 def get_repo_refs(request, apiuser, repoid):
524 524 """
525 525 Returns a dictionary of current references. It returns
526 526 bookmarks, branches, closed_branches, and tags for given repository
527 527
528 528 It's possible to specify ret_type to show only `files` or `dirs`.
529 529
530 530 This command can only be run using an |authtoken| with admin rights,
531 531 or users with at least read rights to |repos|.
532 532
533 533 :param apiuser: This is filled automatically from the |authtoken|.
534 534 :type apiuser: AuthUser
535 535 :param repoid: The repository name or repository ID.
536 536 :type repoid: str or int
537 537
538 538 Example output:
539 539
540 540 .. code-block:: bash
541 541
542 542 id : <id_given_in_input>
543 543 "result": {
544 544 "bookmarks": {
545 545 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
546 546 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
547 547 },
548 548 "branches": {
549 549 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
550 550 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
551 551 },
552 552 "branches_closed": {},
553 553 "tags": {
554 554 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
555 555 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
556 556 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
557 557 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
558 558 }
559 559 }
560 560 error: null
561 561 """
562 562
563 563 repo = get_repo_or_error(repoid)
564 564 if not has_superadmin_permission(apiuser):
565 565 _perms = ('repository.admin', 'repository.write', 'repository.read',)
566 566 validate_repo_permissions(apiuser, repoid, repo, _perms)
567 567
568 568 try:
569 569 # check if repo is not empty by any chance, skip quicker if it is.
570 570 vcs_instance = repo.scm_instance()
571 571 refs = vcs_instance.refs()
572 572 return refs
573 573 except Exception:
574 574 log.exception("Exception occurred while trying to get repo refs")
575 575 raise JSONRPCError(
576 576 'failed to get repo: `%s` references' % repo.repo_name
577 577 )
578 578
579 579
580 580 @jsonrpc_method()
581 581 def create_repo(
582 582 request, apiuser, repo_name, repo_type,
583 583 owner=Optional(OAttr('apiuser')),
584 584 description=Optional(''),
585 585 private=Optional(False),
586 586 clone_uri=Optional(None),
587 587 landing_rev=Optional('rev:tip'),
588 588 enable_statistics=Optional(False),
589 589 enable_locking=Optional(False),
590 590 enable_downloads=Optional(False),
591 591 copy_permissions=Optional(False)):
592 592 """
593 593 Creates a repository.
594 594
595 595 * If the repository name contains "/", repository will be created inside
596 596 a repository group or nested repository groups
597 597
598 598 For example "foo/bar/repo1" will create |repo| called "repo1" inside
599 599 group "foo/bar". You have to have permissions to access and write to
600 600 the last repository group ("bar" in this example)
601 601
602 602 This command can only be run using an |authtoken| with at least
603 603 permissions to create repositories, or write permissions to
604 604 parent repository groups.
605 605
606 606 :param apiuser: This is filled automatically from the |authtoken|.
607 607 :type apiuser: AuthUser
608 608 :param repo_name: Set the repository name.
609 609 :type repo_name: str
610 610 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
611 611 :type repo_type: str
612 612 :param owner: user_id or username
613 613 :type owner: Optional(str)
614 614 :param description: Set the repository description.
615 615 :type description: Optional(str)
616 616 :param private: set repository as private
617 617 :type private: bool
618 618 :param clone_uri: set clone_uri
619 619 :type clone_uri: str
620 620 :param landing_rev: <rev_type>:<rev>
621 621 :type landing_rev: str
622 622 :param enable_locking:
623 623 :type enable_locking: bool
624 624 :param enable_downloads:
625 625 :type enable_downloads: bool
626 626 :param enable_statistics:
627 627 :type enable_statistics: bool
628 628 :param copy_permissions: Copy permission from group in which the
629 629 repository is being created.
630 630 :type copy_permissions: bool
631 631
632 632
633 633 Example output:
634 634
635 635 .. code-block:: bash
636 636
637 637 id : <id_given_in_input>
638 638 result: {
639 639 "msg": "Created new repository `<reponame>`",
640 640 "success": true,
641 641 "task": "<celery task id or None if done sync>"
642 642 }
643 643 error: null
644 644
645 645
646 646 Example error output:
647 647
648 648 .. code-block:: bash
649 649
650 650 id : <id_given_in_input>
651 651 result : null
652 652 error : {
653 653 'failed to create repository `<repo_name>`'
654 654 }
655 655
656 656 """
657 657
658 658 owner = validate_set_owner_permissions(apiuser, owner)
659 659
660 660 description = Optional.extract(description)
661 661 copy_permissions = Optional.extract(copy_permissions)
662 662 clone_uri = Optional.extract(clone_uri)
663 663 landing_commit_ref = Optional.extract(landing_rev)
664 664
665 665 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
666 666 if isinstance(private, Optional):
667 667 private = defs.get('repo_private') or Optional.extract(private)
668 668 if isinstance(repo_type, Optional):
669 669 repo_type = defs.get('repo_type')
670 670 if isinstance(enable_statistics, Optional):
671 671 enable_statistics = defs.get('repo_enable_statistics')
672 672 if isinstance(enable_locking, Optional):
673 673 enable_locking = defs.get('repo_enable_locking')
674 674 if isinstance(enable_downloads, Optional):
675 675 enable_downloads = defs.get('repo_enable_downloads')
676 676
677 677 schema = repo_schema.RepoSchema().bind(
678 678 repo_type_options=rhodecode.BACKENDS.keys(),
679 679 # user caller
680 680 user=apiuser)
681 681
682 682 try:
683 683 schema_data = schema.deserialize(dict(
684 684 repo_name=repo_name,
685 685 repo_type=repo_type,
686 686 repo_owner=owner.username,
687 687 repo_description=description,
688 688 repo_landing_commit_ref=landing_commit_ref,
689 689 repo_clone_uri=clone_uri,
690 690 repo_private=private,
691 691 repo_copy_permissions=copy_permissions,
692 692 repo_enable_statistics=enable_statistics,
693 693 repo_enable_downloads=enable_downloads,
694 694 repo_enable_locking=enable_locking))
695 695 except validation_schema.Invalid as err:
696 696 raise JSONRPCValidationError(colander_exc=err)
697 697
698 698 try:
699 699 data = {
700 700 'owner': owner,
701 701 'repo_name': schema_data['repo_group']['repo_name_without_group'],
702 702 'repo_name_full': schema_data['repo_name'],
703 703 'repo_group': schema_data['repo_group']['repo_group_id'],
704 704 'repo_type': schema_data['repo_type'],
705 705 'repo_description': schema_data['repo_description'],
706 706 'repo_private': schema_data['repo_private'],
707 707 'clone_uri': schema_data['repo_clone_uri'],
708 708 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
709 709 'enable_statistics': schema_data['repo_enable_statistics'],
710 710 'enable_locking': schema_data['repo_enable_locking'],
711 711 'enable_downloads': schema_data['repo_enable_downloads'],
712 712 'repo_copy_permissions': schema_data['repo_copy_permissions'],
713 713 }
714 714
715 715 task = RepoModel().create(form_data=data, cur_user=owner)
716 716 from celery.result import BaseAsyncResult
717 717 task_id = None
718 718 if isinstance(task, BaseAsyncResult):
719 719 task_id = task.task_id
720 720 # no commit, it's done in RepoModel, or async via celery
721 721 return {
722 722 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
723 723 'success': True, # cannot return the repo data here since fork
724 724 # can be done async
725 725 'task': task_id
726 726 }
727 727 except Exception:
728 728 log.exception(
729 729 u"Exception while trying to create the repository %s",
730 730 schema_data['repo_name'])
731 731 raise JSONRPCError(
732 732 'failed to create repository `%s`' % (schema_data['repo_name'],))
733 733
734 734
735 735 @jsonrpc_method()
736 736 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
737 737 description=Optional('')):
738 738 """
739 739 Adds an extra field to a repository.
740 740
741 741 This command can only be run using an |authtoken| with at least
742 742 write permissions to the |repo|.
743 743
744 744 :param apiuser: This is filled automatically from the |authtoken|.
745 745 :type apiuser: AuthUser
746 746 :param repoid: Set the repository name or repository id.
747 747 :type repoid: str or int
748 748 :param key: Create a unique field key for this repository.
749 749 :type key: str
750 750 :param label:
751 751 :type label: Optional(str)
752 752 :param description:
753 753 :type description: Optional(str)
754 754 """
755 755 repo = get_repo_or_error(repoid)
756 756 if not has_superadmin_permission(apiuser):
757 757 _perms = ('repository.admin',)
758 758 validate_repo_permissions(apiuser, repoid, repo, _perms)
759 759
760 760 label = Optional.extract(label) or key
761 761 description = Optional.extract(description)
762 762
763 763 field = RepositoryField.get_by_key_name(key, repo)
764 764 if field:
765 765 raise JSONRPCError('Field with key '
766 766 '`%s` exists for repo `%s`' % (key, repoid))
767 767
768 768 try:
769 769 RepoModel().add_repo_field(repo, key, field_label=label,
770 770 field_desc=description)
771 771 Session().commit()
772 772 return {
773 773 'msg': "Added new repository field `%s`" % (key,),
774 774 'success': True,
775 775 }
776 776 except Exception:
777 777 log.exception("Exception occurred while trying to add field to repo")
778 778 raise JSONRPCError(
779 779 'failed to create new field for repository `%s`' % (repoid,))
780 780
781 781
782 782 @jsonrpc_method()
783 783 def remove_field_from_repo(request, apiuser, repoid, key):
784 784 """
785 785 Removes an extra field from a repository.
786 786
787 787 This command can only be run using an |authtoken| with at least
788 788 write permissions to the |repo|.
789 789
790 790 :param apiuser: This is filled automatically from the |authtoken|.
791 791 :type apiuser: AuthUser
792 792 :param repoid: Set the repository name or repository ID.
793 793 :type repoid: str or int
794 794 :param key: Set the unique field key for this repository.
795 795 :type key: str
796 796 """
797 797
798 798 repo = get_repo_or_error(repoid)
799 799 if not has_superadmin_permission(apiuser):
800 800 _perms = ('repository.admin',)
801 801 validate_repo_permissions(apiuser, repoid, repo, _perms)
802 802
803 803 field = RepositoryField.get_by_key_name(key, repo)
804 804 if not field:
805 805 raise JSONRPCError('Field with key `%s` does not '
806 806 'exists for repo `%s`' % (key, repoid))
807 807
808 808 try:
809 809 RepoModel().delete_repo_field(repo, field_key=key)
810 810 Session().commit()
811 811 return {
812 812 'msg': "Deleted repository field `%s`" % (key,),
813 813 'success': True,
814 814 }
815 815 except Exception:
816 816 log.exception(
817 817 "Exception occurred while trying to delete field from repo")
818 818 raise JSONRPCError(
819 819 'failed to delete field for repository `%s`' % (repoid,))
820 820
821 821
822 822 @jsonrpc_method()
823 823 def update_repo(
824 824 request, apiuser, repoid, repo_name=Optional(None),
825 825 owner=Optional(OAttr('apiuser')), description=Optional(''),
826 826 private=Optional(False), clone_uri=Optional(None),
827 827 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
828 828 enable_statistics=Optional(False),
829 829 enable_locking=Optional(False),
830 830 enable_downloads=Optional(False), fields=Optional('')):
831 831 """
832 832 Updates a repository with the given information.
833 833
834 834 This command can only be run using an |authtoken| with at least
835 835 admin permissions to the |repo|.
836 836
837 837 * If the repository name contains "/", repository will be updated
838 838 accordingly with a repository group or nested repository groups
839 839
840 840 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
841 841 called "repo-test" and place it inside group "foo/bar".
842 842 You have to have permissions to access and write to the last repository
843 843 group ("bar" in this example)
844 844
845 845 :param apiuser: This is filled automatically from the |authtoken|.
846 846 :type apiuser: AuthUser
847 847 :param repoid: repository name or repository ID.
848 848 :type repoid: str or int
849 849 :param repo_name: Update the |repo| name, including the
850 850 repository group it's in.
851 851 :type repo_name: str
852 852 :param owner: Set the |repo| owner.
853 853 :type owner: str
854 854 :param fork_of: Set the |repo| as fork of another |repo|.
855 855 :type fork_of: str
856 856 :param description: Update the |repo| description.
857 857 :type description: str
858 858 :param private: Set the |repo| as private. (True | False)
859 859 :type private: bool
860 860 :param clone_uri: Update the |repo| clone URI.
861 861 :type clone_uri: str
862 862 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
863 863 :type landing_rev: str
864 864 :param enable_statistics: Enable statistics on the |repo|, (True | False).
865 865 :type enable_statistics: bool
866 866 :param enable_locking: Enable |repo| locking.
867 867 :type enable_locking: bool
868 868 :param enable_downloads: Enable downloads from the |repo|, (True | False).
869 869 :type enable_downloads: bool
870 870 :param fields: Add extra fields to the |repo|. Use the following
871 871 example format: ``field_key=field_val,field_key2=fieldval2``.
872 872 Escape ', ' with \,
873 873 :type fields: str
874 874 """
875 875
876 876 repo = get_repo_or_error(repoid)
877 877
878 878 include_secrets = False
879 879 if not has_superadmin_permission(apiuser):
880 880 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
881 881 else:
882 882 include_secrets = True
883 883
884 884 updates = dict(
885 885 repo_name=repo_name
886 886 if not isinstance(repo_name, Optional) else repo.repo_name,
887 887
888 888 fork_id=fork_of
889 889 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
890 890
891 891 user=owner
892 892 if not isinstance(owner, Optional) else repo.user.username,
893 893
894 894 repo_description=description
895 895 if not isinstance(description, Optional) else repo.description,
896 896
897 897 repo_private=private
898 898 if not isinstance(private, Optional) else repo.private,
899 899
900 900 clone_uri=clone_uri
901 901 if not isinstance(clone_uri, Optional) else repo.clone_uri,
902 902
903 903 repo_landing_rev=landing_rev
904 904 if not isinstance(landing_rev, Optional) else repo._landing_revision,
905 905
906 906 repo_enable_statistics=enable_statistics
907 907 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
908 908
909 909 repo_enable_locking=enable_locking
910 910 if not isinstance(enable_locking, Optional) else repo.enable_locking,
911 911
912 912 repo_enable_downloads=enable_downloads
913 913 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
914 914
915 915 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
916 916
917 917 schema = repo_schema.RepoSchema().bind(
918 918 repo_type_options=rhodecode.BACKENDS.keys(),
919 919 repo_ref_options=ref_choices,
920 920 # user caller
921 921 user=apiuser,
922 922 old_values=repo.get_api_data())
923 923 try:
924 924 schema_data = schema.deserialize(dict(
925 925 # we save old value, users cannot change type
926 926 repo_type=repo.repo_type,
927 927
928 928 repo_name=updates['repo_name'],
929 929 repo_owner=updates['user'],
930 930 repo_description=updates['repo_description'],
931 931 repo_clone_uri=updates['clone_uri'],
932 932 repo_fork_of=updates['fork_id'],
933 933 repo_private=updates['repo_private'],
934 934 repo_landing_commit_ref=updates['repo_landing_rev'],
935 935 repo_enable_statistics=updates['repo_enable_statistics'],
936 936 repo_enable_downloads=updates['repo_enable_downloads'],
937 937 repo_enable_locking=updates['repo_enable_locking']))
938 938 except validation_schema.Invalid as err:
939 939 raise JSONRPCValidationError(colander_exc=err)
940 940
941 941 # save validated data back into the updates dict
942 942 validated_updates = dict(
943 943 repo_name=schema_data['repo_group']['repo_name_without_group'],
944 944 repo_group=schema_data['repo_group']['repo_group_id'],
945 945
946 946 user=schema_data['repo_owner'],
947 947 repo_description=schema_data['repo_description'],
948 948 repo_private=schema_data['repo_private'],
949 949 clone_uri=schema_data['repo_clone_uri'],
950 950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
951 951 repo_enable_statistics=schema_data['repo_enable_statistics'],
952 952 repo_enable_locking=schema_data['repo_enable_locking'],
953 953 repo_enable_downloads=schema_data['repo_enable_downloads'],
954 954 )
955 955
956 956 if schema_data['repo_fork_of']:
957 957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
958 958 validated_updates['fork_id'] = fork_repo.repo_id
959 959
960 960 # extra fields
961 961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
962 962 if fields:
963 963 validated_updates.update(fields)
964 964
965 965 try:
966 966 RepoModel().update(repo, **validated_updates)
967 967 Session().commit()
968 968 return {
969 969 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
970 970 'repository': repo.get_api_data(include_secrets=include_secrets)
971 971 }
972 972 except Exception:
973 973 log.exception(
974 974 u"Exception while trying to update the repository %s",
975 975 repoid)
976 976 raise JSONRPCError('failed to update repo `%s`' % repoid)
977 977
978 978
979 979 @jsonrpc_method()
980 980 def fork_repo(request, apiuser, repoid, fork_name,
981 981 owner=Optional(OAttr('apiuser')),
982 982 description=Optional(''),
983 983 private=Optional(False),
984 984 clone_uri=Optional(None),
985 985 landing_rev=Optional('rev:tip'),
986 986 copy_permissions=Optional(False)):
987 987 """
988 988 Creates a fork of the specified |repo|.
989 989
990 990 * If the fork_name contains "/", fork will be created inside
991 991 a repository group or nested repository groups
992 992
993 993 For example "foo/bar/fork-repo" will create fork called "fork-repo"
994 994 inside group "foo/bar". You have to have permissions to access and
995 995 write to the last repository group ("bar" in this example)
996 996
997 997 This command can only be run using an |authtoken| with minimum
998 998 read permissions of the forked repo, create fork permissions for an user.
999 999
1000 1000 :param apiuser: This is filled automatically from the |authtoken|.
1001 1001 :type apiuser: AuthUser
1002 1002 :param repoid: Set repository name or repository ID.
1003 1003 :type repoid: str or int
1004 1004 :param fork_name: Set the fork name, including it's repository group membership.
1005 1005 :type fork_name: str
1006 1006 :param owner: Set the fork owner.
1007 1007 :type owner: str
1008 1008 :param description: Set the fork description.
1009 1009 :type description: str
1010 1010 :param copy_permissions: Copy permissions from parent |repo|. The
1011 1011 default is False.
1012 1012 :type copy_permissions: bool
1013 1013 :param private: Make the fork private. The default is False.
1014 1014 :type private: bool
1015 1015 :param landing_rev: Set the landing revision. The default is tip.
1016 1016
1017 1017 Example output:
1018 1018
1019 1019 .. code-block:: bash
1020 1020
1021 1021 id : <id_for_response>
1022 1022 api_key : "<api_key>"
1023 1023 args: {
1024 1024 "repoid" : "<reponame or repo_id>",
1025 1025 "fork_name": "<forkname>",
1026 1026 "owner": "<username or user_id = Optional(=apiuser)>",
1027 1027 "description": "<description>",
1028 1028 "copy_permissions": "<bool>",
1029 1029 "private": "<bool>",
1030 1030 "landing_rev": "<landing_rev>"
1031 1031 }
1032 1032
1033 1033 Example error output:
1034 1034
1035 1035 .. code-block:: bash
1036 1036
1037 1037 id : <id_given_in_input>
1038 1038 result: {
1039 1039 "msg": "Created fork of `<reponame>` as `<forkname>`",
1040 1040 "success": true,
1041 1041 "task": "<celery task id or None if done sync>"
1042 1042 }
1043 1043 error: null
1044 1044
1045 1045 """
1046 1046
1047 1047 repo = get_repo_or_error(repoid)
1048 1048 repo_name = repo.repo_name
1049 1049
1050 1050 if not has_superadmin_permission(apiuser):
1051 1051 # check if we have at least read permission for
1052 1052 # this repo that we fork !
1053 1053 _perms = (
1054 1054 'repository.admin', 'repository.write', 'repository.read')
1055 1055 validate_repo_permissions(apiuser, repoid, repo, _perms)
1056 1056
1057 1057 # check if the regular user has at least fork permissions as well
1058 1058 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1059 1059 raise JSONRPCForbidden()
1060 1060
1061 1061 # check if user can set owner parameter
1062 1062 owner = validate_set_owner_permissions(apiuser, owner)
1063 1063
1064 1064 description = Optional.extract(description)
1065 1065 copy_permissions = Optional.extract(copy_permissions)
1066 1066 clone_uri = Optional.extract(clone_uri)
1067 1067 landing_commit_ref = Optional.extract(landing_rev)
1068 1068 private = Optional.extract(private)
1069 1069
1070 1070 schema = repo_schema.RepoSchema().bind(
1071 1071 repo_type_options=rhodecode.BACKENDS.keys(),
1072 1072 # user caller
1073 1073 user=apiuser)
1074 1074
1075 1075 try:
1076 1076 schema_data = schema.deserialize(dict(
1077 1077 repo_name=fork_name,
1078 1078 repo_type=repo.repo_type,
1079 1079 repo_owner=owner.username,
1080 1080 repo_description=description,
1081 1081 repo_landing_commit_ref=landing_commit_ref,
1082 1082 repo_clone_uri=clone_uri,
1083 1083 repo_private=private,
1084 1084 repo_copy_permissions=copy_permissions))
1085 1085 except validation_schema.Invalid as err:
1086 1086 raise JSONRPCValidationError(colander_exc=err)
1087 1087
1088 1088 try:
1089 1089 data = {
1090 1090 'fork_parent_id': repo.repo_id,
1091 1091
1092 1092 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1093 1093 'repo_name_full': schema_data['repo_name'],
1094 1094 'repo_group': schema_data['repo_group']['repo_group_id'],
1095 1095 'repo_type': schema_data['repo_type'],
1096 1096 'description': schema_data['repo_description'],
1097 1097 'private': schema_data['repo_private'],
1098 1098 'copy_permissions': schema_data['repo_copy_permissions'],
1099 1099 'landing_rev': schema_data['repo_landing_commit_ref'],
1100 1100 }
1101 1101
1102 1102 task = RepoModel().create_fork(data, cur_user=owner)
1103 1103 # no commit, it's done in RepoModel, or async via celery
1104 1104 from celery.result import BaseAsyncResult
1105 1105 task_id = None
1106 1106 if isinstance(task, BaseAsyncResult):
1107 1107 task_id = task.task_id
1108 1108 return {
1109 1109 'msg': 'Created fork of `%s` as `%s`' % (
1110 1110 repo.repo_name, schema_data['repo_name']),
1111 1111 'success': True, # cannot return the repo data here since fork
1112 1112 # can be done async
1113 1113 'task': task_id
1114 1114 }
1115 1115 except Exception:
1116 1116 log.exception(
1117 1117 u"Exception while trying to create fork %s",
1118 1118 schema_data['repo_name'])
1119 1119 raise JSONRPCError(
1120 1120 'failed to fork repository `%s` as `%s`' % (
1121 1121 repo_name, schema_data['repo_name']))
1122 1122
1123 1123
1124 1124 @jsonrpc_method()
1125 1125 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1126 1126 """
1127 1127 Deletes a repository.
1128 1128
1129 1129 * When the `forks` parameter is set it's possible to detach or delete
1130 1130 forks of deleted repository.
1131 1131
1132 1132 This command can only be run using an |authtoken| with admin
1133 1133 permissions on the |repo|.
1134 1134
1135 1135 :param apiuser: This is filled automatically from the |authtoken|.
1136 1136 :type apiuser: AuthUser
1137 1137 :param repoid: Set the repository name or repository ID.
1138 1138 :type repoid: str or int
1139 1139 :param forks: Set to `detach` or `delete` forks from the |repo|.
1140 1140 :type forks: Optional(str)
1141 1141
1142 1142 Example error output:
1143 1143
1144 1144 .. code-block:: bash
1145 1145
1146 1146 id : <id_given_in_input>
1147 1147 result: {
1148 1148 "msg": "Deleted repository `<reponame>`",
1149 1149 "success": true
1150 1150 }
1151 1151 error: null
1152 1152 """
1153 1153
1154 1154 repo = get_repo_or_error(repoid)
1155 1155 if not has_superadmin_permission(apiuser):
1156 1156 _perms = ('repository.admin',)
1157 1157 validate_repo_permissions(apiuser, repoid, repo, _perms)
1158 1158
1159 1159 try:
1160 1160 handle_forks = Optional.extract(forks)
1161 1161 _forks_msg = ''
1162 1162 _forks = [f for f in repo.forks]
1163 1163 if handle_forks == 'detach':
1164 1164 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1165 1165 elif handle_forks == 'delete':
1166 1166 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1167 1167 elif _forks:
1168 1168 raise JSONRPCError(
1169 1169 'Cannot delete `%s` it still contains attached forks' %
1170 1170 (repo.repo_name,)
1171 1171 )
1172 1172
1173 1173 RepoModel().delete(repo, forks=forks)
1174 1174 Session().commit()
1175 1175 return {
1176 1176 'msg': 'Deleted repository `%s`%s' % (
1177 1177 repo.repo_name, _forks_msg),
1178 1178 'success': True
1179 1179 }
1180 1180 except Exception:
1181 1181 log.exception("Exception occurred while trying to delete repo")
1182 1182 raise JSONRPCError(
1183 1183 'failed to delete repository `%s`' % (repo.repo_name,)
1184 1184 )
1185 1185
1186 1186
1187 1187 #TODO: marcink, change name ?
1188 1188 @jsonrpc_method()
1189 1189 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1190 1190 """
1191 1191 Invalidates the cache for the specified repository.
1192 1192
1193 1193 This command can only be run using an |authtoken| with admin rights to
1194 1194 the specified repository.
1195 1195
1196 1196 This command takes the following options:
1197 1197
1198 1198 :param apiuser: This is filled automatically from |authtoken|.
1199 1199 :type apiuser: AuthUser
1200 1200 :param repoid: Sets the repository name or repository ID.
1201 1201 :type repoid: str or int
1202 1202 :param delete_keys: This deletes the invalidated keys instead of
1203 1203 just flagging them.
1204 1204 :type delete_keys: Optional(``True`` | ``False``)
1205 1205
1206 1206 Example output:
1207 1207
1208 1208 .. code-block:: bash
1209 1209
1210 1210 id : <id_given_in_input>
1211 1211 result : {
1212 1212 'msg': Cache for repository `<repository name>` was invalidated,
1213 1213 'repository': <repository name>
1214 1214 }
1215 1215 error : null
1216 1216
1217 1217 Example error output:
1218 1218
1219 1219 .. code-block:: bash
1220 1220
1221 1221 id : <id_given_in_input>
1222 1222 result : null
1223 1223 error : {
1224 1224 'Error occurred during cache invalidation action'
1225 1225 }
1226 1226
1227 1227 """
1228 1228
1229 1229 repo = get_repo_or_error(repoid)
1230 1230 if not has_superadmin_permission(apiuser):
1231 1231 _perms = ('repository.admin', 'repository.write',)
1232 1232 validate_repo_permissions(apiuser, repoid, repo, _perms)
1233 1233
1234 1234 delete = Optional.extract(delete_keys)
1235 1235 try:
1236 1236 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1237 1237 return {
1238 1238 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1239 1239 'repository': repo.repo_name
1240 1240 }
1241 1241 except Exception:
1242 1242 log.exception(
1243 1243 "Exception occurred while trying to invalidate repo cache")
1244 1244 raise JSONRPCError(
1245 1245 'Error occurred during cache invalidation action'
1246 1246 )
1247 1247
1248 1248
1249 1249 #TODO: marcink, change name ?
1250 1250 @jsonrpc_method()
1251 1251 def lock(request, apiuser, repoid, locked=Optional(None),
1252 1252 userid=Optional(OAttr('apiuser'))):
1253 1253 """
1254 1254 Sets the lock state of the specified |repo| by the given user.
1255 1255 From more information, see :ref:`repo-locking`.
1256 1256
1257 1257 * If the ``userid`` option is not set, the repository is locked to the
1258 1258 user who called the method.
1259 1259 * If the ``locked`` parameter is not set, the current lock state of the
1260 1260 repository is displayed.
1261 1261
1262 1262 This command can only be run using an |authtoken| with admin rights to
1263 1263 the specified repository.
1264 1264
1265 1265 This command takes the following options:
1266 1266
1267 1267 :param apiuser: This is filled automatically from the |authtoken|.
1268 1268 :type apiuser: AuthUser
1269 1269 :param repoid: Sets the repository name or repository ID.
1270 1270 :type repoid: str or int
1271 1271 :param locked: Sets the lock state.
1272 1272 :type locked: Optional(``True`` | ``False``)
1273 1273 :param userid: Set the repository lock to this user.
1274 1274 :type userid: Optional(str or int)
1275 1275
1276 1276 Example error output:
1277 1277
1278 1278 .. code-block:: bash
1279 1279
1280 1280 id : <id_given_in_input>
1281 1281 result : {
1282 1282 'repo': '<reponame>',
1283 1283 'locked': <bool: lock state>,
1284 1284 'locked_since': <int: lock timestamp>,
1285 1285 'locked_by': <username of person who made the lock>,
1286 1286 'lock_reason': <str: reason for locking>,
1287 1287 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1288 1288 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1289 1289 or
1290 1290 'msg': 'Repo `<repository name>` not locked.'
1291 1291 or
1292 1292 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1293 1293 }
1294 1294 error : null
1295 1295
1296 1296 Example error output:
1297 1297
1298 1298 .. code-block:: bash
1299 1299
1300 1300 id : <id_given_in_input>
1301 1301 result : null
1302 1302 error : {
1303 1303 'Error occurred locking repository `<reponame>`'
1304 1304 }
1305 1305 """
1306 1306
1307 1307 repo = get_repo_or_error(repoid)
1308 1308 if not has_superadmin_permission(apiuser):
1309 1309 # check if we have at least write permission for this repo !
1310 1310 _perms = ('repository.admin', 'repository.write',)
1311 1311 validate_repo_permissions(apiuser, repoid, repo, _perms)
1312 1312
1313 1313 # make sure normal user does not pass someone else userid,
1314 1314 # he is not allowed to do that
1315 1315 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1316 1316 raise JSONRPCError('userid is not the same as your user')
1317 1317
1318 1318 if isinstance(userid, Optional):
1319 1319 userid = apiuser.user_id
1320 1320
1321 1321 user = get_user_or_error(userid)
1322 1322
1323 1323 if isinstance(locked, Optional):
1324 1324 lockobj = repo.locked
1325 1325
1326 1326 if lockobj[0] is None:
1327 1327 _d = {
1328 1328 'repo': repo.repo_name,
1329 1329 'locked': False,
1330 1330 'locked_since': None,
1331 1331 'locked_by': None,
1332 1332 'lock_reason': None,
1333 1333 'lock_state_changed': False,
1334 1334 'msg': 'Repo `%s` not locked.' % repo.repo_name
1335 1335 }
1336 1336 return _d
1337 1337 else:
1338 1338 _user_id, _time, _reason = lockobj
1339 1339 lock_user = get_user_or_error(userid)
1340 1340 _d = {
1341 1341 'repo': repo.repo_name,
1342 1342 'locked': True,
1343 1343 'locked_since': _time,
1344 1344 'locked_by': lock_user.username,
1345 1345 'lock_reason': _reason,
1346 1346 'lock_state_changed': False,
1347 1347 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1348 1348 % (repo.repo_name, lock_user.username,
1349 1349 json.dumps(time_to_datetime(_time))))
1350 1350 }
1351 1351 return _d
1352 1352
1353 1353 # force locked state through a flag
1354 1354 else:
1355 1355 locked = str2bool(locked)
1356 1356 lock_reason = Repository.LOCK_API
1357 1357 try:
1358 1358 if locked:
1359 1359 lock_time = time.time()
1360 1360 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1361 1361 else:
1362 1362 lock_time = None
1363 1363 Repository.unlock(repo)
1364 1364 _d = {
1365 1365 'repo': repo.repo_name,
1366 1366 'locked': locked,
1367 1367 'locked_since': lock_time,
1368 1368 'locked_by': user.username,
1369 1369 'lock_reason': lock_reason,
1370 1370 'lock_state_changed': True,
1371 1371 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1372 1372 % (user.username, repo.repo_name, locked))
1373 1373 }
1374 1374 return _d
1375 1375 except Exception:
1376 1376 log.exception(
1377 1377 "Exception occurred while trying to lock repository")
1378 1378 raise JSONRPCError(
1379 1379 'Error occurred locking repository `%s`' % repo.repo_name
1380 1380 )
1381 1381
1382 1382
1383 1383 @jsonrpc_method()
1384 1384 def comment_commit(
1385 1385 request, apiuser, repoid, commit_id, message,
1386 1386 userid=Optional(OAttr('apiuser')), status=Optional(None)):
1387 1387 """
1388 1388 Set a commit comment, and optionally change the status of the commit.
1389 1389
1390 1390 :param apiuser: This is filled automatically from the |authtoken|.
1391 1391 :type apiuser: AuthUser
1392 1392 :param repoid: Set the repository name or repository ID.
1393 1393 :type repoid: str or int
1394 1394 :param commit_id: Specify the commit_id for which to set a comment.
1395 1395 :type commit_id: str
1396 1396 :param message: The comment text.
1397 1397 :type message: str
1398 1398 :param userid: Set the user name of the comment creator.
1399 1399 :type userid: Optional(str or int)
1400 1400 :param status: status, one of 'not_reviewed', 'approved', 'rejected',
1401 1401 'under_review'
1402 1402 :type status: str
1403 1403
1404 1404 Example error output:
1405 1405
1406 1406 .. code-block:: json
1407 1407
1408 1408 {
1409 1409 "id" : <id_given_in_input>,
1410 1410 "result" : {
1411 1411 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1412 1412 "status_change": null or <status>,
1413 1413 "success": true
1414 1414 },
1415 1415 "error" : null
1416 1416 }
1417 1417
1418 1418 """
1419 1419 repo = get_repo_or_error(repoid)
1420 1420 if not has_superadmin_permission(apiuser):
1421 1421 _perms = ('repository.read', 'repository.write', 'repository.admin')
1422 1422 validate_repo_permissions(apiuser, repoid, repo, _perms)
1423 1423
1424 1424 if isinstance(userid, Optional):
1425 1425 userid = apiuser.user_id
1426 1426
1427 1427 user = get_user_or_error(userid)
1428 1428 status = Optional.extract(status)
1429 1429
1430 1430 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1431 1431 if status and status not in allowed_statuses:
1432 1432 raise JSONRPCError('Bad status, must be on '
1433 1433 'of %s got %s' % (allowed_statuses, status,))
1434 1434
1435 1435 try:
1436 1436 rc_config = SettingsModel().get_all_settings()
1437 1437 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1438 1438 status_change_label = ChangesetStatus.get_status_lbl(status)
1439 1439 comm = ChangesetCommentsModel().create(
1440 message, repo, user, revision=commit_id,
1440 message, repo, user, commit_id=commit_id,
1441 1441 status_change=status_change_label,
1442 1442 status_change_type=status,
1443 1443 renderer=renderer)
1444 1444 if status:
1445 1445 # also do a status change
1446 1446 try:
1447 1447 ChangesetStatusModel().set_status(
1448 1448 repo, status, user, comm, revision=commit_id,
1449 1449 dont_allow_on_closed_pull_request=True
1450 1450 )
1451 1451 except StatusChangeOnClosedPullRequestError:
1452 1452 log.exception(
1453 1453 "Exception occurred while trying to change repo commit status")
1454 1454 msg = ('Changing status on a changeset associated with '
1455 1455 'a closed pull request is not allowed')
1456 1456 raise JSONRPCError(msg)
1457 1457
1458 1458 Session().commit()
1459 1459 return {
1460 1460 'msg': (
1461 1461 'Commented on commit `%s` for repository `%s`' % (
1462 1462 comm.revision, repo.repo_name)),
1463 1463 'status_change': status,
1464 1464 'success': True,
1465 1465 }
1466 1466 except JSONRPCError:
1467 1467 # catch any inside errors, and re-raise them to prevent from
1468 1468 # below global catch to silence them
1469 1469 raise
1470 1470 except Exception:
1471 1471 log.exception("Exception occurred while trying to comment on commit")
1472 1472 raise JSONRPCError(
1473 1473 'failed to set comment on repository `%s`' % (repo.repo_name,)
1474 1474 )
1475 1475
1476 1476
1477 1477 @jsonrpc_method()
1478 1478 def grant_user_permission(request, apiuser, repoid, userid, perm):
1479 1479 """
1480 1480 Grant permissions for the specified user on the given repository,
1481 1481 or update existing permissions if found.
1482 1482
1483 1483 This command can only be run using an |authtoken| with admin
1484 1484 permissions on the |repo|.
1485 1485
1486 1486 :param apiuser: This is filled automatically from the |authtoken|.
1487 1487 :type apiuser: AuthUser
1488 1488 :param repoid: Set the repository name or repository ID.
1489 1489 :type repoid: str or int
1490 1490 :param userid: Set the user name.
1491 1491 :type userid: str
1492 1492 :param perm: Set the user permissions, using the following format
1493 1493 ``(repository.(none|read|write|admin))``
1494 1494 :type perm: str
1495 1495
1496 1496 Example output:
1497 1497
1498 1498 .. code-block:: bash
1499 1499
1500 1500 id : <id_given_in_input>
1501 1501 result: {
1502 1502 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1503 1503 "success": true
1504 1504 }
1505 1505 error: null
1506 1506 """
1507 1507
1508 1508 repo = get_repo_or_error(repoid)
1509 1509 user = get_user_or_error(userid)
1510 1510 perm = get_perm_or_error(perm)
1511 1511 if not has_superadmin_permission(apiuser):
1512 1512 _perms = ('repository.admin',)
1513 1513 validate_repo_permissions(apiuser, repoid, repo, _perms)
1514 1514
1515 1515 try:
1516 1516
1517 1517 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1518 1518
1519 1519 Session().commit()
1520 1520 return {
1521 1521 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1522 1522 perm.permission_name, user.username, repo.repo_name
1523 1523 ),
1524 1524 'success': True
1525 1525 }
1526 1526 except Exception:
1527 1527 log.exception(
1528 1528 "Exception occurred while trying edit permissions for repo")
1529 1529 raise JSONRPCError(
1530 1530 'failed to edit permission for user: `%s` in repo: `%s`' % (
1531 1531 userid, repoid
1532 1532 )
1533 1533 )
1534 1534
1535 1535
1536 1536 @jsonrpc_method()
1537 1537 def revoke_user_permission(request, apiuser, repoid, userid):
1538 1538 """
1539 1539 Revoke permission for a user on the specified repository.
1540 1540
1541 1541 This command can only be run using an |authtoken| with admin
1542 1542 permissions on the |repo|.
1543 1543
1544 1544 :param apiuser: This is filled automatically from the |authtoken|.
1545 1545 :type apiuser: AuthUser
1546 1546 :param repoid: Set the repository name or repository ID.
1547 1547 :type repoid: str or int
1548 1548 :param userid: Set the user name of revoked user.
1549 1549 :type userid: str or int
1550 1550
1551 1551 Example error output:
1552 1552
1553 1553 .. code-block:: bash
1554 1554
1555 1555 id : <id_given_in_input>
1556 1556 result: {
1557 1557 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1558 1558 "success": true
1559 1559 }
1560 1560 error: null
1561 1561 """
1562 1562
1563 1563 repo = get_repo_or_error(repoid)
1564 1564 user = get_user_or_error(userid)
1565 1565 if not has_superadmin_permission(apiuser):
1566 1566 _perms = ('repository.admin',)
1567 1567 validate_repo_permissions(apiuser, repoid, repo, _perms)
1568 1568
1569 1569 try:
1570 1570 RepoModel().revoke_user_permission(repo=repo, user=user)
1571 1571 Session().commit()
1572 1572 return {
1573 1573 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1574 1574 user.username, repo.repo_name
1575 1575 ),
1576 1576 'success': True
1577 1577 }
1578 1578 except Exception:
1579 1579 log.exception(
1580 1580 "Exception occurred while trying revoke permissions to repo")
1581 1581 raise JSONRPCError(
1582 1582 'failed to edit permission for user: `%s` in repo: `%s`' % (
1583 1583 userid, repoid
1584 1584 )
1585 1585 )
1586 1586
1587 1587
1588 1588 @jsonrpc_method()
1589 1589 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1590 1590 """
1591 1591 Grant permission for a user group on the specified repository,
1592 1592 or update existing permissions.
1593 1593
1594 1594 This command can only be run using an |authtoken| with admin
1595 1595 permissions on the |repo|.
1596 1596
1597 1597 :param apiuser: This is filled automatically from the |authtoken|.
1598 1598 :type apiuser: AuthUser
1599 1599 :param repoid: Set the repository name or repository ID.
1600 1600 :type repoid: str or int
1601 1601 :param usergroupid: Specify the ID of the user group.
1602 1602 :type usergroupid: str or int
1603 1603 :param perm: Set the user group permissions using the following
1604 1604 format: (repository.(none|read|write|admin))
1605 1605 :type perm: str
1606 1606
1607 1607 Example output:
1608 1608
1609 1609 .. code-block:: bash
1610 1610
1611 1611 id : <id_given_in_input>
1612 1612 result : {
1613 1613 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1614 1614 "success": true
1615 1615
1616 1616 }
1617 1617 error : null
1618 1618
1619 1619 Example error output:
1620 1620
1621 1621 .. code-block:: bash
1622 1622
1623 1623 id : <id_given_in_input>
1624 1624 result : null
1625 1625 error : {
1626 1626 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1627 1627 }
1628 1628
1629 1629 """
1630 1630
1631 1631 repo = get_repo_or_error(repoid)
1632 1632 perm = get_perm_or_error(perm)
1633 1633 if not has_superadmin_permission(apiuser):
1634 1634 _perms = ('repository.admin',)
1635 1635 validate_repo_permissions(apiuser, repoid, repo, _perms)
1636 1636
1637 1637 user_group = get_user_group_or_error(usergroupid)
1638 1638 if not has_superadmin_permission(apiuser):
1639 1639 # check if we have at least read permission for this user group !
1640 1640 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1641 1641 if not HasUserGroupPermissionAnyApi(*_perms)(
1642 1642 user=apiuser, user_group_name=user_group.users_group_name):
1643 1643 raise JSONRPCError(
1644 1644 'user group `%s` does not exist' % (usergroupid,))
1645 1645
1646 1646 try:
1647 1647 RepoModel().grant_user_group_permission(
1648 1648 repo=repo, group_name=user_group, perm=perm)
1649 1649
1650 1650 Session().commit()
1651 1651 return {
1652 1652 'msg': 'Granted perm: `%s` for user group: `%s` in '
1653 1653 'repo: `%s`' % (
1654 1654 perm.permission_name, user_group.users_group_name,
1655 1655 repo.repo_name
1656 1656 ),
1657 1657 'success': True
1658 1658 }
1659 1659 except Exception:
1660 1660 log.exception(
1661 1661 "Exception occurred while trying change permission on repo")
1662 1662 raise JSONRPCError(
1663 1663 'failed to edit permission for user group: `%s` in '
1664 1664 'repo: `%s`' % (
1665 1665 usergroupid, repo.repo_name
1666 1666 )
1667 1667 )
1668 1668
1669 1669
1670 1670 @jsonrpc_method()
1671 1671 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1672 1672 """
1673 1673 Revoke the permissions of a user group on a given repository.
1674 1674
1675 1675 This command can only be run using an |authtoken| with admin
1676 1676 permissions on the |repo|.
1677 1677
1678 1678 :param apiuser: This is filled automatically from the |authtoken|.
1679 1679 :type apiuser: AuthUser
1680 1680 :param repoid: Set the repository name or repository ID.
1681 1681 :type repoid: str or int
1682 1682 :param usergroupid: Specify the user group ID.
1683 1683 :type usergroupid: str or int
1684 1684
1685 1685 Example output:
1686 1686
1687 1687 .. code-block:: bash
1688 1688
1689 1689 id : <id_given_in_input>
1690 1690 result: {
1691 1691 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1692 1692 "success": true
1693 1693 }
1694 1694 error: null
1695 1695 """
1696 1696
1697 1697 repo = get_repo_or_error(repoid)
1698 1698 if not has_superadmin_permission(apiuser):
1699 1699 _perms = ('repository.admin',)
1700 1700 validate_repo_permissions(apiuser, repoid, repo, _perms)
1701 1701
1702 1702 user_group = get_user_group_or_error(usergroupid)
1703 1703 if not has_superadmin_permission(apiuser):
1704 1704 # check if we have at least read permission for this user group !
1705 1705 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1706 1706 if not HasUserGroupPermissionAnyApi(*_perms)(
1707 1707 user=apiuser, user_group_name=user_group.users_group_name):
1708 1708 raise JSONRPCError(
1709 1709 'user group `%s` does not exist' % (usergroupid,))
1710 1710
1711 1711 try:
1712 1712 RepoModel().revoke_user_group_permission(
1713 1713 repo=repo, group_name=user_group)
1714 1714
1715 1715 Session().commit()
1716 1716 return {
1717 1717 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1718 1718 user_group.users_group_name, repo.repo_name
1719 1719 ),
1720 1720 'success': True
1721 1721 }
1722 1722 except Exception:
1723 1723 log.exception("Exception occurred while trying revoke "
1724 1724 "user group permission on repo")
1725 1725 raise JSONRPCError(
1726 1726 'failed to edit permission for user group: `%s` in '
1727 1727 'repo: `%s`' % (
1728 1728 user_group.users_group_name, repo.repo_name
1729 1729 )
1730 1730 )
1731 1731
1732 1732
1733 1733 @jsonrpc_method()
1734 1734 def pull(request, apiuser, repoid):
1735 1735 """
1736 1736 Triggers a pull on the given repository from a remote location. You
1737 1737 can use this to keep remote repositories up-to-date.
1738 1738
1739 1739 This command can only be run using an |authtoken| with admin
1740 1740 rights to the specified repository. For more information,
1741 1741 see :ref:`config-token-ref`.
1742 1742
1743 1743 This command takes the following options:
1744 1744
1745 1745 :param apiuser: This is filled automatically from the |authtoken|.
1746 1746 :type apiuser: AuthUser
1747 1747 :param repoid: The repository name or repository ID.
1748 1748 :type repoid: str or int
1749 1749
1750 1750 Example output:
1751 1751
1752 1752 .. code-block:: bash
1753 1753
1754 1754 id : <id_given_in_input>
1755 1755 result : {
1756 1756 "msg": "Pulled from `<repository name>`"
1757 1757 "repository": "<repository name>"
1758 1758 }
1759 1759 error : null
1760 1760
1761 1761 Example error output:
1762 1762
1763 1763 .. code-block:: bash
1764 1764
1765 1765 id : <id_given_in_input>
1766 1766 result : null
1767 1767 error : {
1768 1768 "Unable to pull changes from `<reponame>`"
1769 1769 }
1770 1770
1771 1771 """
1772 1772
1773 1773 repo = get_repo_or_error(repoid)
1774 1774 if not has_superadmin_permission(apiuser):
1775 1775 _perms = ('repository.admin',)
1776 1776 validate_repo_permissions(apiuser, repoid, repo, _perms)
1777 1777
1778 1778 try:
1779 1779 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1780 1780 return {
1781 1781 'msg': 'Pulled from `%s`' % repo.repo_name,
1782 1782 'repository': repo.repo_name
1783 1783 }
1784 1784 except Exception:
1785 1785 log.exception("Exception occurred while trying to "
1786 1786 "pull changes from remote location")
1787 1787 raise JSONRPCError(
1788 1788 'Unable to pull changes from `%s`' % repo.repo_name
1789 1789 )
1790 1790
1791 1791
1792 1792 @jsonrpc_method()
1793 1793 def strip(request, apiuser, repoid, revision, branch):
1794 1794 """
1795 1795 Strips the given revision from the specified repository.
1796 1796
1797 1797 * This will remove the revision and all of its decendants.
1798 1798
1799 1799 This command can only be run using an |authtoken| with admin rights to
1800 1800 the specified repository.
1801 1801
1802 1802 This command takes the following options:
1803 1803
1804 1804 :param apiuser: This is filled automatically from the |authtoken|.
1805 1805 :type apiuser: AuthUser
1806 1806 :param repoid: The repository name or repository ID.
1807 1807 :type repoid: str or int
1808 1808 :param revision: The revision you wish to strip.
1809 1809 :type revision: str
1810 1810 :param branch: The branch from which to strip the revision.
1811 1811 :type branch: str
1812 1812
1813 1813 Example output:
1814 1814
1815 1815 .. code-block:: bash
1816 1816
1817 1817 id : <id_given_in_input>
1818 1818 result : {
1819 1819 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1820 1820 "repository": "<repository name>"
1821 1821 }
1822 1822 error : null
1823 1823
1824 1824 Example error output:
1825 1825
1826 1826 .. code-block:: bash
1827 1827
1828 1828 id : <id_given_in_input>
1829 1829 result : null
1830 1830 error : {
1831 1831 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1832 1832 }
1833 1833
1834 1834 """
1835 1835
1836 1836 repo = get_repo_or_error(repoid)
1837 1837 if not has_superadmin_permission(apiuser):
1838 1838 _perms = ('repository.admin',)
1839 1839 validate_repo_permissions(apiuser, repoid, repo, _perms)
1840 1840
1841 1841 try:
1842 1842 ScmModel().strip(repo, revision, branch)
1843 1843 return {
1844 1844 'msg': 'Stripped commit %s from repo `%s`' % (
1845 1845 revision, repo.repo_name),
1846 1846 'repository': repo.repo_name
1847 1847 }
1848 1848 except Exception:
1849 1849 log.exception("Exception while trying to strip")
1850 1850 raise JSONRPCError(
1851 1851 'Unable to strip commit %s from repo `%s`' % (
1852 1852 revision, repo.repo_name)
1853 1853 )
1854 1854
1855 1855
1856 1856 @jsonrpc_method()
1857 1857 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1858 1858 """
1859 1859 Returns all settings for a repository. If key is given it only returns the
1860 1860 setting identified by the key or null.
1861 1861
1862 1862 :param apiuser: This is filled automatically from the |authtoken|.
1863 1863 :type apiuser: AuthUser
1864 1864 :param repoid: The repository name or repository id.
1865 1865 :type repoid: str or int
1866 1866 :param key: Key of the setting to return.
1867 1867 :type: key: Optional(str)
1868 1868
1869 1869 Example output:
1870 1870
1871 1871 .. code-block:: bash
1872 1872
1873 1873 {
1874 1874 "error": null,
1875 1875 "id": 237,
1876 1876 "result": {
1877 1877 "extensions_largefiles": true,
1878 1878 "hooks_changegroup_push_logger": true,
1879 1879 "hooks_changegroup_repo_size": false,
1880 1880 "hooks_outgoing_pull_logger": true,
1881 1881 "phases_publish": "True",
1882 1882 "rhodecode_hg_use_rebase_for_merging": true,
1883 1883 "rhodecode_pr_merge_enabled": true,
1884 1884 "rhodecode_use_outdated_comments": true
1885 1885 }
1886 1886 }
1887 1887 """
1888 1888
1889 1889 # Restrict access to this api method to admins only.
1890 1890 if not has_superadmin_permission(apiuser):
1891 1891 raise JSONRPCForbidden()
1892 1892
1893 1893 try:
1894 1894 repo = get_repo_or_error(repoid)
1895 1895 settings_model = VcsSettingsModel(repo=repo)
1896 1896 settings = settings_model.get_global_settings()
1897 1897 settings.update(settings_model.get_repo_settings())
1898 1898
1899 1899 # If only a single setting is requested fetch it from all settings.
1900 1900 key = Optional.extract(key)
1901 1901 if key is not None:
1902 1902 settings = settings.get(key, None)
1903 1903 except Exception:
1904 1904 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1905 1905 log.exception(msg)
1906 1906 raise JSONRPCError(msg)
1907 1907
1908 1908 return settings
1909 1909
1910 1910
1911 1911 @jsonrpc_method()
1912 1912 def set_repo_settings(request, apiuser, repoid, settings):
1913 1913 """
1914 1914 Update repository settings. Returns true on success.
1915 1915
1916 1916 :param apiuser: This is filled automatically from the |authtoken|.
1917 1917 :type apiuser: AuthUser
1918 1918 :param repoid: The repository name or repository id.
1919 1919 :type repoid: str or int
1920 1920 :param settings: The new settings for the repository.
1921 1921 :type: settings: dict
1922 1922
1923 1923 Example output:
1924 1924
1925 1925 .. code-block:: bash
1926 1926
1927 1927 {
1928 1928 "error": null,
1929 1929 "id": 237,
1930 1930 "result": true
1931 1931 }
1932 1932 """
1933 1933 # Restrict access to this api method to admins only.
1934 1934 if not has_superadmin_permission(apiuser):
1935 1935 raise JSONRPCForbidden()
1936 1936
1937 1937 if type(settings) is not dict:
1938 1938 raise JSONRPCError('Settings have to be a JSON Object.')
1939 1939
1940 1940 try:
1941 1941 settings_model = VcsSettingsModel(repo=repoid)
1942 1942
1943 1943 # Merge global, repo and incoming settings.
1944 1944 new_settings = settings_model.get_global_settings()
1945 1945 new_settings.update(settings_model.get_repo_settings())
1946 1946 new_settings.update(settings)
1947 1947
1948 1948 # Update the settings.
1949 1949 inherit_global_settings = new_settings.get(
1950 1950 'inherit_global_settings', False)
1951 1951 settings_model.create_or_update_repo_settings(
1952 1952 new_settings, inherit_global_settings=inherit_global_settings)
1953 1953 Session().commit()
1954 1954 except Exception:
1955 1955 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1956 1956 log.exception(msg)
1957 1957 raise JSONRPCError(msg)
1958 1958
1959 1959 # Indicate success.
1960 1960 return True
@@ -1,470 +1,470 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import ChangesetCommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 159
160 160 # fetch global flags of ignore ws or context lines
161 161 context_lcl = get_line_ctx('', request.GET)
162 162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163 163
164 164 # diff_limit will cut off the whole diff if the limit is applied
165 165 # otherwise it will just hide the big files from the front-end
166 166 diff_limit = self.cut_off_limit_diff
167 167 file_limit = self.cut_off_limit_file
168 168
169 169 # get ranges of commit ids if preset
170 170 commit_range = commit_id_range.split('...')[:2]
171 171
172 172 try:
173 173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 174 'message', 'parents']
175 175
176 176 if len(commit_range) == 2:
177 177 commits = c.rhodecode_repo.get_commits(
178 178 start_id=commit_range[0], end_id=commit_range[1],
179 179 pre_load=pre_load)
180 180 commits = list(commits)
181 181 else:
182 182 commits = [c.rhodecode_repo.get_commit(
183 183 commit_id=commit_id_range, pre_load=pre_load)]
184 184
185 185 c.commit_ranges = commits
186 186 if not c.commit_ranges:
187 187 raise RepositoryError(
188 188 'The commit range returned an empty result')
189 189 except CommitDoesNotExistError:
190 190 msg = _('No such commit exists for this repository')
191 191 h.flash(msg, category='error')
192 192 raise HTTPNotFound()
193 193 except Exception:
194 194 log.exception("General failure")
195 195 raise HTTPNotFound()
196 196
197 197 c.changes = OrderedDict()
198 198 c.lines_added = 0
199 199 c.lines_deleted = 0
200 200
201 201 # auto collapse if we have more than limit
202 202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204 204
205 205 c.commit_statuses = ChangesetStatus.STATUSES
206 206 c.inline_comments = []
207 207 c.files = []
208 208
209 209 c.statuses = []
210 210 c.comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = ChangesetCommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 # Iterate over ranges (default commit view is always one commit)
230 230 for commit in c.commit_ranges:
231 231 c.changes[commit.raw_id] = []
232 232
233 233 commit2 = commit
234 234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235 235
236 236 _diff = c.rhodecode_repo.get_diff(
237 237 commit1, commit2,
238 238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 239 diff_processor = diffs.DiffProcessor(
240 240 _diff, format='newdiff', diff_limit=diff_limit,
241 241 file_limit=file_limit, show_full_diff=fulldiff)
242 242
243 243 commit_changes = OrderedDict()
244 244 if method == 'show':
245 245 _parsed = diff_processor.prepare()
246 246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247 247
248 248 _parsed = diff_processor.prepare()
249 249
250 250 def _node_getter(commit):
251 251 def get_node(fname):
252 252 try:
253 253 return commit.get_node(fname)
254 254 except NodeDoesNotExistError:
255 255 return None
256 256 return get_node
257 257
258 258 inline_comments = ChangesetCommentsModel().get_inline_comments(
259 259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
261 261 inline_comments)
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=c.repo_name,
265 265 source_node_getter=_node_getter(commit1),
266 266 target_node_getter=_node_getter(commit2),
267 267 comments=inline_comments
268 268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 269 c.changes[commit.raw_id] = diffset
270 270 else:
271 271 # downloads/raw we only need RAW diff nothing else
272 272 diff = diff_processor.as_raw()
273 273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274 274
275 275 # sort comments by how they were generated
276 276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277 277
278 278
279 279 if len(c.commit_ranges) == 1:
280 280 c.commit = c.commit_ranges[0]
281 281 c.parent_tmpl = ''.join(
282 282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 283 if method == 'download':
284 284 response.content_type = 'text/plain'
285 285 response.content_disposition = (
286 286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 287 return diff
288 288 elif method == 'patch':
289 289 response.content_type = 'text/plain'
290 290 c.diff = safe_unicode(diff)
291 291 return render('changeset/patch_changeset.mako')
292 292 elif method == 'raw':
293 293 response.content_type = 'text/plain'
294 294 return diff
295 295 elif method == 'show':
296 296 if len(c.commit_ranges) == 1:
297 297 return render('changeset/changeset.mako')
298 298 else:
299 299 c.ancestor = None
300 300 c.target_repo = c.rhodecode_db_repo
301 301 return render('changeset/changeset_range.mako')
302 302
303 303 @LoginRequired()
304 304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 305 'repository.admin')
306 306 def index(self, revision, method='show'):
307 307 return self._index(revision, method=method)
308 308
309 309 @LoginRequired()
310 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 311 'repository.admin')
312 312 def changeset_raw(self, revision):
313 313 return self._index(revision, method='raw')
314 314
315 315 @LoginRequired()
316 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 317 'repository.admin')
318 318 def changeset_patch(self, revision):
319 319 return self._index(revision, method='patch')
320 320
321 321 @LoginRequired()
322 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 323 'repository.admin')
324 324 def changeset_download(self, revision):
325 325 return self._index(revision, method='download')
326 326
327 327 @LoginRequired()
328 328 @NotAnonymous()
329 329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 330 'repository.admin')
331 331 @auth.CSRFRequired()
332 332 @jsonify
333 333 def comment(self, repo_name, revision):
334 334 commit_id = revision
335 335 status = request.POST.get('changeset_status', None)
336 336 text = request.POST.get('text')
337 337 if status:
338 338 text = text or (_('Status change %(transition_icon)s %(status)s')
339 339 % {'transition_icon': '>',
340 340 'status': ChangesetStatus.get_status_lbl(status)})
341 341
342 342 multi_commit_ids = filter(
343 343 lambda s: s not in ['', None],
344 344 request.POST.get('commit_ids', '').split(','),)
345 345
346 346 commit_ids = multi_commit_ids or [commit_id]
347 347 comment = None
348 348 for current_id in filter(None, commit_ids):
349 349 c.co = comment = ChangesetCommentsModel().create(
350 350 text=text,
351 351 repo=c.rhodecode_db_repo.repo_id,
352 352 user=c.rhodecode_user.user_id,
353 revision=current_id,
353 commit_id=current_id,
354 354 f_path=request.POST.get('f_path'),
355 355 line_no=request.POST.get('line'),
356 356 status_change=(ChangesetStatus.get_status_lbl(status)
357 357 if status else None),
358 358 status_change_type=status
359 359 )
360 360 c.inline_comment = True if comment.line_no else False
361 361
362 362 # get status if set !
363 363 if status:
364 364 # if latest status was from pull request and it's closed
365 365 # disallow changing status !
366 366 # dont_allow_on_closed_pull_request = True !
367 367
368 368 try:
369 369 ChangesetStatusModel().set_status(
370 370 c.rhodecode_db_repo.repo_id,
371 371 status,
372 372 c.rhodecode_user.user_id,
373 373 comment,
374 374 revision=current_id,
375 375 dont_allow_on_closed_pull_request=True
376 376 )
377 377 except StatusChangeOnClosedPullRequestError:
378 378 msg = _('Changing the status of a commit associated with '
379 379 'a closed pull request is not allowed')
380 380 log.exception(msg)
381 381 h.flash(msg, category='warning')
382 382 return redirect(h.url(
383 383 'changeset_home', repo_name=repo_name,
384 384 revision=current_id))
385 385
386 386 # finalize, commit and redirect
387 387 Session().commit()
388 388
389 389 data = {
390 390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 391 }
392 392 if comment:
393 393 data.update(comment.get_dict())
394 394 data.update({'rendered_text':
395 395 render('changeset/changeset_comment_block.mako')})
396 396
397 397 return data
398 398
399 399 @LoginRequired()
400 400 @NotAnonymous()
401 401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 402 'repository.admin')
403 403 @auth.CSRFRequired()
404 404 def preview_comment(self):
405 405 # Technically a CSRF token is not needed as no state changes with this
406 406 # call. However, as this is a POST is better to have it, so automated
407 407 # tools don't flag it as potential CSRF.
408 408 # Post is required because the payload could be bigger than the maximum
409 409 # allowed by GET.
410 410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 411 raise HTTPBadRequest()
412 412 text = request.POST.get('text')
413 413 renderer = request.POST.get('renderer') or 'rst'
414 414 if text:
415 415 return h.render(text, renderer=renderer, mentions=True)
416 416 return ''
417 417
418 418 @LoginRequired()
419 419 @NotAnonymous()
420 420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 421 'repository.admin')
422 422 @auth.CSRFRequired()
423 423 @jsonify
424 424 def delete_comment(self, repo_name, comment_id):
425 425 comment = ChangesetComment.get(comment_id)
426 426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 429 ChangesetCommentsModel().delete(comment=comment)
430 430 Session().commit()
431 431 return True
432 432 else:
433 433 raise HTTPForbidden()
434 434
435 435 @LoginRequired()
436 436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 437 'repository.admin')
438 438 @jsonify
439 439 def changeset_info(self, repo_name, revision):
440 440 if request.is_xhr:
441 441 try:
442 442 return c.rhodecode_repo.get_commit(commit_id=revision)
443 443 except CommitDoesNotExistError as e:
444 444 return EmptyCommit(message=str(e))
445 445 else:
446 446 raise HTTPBadRequest()
447 447
448 448 @LoginRequired()
449 449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 450 'repository.admin')
451 451 @jsonify
452 452 def changeset_children(self, repo_name, revision):
453 453 if request.is_xhr:
454 454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 455 result = {"results": commit.children}
456 456 return result
457 457 else:
458 458 raise HTTPBadRequest()
459 459
460 460 @LoginRequired()
461 461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 462 'repository.admin')
463 463 @jsonify
464 464 def changeset_parents(self, repo_name, revision):
465 465 if request.is_xhr:
466 466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 467 result = {"results": commit.parents}
468 468 return result
469 469 else:
470 470 raise HTTPBadRequest()
@@ -1,530 +1,530 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class ChangesetCommentsModel(BaseModel):
52 52
53 53 cls = ChangesetComment
54 54
55 55 DIFF_CONTEXT_BEFORE = 3
56 56 DIFF_CONTEXT_AFTER = 3
57 57
58 58 def __get_commit_comment(self, changeset_comment):
59 59 return self._get_instance(ChangesetComment, changeset_comment)
60 60
61 61 def __get_pull_request(self, pull_request):
62 62 return self._get_instance(PullRequest, pull_request)
63 63
64 64 def _extract_mentions(self, s):
65 65 user_objects = []
66 66 for username in extract_mentioned_users(s):
67 67 user_obj = User.get_by_username(username, case_insensitive=True)
68 68 if user_obj:
69 69 user_objects.append(user_obj)
70 70 return user_objects
71 71
72 72 def _get_renderer(self, global_renderer='rst'):
73 73 try:
74 74 # try reading from visual context
75 75 from pylons import tmpl_context
76 76 global_renderer = tmpl_context.visual.default_renderer
77 77 except AttributeError:
78 78 log.debug("Renderer not set, falling back "
79 79 "to default renderer '%s'", global_renderer)
80 80 except Exception:
81 81 log.error(traceback.format_exc())
82 82 return global_renderer
83 83
84 def create(self, text, repo, user, revision=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None,
84 def create(self, text, repo, user, commit_id=None, pull_request=None,
85 f_path=None, line_no=None, status_change=None, comment_type=None,
86 86 status_change_type=None, closing_pr=False,
87 87 send_email=True, renderer=None):
88 88 """
89 89 Creates new comment for commit or pull request.
90 90 IF status_change is not none this comment is associated with a
91 91 status change of commit or commit associated with pull request
92 92
93 93 :param text:
94 94 :param repo:
95 95 :param user:
96 :param revision:
96 :param commit_id:
97 97 :param pull_request:
98 98 :param f_path:
99 99 :param line_no:
100 100 :param status_change: Label for status change
101 :param comment_type: Type of comment
101 102 :param status_change_type: type of status change
102 103 :param closing_pr:
103 104 :param send_email:
105 :param renderer: pick renderer for this comment
104 106 """
105 107 if not text:
106 108 log.warning('Missing text for comment, skipping...')
107 109 return
108 110
109 111 if not renderer:
110 112 renderer = self._get_renderer()
111 113
112 114 repo = self._get_repo(repo)
113 115 user = self._get_user(user)
114 116 comment = ChangesetComment()
115 117 comment.renderer = renderer
116 118 comment.repo = repo
117 119 comment.author = user
118 120 comment.text = text
119 121 comment.f_path = f_path
120 122 comment.line_no = line_no
121 123
122 #TODO (marcink): fix this and remove revision as param
123 commit_id = revision
124 124 pull_request_id = pull_request
125 125
126 126 commit_obj = None
127 127 pull_request_obj = None
128 128
129 129 if commit_id:
130 130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 131 # do a lookup, so we don't pass something bad here
132 132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 133 comment.revision = commit_obj.raw_id
134 134
135 135 elif pull_request_id:
136 136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 137 pull_request_obj = self.__get_pull_request(pull_request_id)
138 138 comment.pull_request = pull_request_obj
139 139 else:
140 140 raise Exception('Please specify commit or pull_request_id')
141 141
142 142 Session().add(comment)
143 143 Session().flush()
144 144 kwargs = {
145 145 'user': user,
146 146 'renderer_type': renderer,
147 147 'repo_name': repo.repo_name,
148 148 'status_change': status_change,
149 149 'status_change_type': status_change_type,
150 150 'comment_body': text,
151 151 'comment_file': f_path,
152 152 'comment_line': line_no,
153 153 }
154 154
155 155 if commit_obj:
156 156 recipients = ChangesetComment.get_users(
157 157 revision=commit_obj.raw_id)
158 158 # add commit author if it's in RhodeCode system
159 159 cs_author = User.get_from_cs_author(commit_obj.author)
160 160 if not cs_author:
161 161 # use repo owner if we cannot extract the author correctly
162 162 cs_author = repo.user
163 163 recipients += [cs_author]
164 164
165 165 commit_comment_url = self.get_url(comment)
166 166
167 167 target_repo_url = h.link_to(
168 168 repo.repo_name,
169 169 h.url('summary_home',
170 170 repo_name=repo.repo_name, qualified=True))
171 171
172 172 # commit specifics
173 173 kwargs.update({
174 174 'commit': commit_obj,
175 175 'commit_message': commit_obj.message,
176 176 'commit_target_repo': target_repo_url,
177 177 'commit_comment_url': commit_comment_url,
178 178 })
179 179
180 180 elif pull_request_obj:
181 181 # get the current participants of this pull request
182 182 recipients = ChangesetComment.get_users(
183 183 pull_request_id=pull_request_obj.pull_request_id)
184 184 # add pull request author
185 185 recipients += [pull_request_obj.author]
186 186
187 187 # add the reviewers to notification
188 188 recipients += [x.user for x in pull_request_obj.reviewers]
189 189
190 190 pr_target_repo = pull_request_obj.target_repo
191 191 pr_source_repo = pull_request_obj.source_repo
192 192
193 193 pr_comment_url = h.url(
194 194 'pullrequest_show',
195 195 repo_name=pr_target_repo.repo_name,
196 196 pull_request_id=pull_request_obj.pull_request_id,
197 197 anchor='comment-%s' % comment.comment_id,
198 198 qualified=True,)
199 199
200 200 # set some variables for email notification
201 201 pr_target_repo_url = h.url(
202 202 'summary_home', repo_name=pr_target_repo.repo_name,
203 203 qualified=True)
204 204
205 205 pr_source_repo_url = h.url(
206 206 'summary_home', repo_name=pr_source_repo.repo_name,
207 207 qualified=True)
208 208
209 209 # pull request specifics
210 210 kwargs.update({
211 211 'pull_request': pull_request_obj,
212 212 'pr_id': pull_request_obj.pull_request_id,
213 213 'pr_target_repo': pr_target_repo,
214 214 'pr_target_repo_url': pr_target_repo_url,
215 215 'pr_source_repo': pr_source_repo,
216 216 'pr_source_repo_url': pr_source_repo_url,
217 217 'pr_comment_url': pr_comment_url,
218 218 'pr_closing': closing_pr,
219 219 })
220 220 if send_email:
221 221 # pre-generate the subject for notification itself
222 222 (subject,
223 223 _h, _e, # we don't care about those
224 224 body_plaintext) = EmailNotificationModel().render_email(
225 225 notification_type, **kwargs)
226 226
227 227 mention_recipients = set(
228 228 self._extract_mentions(text)).difference(recipients)
229 229
230 230 # create notification objects, and emails
231 231 NotificationModel().create(
232 232 created_by=user,
233 233 notification_subject=subject,
234 234 notification_body=body_plaintext,
235 235 notification_type=notification_type,
236 236 recipients=recipients,
237 237 mention_recipients=mention_recipients,
238 238 email_kwargs=kwargs,
239 239 )
240 240
241 241 action = (
242 242 'user_commented_pull_request:{}'.format(
243 243 comment.pull_request.pull_request_id)
244 244 if comment.pull_request
245 245 else 'user_commented_revision:{}'.format(comment.revision)
246 246 )
247 247 action_logger(user, action, comment.repo)
248 248
249 249 registry = get_current_registry()
250 250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 251 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 252 msg_url = ''
253 253 if commit_obj:
254 254 msg_url = commit_comment_url
255 255 repo_name = repo.repo_name
256 256 elif pull_request_obj:
257 257 msg_url = pr_comment_url
258 258 repo_name = pr_target_repo.repo_name
259 259
260 260 if channelstream_config.get('enabled'):
261 261 message = '<strong>{}</strong> {} - ' \
262 262 '<a onclick="window.location=\'{}\';' \
263 263 'window.location.reload()">' \
264 264 '<strong>{}</strong></a>'
265 265 message = message.format(
266 266 user.username, _('made a comment'), msg_url,
267 267 _('Show it now'))
268 268 channel = '/repo${}$/pr/{}'.format(
269 269 repo_name,
270 270 pull_request_id
271 271 )
272 272 payload = {
273 273 'type': 'message',
274 274 'timestamp': datetime.utcnow(),
275 275 'user': 'system',
276 276 'exclude_users': [user.username],
277 277 'channel': channel,
278 278 'message': {
279 279 'message': message,
280 280 'level': 'info',
281 281 'topic': '/notifications'
282 282 }
283 283 }
284 284 channelstream_request(channelstream_config, [payload],
285 285 '/message', raise_exc=False)
286 286
287 287 return comment
288 288
289 289 def delete(self, comment):
290 290 """
291 291 Deletes given comment
292 292
293 293 :param comment_id:
294 294 """
295 295 comment = self.__get_commit_comment(comment)
296 296 Session().delete(comment)
297 297
298 298 return comment
299 299
300 300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 301 q = ChangesetComment.query()\
302 302 .filter(ChangesetComment.repo_id == repo_id)
303 303 if revision:
304 304 q = q.filter(ChangesetComment.revision == revision)
305 305 elif pull_request:
306 306 pull_request = self.__get_pull_request(pull_request)
307 307 q = q.filter(ChangesetComment.pull_request == pull_request)
308 308 else:
309 309 raise Exception('Please specify commit or pull_request')
310 310 q = q.order_by(ChangesetComment.created_on)
311 311 return q.all()
312 312
313 313 def get_url(self, comment):
314 314 comment = self.__get_commit_comment(comment)
315 315 if comment.pull_request:
316 316 return h.url(
317 317 'pullrequest_show',
318 318 repo_name=comment.pull_request.target_repo.repo_name,
319 319 pull_request_id=comment.pull_request.pull_request_id,
320 320 anchor='comment-%s' % comment.comment_id,
321 321 qualified=True,)
322 322 else:
323 323 return h.url(
324 324 'changeset_home',
325 325 repo_name=comment.repo.repo_name,
326 326 revision=comment.revision,
327 327 anchor='comment-%s' % comment.comment_id,
328 328 qualified=True,)
329 329
330 330 def get_comments(self, repo_id, revision=None, pull_request=None):
331 331 """
332 332 Gets main comments based on revision or pull_request_id
333 333
334 334 :param repo_id:
335 335 :param revision:
336 336 :param pull_request:
337 337 """
338 338
339 339 q = ChangesetComment.query()\
340 340 .filter(ChangesetComment.repo_id == repo_id)\
341 341 .filter(ChangesetComment.line_no == None)\
342 342 .filter(ChangesetComment.f_path == None)
343 343 if revision:
344 344 q = q.filter(ChangesetComment.revision == revision)
345 345 elif pull_request:
346 346 pull_request = self.__get_pull_request(pull_request)
347 347 q = q.filter(ChangesetComment.pull_request == pull_request)
348 348 else:
349 349 raise Exception('Please specify commit or pull_request')
350 350 q = q.order_by(ChangesetComment.created_on)
351 351 return q.all()
352 352
353 353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 355 return self._group_comments_by_path_and_line_number(q)
356 356
357 357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 358 version=None, include_aggregates=False):
359 359 version_aggregates = collections.defaultdict(list)
360 360 inline_cnt = 0
361 361 for fname, per_line_comments in inline_comments.iteritems():
362 362 for lno, comments in per_line_comments.iteritems():
363 363 for comm in comments:
364 364 version_aggregates[comm.pull_request_version_id].append(comm)
365 365 if not comm.outdated_at_version(version) and skip_outdated:
366 366 inline_cnt += 1
367 367
368 368 if include_aggregates:
369 369 return inline_cnt, version_aggregates
370 370 return inline_cnt
371 371
372 372 def get_outdated_comments(self, repo_id, pull_request):
373 373 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
374 374 # of a pull request.
375 375 q = self._all_inline_comments_of_pull_request(pull_request)
376 376 q = q.filter(
377 377 ChangesetComment.display_state ==
378 378 ChangesetComment.COMMENT_OUTDATED
379 379 ).order_by(ChangesetComment.comment_id.asc())
380 380
381 381 return self._group_comments_by_path_and_line_number(q)
382 382
383 383 def _get_inline_comments_query(self, repo_id, revision, pull_request):
384 384 # TODO: johbo: Split this into two methods: One for PR and one for
385 385 # commit.
386 386 if revision:
387 387 q = Session().query(ChangesetComment).filter(
388 388 ChangesetComment.repo_id == repo_id,
389 389 ChangesetComment.line_no != null(),
390 390 ChangesetComment.f_path != null(),
391 391 ChangesetComment.revision == revision)
392 392
393 393 elif pull_request:
394 394 pull_request = self.__get_pull_request(pull_request)
395 395 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
396 396 q = self._visible_inline_comments_of_pull_request(pull_request)
397 397 else:
398 398 q = self._all_inline_comments_of_pull_request(pull_request)
399 399
400 400 else:
401 401 raise Exception('Please specify commit or pull_request_id')
402 402 q = q.order_by(ChangesetComment.comment_id.asc())
403 403 return q
404 404
405 405 def _group_comments_by_path_and_line_number(self, q):
406 406 comments = q.all()
407 407 paths = collections.defaultdict(lambda: collections.defaultdict(list))
408 408 for co in comments:
409 409 paths[co.f_path][co.line_no].append(co)
410 410 return paths
411 411
412 412 @classmethod
413 413 def needed_extra_diff_context(cls):
414 414 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
415 415
416 416 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
417 417 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
418 418 return
419 419
420 420 comments = self._visible_inline_comments_of_pull_request(pull_request)
421 421 comments_to_outdate = comments.all()
422 422
423 423 for comment in comments_to_outdate:
424 424 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
425 425
426 426 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
427 427 diff_line = _parse_comment_line_number(comment.line_no)
428 428
429 429 try:
430 430 old_context = old_diff_proc.get_context_of_line(
431 431 path=comment.f_path, diff_line=diff_line)
432 432 new_context = new_diff_proc.get_context_of_line(
433 433 path=comment.f_path, diff_line=diff_line)
434 434 except (diffs.LineNotInDiffException,
435 435 diffs.FileNotInDiffException):
436 436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
437 437 return
438 438
439 439 if old_context == new_context:
440 440 return
441 441
442 442 if self._should_relocate_diff_line(diff_line):
443 443 new_diff_lines = new_diff_proc.find_context(
444 444 path=comment.f_path, context=old_context,
445 445 offset=self.DIFF_CONTEXT_BEFORE)
446 446 if not new_diff_lines:
447 447 comment.display_state = ChangesetComment.COMMENT_OUTDATED
448 448 else:
449 449 new_diff_line = self._choose_closest_diff_line(
450 450 diff_line, new_diff_lines)
451 451 comment.line_no = _diff_to_comment_line_number(new_diff_line)
452 452 else:
453 453 comment.display_state = ChangesetComment.COMMENT_OUTDATED
454 454
455 455 def _should_relocate_diff_line(self, diff_line):
456 456 """
457 457 Checks if relocation shall be tried for the given `diff_line`.
458 458
459 459 If a comment points into the first lines, then we can have a situation
460 460 that after an update another line has been added on top. In this case
461 461 we would find the context still and move the comment around. This
462 462 would be wrong.
463 463 """
464 464 should_relocate = (
465 465 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
466 466 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
467 467 return should_relocate
468 468
469 469 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
470 470 candidate = new_diff_lines[0]
471 471 best_delta = _diff_line_delta(diff_line, candidate)
472 472 for new_diff_line in new_diff_lines[1:]:
473 473 delta = _diff_line_delta(diff_line, new_diff_line)
474 474 if delta < best_delta:
475 475 candidate = new_diff_line
476 476 best_delta = delta
477 477 return candidate
478 478
479 479 def _visible_inline_comments_of_pull_request(self, pull_request):
480 480 comments = self._all_inline_comments_of_pull_request(pull_request)
481 481 comments = comments.filter(
482 482 coalesce(ChangesetComment.display_state, '') !=
483 483 ChangesetComment.COMMENT_OUTDATED)
484 484 return comments
485 485
486 486 def _all_inline_comments_of_pull_request(self, pull_request):
487 487 comments = Session().query(ChangesetComment)\
488 488 .filter(ChangesetComment.line_no != None)\
489 489 .filter(ChangesetComment.f_path != None)\
490 490 .filter(ChangesetComment.pull_request == pull_request)
491 491 return comments
492 492
493 493 @staticmethod
494 494 def use_outdated_comments(pull_request):
495 495 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
496 496 settings = settings_model.get_general_settings()
497 497 return settings.get('rhodecode_use_outdated_comments', False)
498 498
499 499
500 500 def _parse_comment_line_number(line_no):
501 501 """
502 502 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
503 503 """
504 504 old_line = None
505 505 new_line = None
506 506 if line_no.startswith('o'):
507 507 old_line = int(line_no[1:])
508 508 elif line_no.startswith('n'):
509 509 new_line = int(line_no[1:])
510 510 else:
511 511 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
512 512 return diffs.DiffLineNumber(old_line, new_line)
513 513
514 514
515 515 def _diff_to_comment_line_number(diff_line):
516 516 if diff_line.new is not None:
517 517 return u'n{}'.format(diff_line.new)
518 518 elif diff_line.old is not None:
519 519 return u'o{}'.format(diff_line.old)
520 520 return u''
521 521
522 522
523 523 def _diff_line_delta(a, b):
524 524 if None not in (a.new, b.new):
525 525 return abs(a.new - b.new)
526 526 elif None not in (a.old, b.old):
527 527 return abs(a.old - b.old)
528 528 else:
529 529 raise ValueError(
530 530 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now