##// END OF EJS Templates
fix(test): fixed test and pull api endpoint. Fixes: RCCE-29
ilin.s -
r5264:cc734548 default
parent child Browse files
Show More
@@ -1,2535 +1,2536 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import time
21 21
22 22 import rhodecode
23 23 from rhodecode.api import (
24 24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
28 28 get_perm_or_error, parse_args, get_origin, build_commit_data,
29 29 validate_set_owner_permissions)
30 30 from rhodecode.lib import audit_logger, rc_cache, channelstream
31 31 from rhodecode.lib import repo_maintenance
32 32 from rhodecode.lib.auth import (
33 33 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
34 34 HasRepoPermissionAnyApi)
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 36 from rhodecode.lib.utils2 import (
37 37 str2bool, time_to_datetime, safe_str, safe_int)
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.exceptions import (
40 40 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
41 41 from rhodecode.lib.vcs import RepositoryError
42 42 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (
46 46 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
47 47 ChangesetComment)
48 48 from rhodecode.model.permission import PermissionModel
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.scm import ScmModel, RepoList
52 52 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
53 53 from rhodecode.model import validation_schema
54 54 from rhodecode.model.validation_schema.schemas import repo_schema
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 @jsonrpc_method()
60 60 def get_repo(request, apiuser, repoid, cache=Optional(True)):
61 61 """
62 62 Gets an existing repository by its name or repository_id.
63 63
64 64 The members section so the output returns users groups or users
65 65 associated with that repository.
66 66
67 67 This command can only be run using an |authtoken| with admin rights,
68 68 or users with at least read rights to the |repo|.
69 69
70 70 :param apiuser: This is filled automatically from the |authtoken|.
71 71 :type apiuser: AuthUser
72 72 :param repoid: The repository name or repository id.
73 73 :type repoid: str or int
74 74 :param cache: use the cached value for last changeset
75 75 :type: cache: Optional(bool)
76 76
77 77 Example output:
78 78
79 79 .. code-block:: bash
80 80
81 81 {
82 82 "error": null,
83 83 "id": <repo_id>,
84 84 "result": {
85 85 "clone_uri": null,
86 86 "created_on": "timestamp",
87 87 "description": "repo description",
88 88 "enable_downloads": false,
89 89 "enable_locking": false,
90 90 "enable_statistics": false,
91 91 "followers": [
92 92 {
93 93 "active": true,
94 94 "admin": false,
95 95 "api_key": "****************************************",
96 96 "api_keys": [
97 97 "****************************************"
98 98 ],
99 99 "email": "user@example.com",
100 100 "emails": [
101 101 "user@example.com"
102 102 ],
103 103 "extern_name": "rhodecode",
104 104 "extern_type": "rhodecode",
105 105 "firstname": "username",
106 106 "ip_addresses": [],
107 107 "language": null,
108 108 "last_login": "2015-09-16T17:16:35.854",
109 109 "lastname": "surname",
110 110 "user_id": <user_id>,
111 111 "username": "name"
112 112 }
113 113 ],
114 114 "fork_of": "parent-repo",
115 115 "landing_rev": [
116 116 "rev",
117 117 "tip"
118 118 ],
119 119 "last_changeset": {
120 120 "author": "User <user@example.com>",
121 121 "branch": "default",
122 122 "date": "timestamp",
123 123 "message": "last commit message",
124 124 "parents": [
125 125 {
126 126 "raw_id": "commit-id"
127 127 }
128 128 ],
129 129 "raw_id": "commit-id",
130 130 "revision": <revision number>,
131 131 "short_id": "short id"
132 132 },
133 133 "lock_reason": null,
134 134 "locked_by": null,
135 135 "locked_date": null,
136 136 "owner": "owner-name",
137 137 "permissions": [
138 138 {
139 139 "name": "super-admin-name",
140 140 "origin": "super-admin",
141 141 "permission": "repository.admin",
142 142 "type": "user"
143 143 },
144 144 {
145 145 "name": "owner-name",
146 146 "origin": "owner",
147 147 "permission": "repository.admin",
148 148 "type": "user"
149 149 },
150 150 {
151 151 "name": "user-group-name",
152 152 "origin": "permission",
153 153 "permission": "repository.write",
154 154 "type": "user_group"
155 155 }
156 156 ],
157 157 "private": true,
158 158 "repo_id": 676,
159 159 "repo_name": "user-group/repo-name",
160 160 "repo_type": "hg"
161 161 }
162 162 }
163 163 """
164 164
165 165 repo = get_repo_or_error(repoid)
166 166 cache = Optional.extract(cache)
167 167
168 168 include_secrets = False
169 169 if has_superadmin_permission(apiuser):
170 170 include_secrets = True
171 171 else:
172 172 # check if we have at least read permission for this repo !
173 173 _perms = (
174 174 'repository.admin', 'repository.write', 'repository.read',)
175 175 validate_repo_permissions(apiuser, repoid, repo, _perms)
176 176
177 177 permissions = []
178 178 for _user in repo.permissions():
179 179 user_data = {
180 180 'name': _user.username,
181 181 'permission': _user.permission,
182 182 'origin': get_origin(_user),
183 183 'type': "user",
184 184 }
185 185 permissions.append(user_data)
186 186
187 187 for _user_group in repo.permission_user_groups():
188 188 user_group_data = {
189 189 'name': _user_group.users_group_name,
190 190 'permission': _user_group.permission,
191 191 'origin': get_origin(_user_group),
192 192 'type': "user_group",
193 193 }
194 194 permissions.append(user_group_data)
195 195
196 196 following_users = [
197 197 user.user.get_api_data(include_secrets=include_secrets)
198 198 for user in repo.followers]
199 199
200 200 if not cache:
201 201 repo.update_commit_cache()
202 202 data = repo.get_api_data(include_secrets=include_secrets)
203 203 data['permissions'] = permissions
204 204 data['followers'] = following_users
205 205
206 206 return data
207 207
208 208
209 209 @jsonrpc_method()
210 210 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
211 211 """
212 212 Lists all existing repositories.
213 213
214 214 This command can only be run using an |authtoken| with admin rights,
215 215 or users with at least read rights to |repos|.
216 216
217 217 :param apiuser: This is filled automatically from the |authtoken|.
218 218 :type apiuser: AuthUser
219 219 :param root: specify root repository group to fetch repositories.
220 220 filters the returned repositories to be members of given root group.
221 221 :type root: Optional(None)
222 222 :param traverse: traverse given root into subrepositories. With this flag
223 223 set to False, it will only return top-level repositories from `root`.
224 224 if root is empty it will return just top-level repositories.
225 225 :type traverse: Optional(True)
226 226
227 227
228 228 Example output:
229 229
230 230 .. code-block:: bash
231 231
232 232 id : <id_given_in_input>
233 233 result: [
234 234 {
235 235 "repo_id" : "<repo_id>",
236 236 "repo_name" : "<reponame>"
237 237 "repo_type" : "<repo_type>",
238 238 "clone_uri" : "<clone_uri>",
239 239 "private": : "<bool>",
240 240 "created_on" : "<datetimecreated>",
241 241 "description" : "<description>",
242 242 "landing_rev": "<landing_rev>",
243 243 "owner": "<repo_owner>",
244 244 "fork_of": "<name_of_fork_parent>",
245 245 "enable_downloads": "<bool>",
246 246 "enable_locking": "<bool>",
247 247 "enable_statistics": "<bool>",
248 248 },
249 249 ...
250 250 ]
251 251 error: null
252 252 """
253 253
254 254 include_secrets = has_superadmin_permission(apiuser)
255 255 _perms = ('repository.read', 'repository.write', 'repository.admin',)
256 256 extras = {'user': apiuser}
257 257
258 258 root = Optional.extract(root)
259 259 traverse = Optional.extract(traverse, binary=True)
260 260
261 261 if root:
262 262 # verify parent existance, if it's empty return an error
263 263 parent = RepoGroup.get_by_group_name(root)
264 264 if not parent:
265 265 raise JSONRPCError(
266 266 f'Root repository group `{root}` does not exist')
267 267
268 268 if traverse:
269 269 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
270 270 else:
271 271 repos = RepoModel().get_repos_for_root(root=parent)
272 272 else:
273 273 if traverse:
274 274 repos = RepoModel().get_all()
275 275 else:
276 276 # return just top-level
277 277 repos = RepoModel().get_repos_for_root(root=None)
278 278
279 279 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
280 280 return [repo.get_api_data(include_secrets=include_secrets)
281 281 for repo in repo_list]
282 282
283 283
284 284 @jsonrpc_method()
285 285 def get_repo_changeset(request, apiuser, repoid, revision,
286 286 details=Optional('basic')):
287 287 """
288 288 Returns information about a changeset.
289 289
290 290 Additionally parameters define the amount of details returned by
291 291 this function.
292 292
293 293 This command can only be run using an |authtoken| with admin rights,
294 294 or users with at least read rights to the |repo|.
295 295
296 296 :param apiuser: This is filled automatically from the |authtoken|.
297 297 :type apiuser: AuthUser
298 298 :param repoid: The repository name or repository id
299 299 :type repoid: str or int
300 300 :param revision: revision for which listing should be done
301 301 :type revision: str
302 302 :param details: details can be 'basic|extended|full' full gives diff
303 303 info details like the diff itself, and number of changed files etc.
304 304 :type details: Optional(str)
305 305
306 306 """
307 307 repo = get_repo_or_error(repoid)
308 308 if not has_superadmin_permission(apiuser):
309 309 _perms = ('repository.admin', 'repository.write', 'repository.read',)
310 310 validate_repo_permissions(apiuser, repoid, repo, _perms)
311 311
312 312 changes_details = Optional.extract(details)
313 313 _changes_details_types = ['basic', 'extended', 'full']
314 314 if changes_details not in _changes_details_types:
315 315 raise JSONRPCError(
316 316 'ret_type must be one of %s' % (
317 317 ','.join(_changes_details_types)))
318 318
319 319 vcs_repo = repo.scm_instance()
320 320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
321 321 'status', '_commit', '_file_paths']
322 322
323 323 try:
324 324 commit = repo.get_commit(commit_id=revision, pre_load=pre_load)
325 325 except TypeError as e:
326 326 raise JSONRPCError(safe_str(e))
327 327 _cs_json = commit.__json__()
328 328 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
329 329 if changes_details == 'full':
330 330 _cs_json['refs'] = commit._get_refs()
331 331 return _cs_json
332 332
333 333
334 334 @jsonrpc_method()
335 335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
336 336 details=Optional('basic')):
337 337 """
338 338 Returns a set of commits limited by the number starting
339 339 from the `start_rev` option.
340 340
341 341 Additional parameters define the amount of details returned by this
342 342 function.
343 343
344 344 This command can only be run using an |authtoken| with admin rights,
345 345 or users with at least read rights to |repos|.
346 346
347 347 :param apiuser: This is filled automatically from the |authtoken|.
348 348 :type apiuser: AuthUser
349 349 :param repoid: The repository name or repository ID.
350 350 :type repoid: str or int
351 351 :param start_rev: The starting revision from where to get changesets.
352 352 :type start_rev: str
353 353 :param limit: Limit the number of commits to this amount
354 354 :type limit: str or int
355 355 :param details: Set the level of detail returned. Valid option are:
356 356 ``basic``, ``extended`` and ``full``.
357 357 :type details: Optional(str)
358 358
359 359 .. note::
360 360
361 361 Setting the parameter `details` to the value ``full`` is extensive
362 362 and returns details like the diff itself, and the number
363 363 of changed files.
364 364
365 365 """
366 366 repo = get_repo_or_error(repoid)
367 367 if not has_superadmin_permission(apiuser):
368 368 _perms = ('repository.admin', 'repository.write', 'repository.read',)
369 369 validate_repo_permissions(apiuser, repoid, repo, _perms)
370 370
371 371 changes_details = Optional.extract(details)
372 372 _changes_details_types = ['basic', 'extended', 'full']
373 373 if changes_details not in _changes_details_types:
374 374 raise JSONRPCError(
375 375 'ret_type must be one of %s' % (
376 376 ','.join(_changes_details_types)))
377 377
378 378 limit = int(limit)
379 379 pre_load = ['author', 'branch', 'date', 'message', 'parents',
380 380 'status', '_commit', '_file_paths']
381 381
382 382 vcs_repo = repo.scm_instance()
383 383 # SVN needs a special case to distinguish its index and commit id
384 384 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
385 385 start_rev = vcs_repo.commit_ids[0]
386 386
387 387 try:
388 388 commits = vcs_repo.get_commits(
389 389 start_id=start_rev, pre_load=pre_load, translate_tags=False)
390 390 except TypeError as e:
391 391 raise JSONRPCError(safe_str(e))
392 392 except Exception:
393 393 log.exception('Fetching of commits failed')
394 394 raise JSONRPCError('Error occurred during commit fetching')
395 395
396 396 ret = []
397 397 for cnt, commit in enumerate(commits):
398 398 if cnt >= limit != -1:
399 399 break
400 400 _cs_json = commit.__json__()
401 401 _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details)
402 402 if changes_details == 'full':
403 403 _cs_json['refs'] = {
404 404 'branches': [commit.branch],
405 405 'bookmarks': getattr(commit, 'bookmarks', []),
406 406 'tags': commit.tags
407 407 }
408 408 ret.append(_cs_json)
409 409 return ret
410 410
411 411
412 412 @jsonrpc_method()
413 413 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
414 414 ret_type=Optional('all'), details=Optional('basic'),
415 415 max_file_bytes=Optional(None)):
416 416 """
417 417 Returns a list of nodes and children in a flat list for a given
418 418 path at given revision.
419 419
420 420 It's possible to specify ret_type to show only `files` or `dirs`.
421 421
422 422 This command can only be run using an |authtoken| with admin rights,
423 423 or users with at least read rights to |repos|.
424 424
425 425 :param apiuser: This is filled automatically from the |authtoken|.
426 426 :type apiuser: AuthUser
427 427 :param repoid: The repository name or repository ID.
428 428 :type repoid: str or int
429 429 :param revision: The revision for which listing should be done.
430 430 :type revision: str
431 431 :param root_path: The path from which to start displaying.
432 432 :type root_path: str
433 433 :param ret_type: Set the return type. Valid options are
434 434 ``all`` (default), ``files`` and ``dirs``.
435 435 :type ret_type: Optional(str)
436 436 :param details: Returns extended information about nodes, such as
437 437 md5, binary, and or content.
438 438 The valid options are ``basic`` and ``full``.
439 439 :type details: Optional(str)
440 440 :param max_file_bytes: Only return file content under this file size bytes
441 441 :type details: Optional(int)
442 442
443 443 Example output:
444 444
445 445 .. code-block:: bash
446 446
447 447 id : <id_given_in_input>
448 448 result: [
449 449 {
450 450 "binary": false,
451 451 "content": "File line",
452 452 "extension": "md",
453 453 "lines": 2,
454 454 "md5": "059fa5d29b19c0657e384749480f6422",
455 455 "mimetype": "text/x-minidsrc",
456 456 "name": "file.md",
457 457 "size": 580,
458 458 "type": "file"
459 459 },
460 460 ...
461 461 ]
462 462 error: null
463 463 """
464 464
465 465 repo = get_repo_or_error(repoid)
466 466 if not has_superadmin_permission(apiuser):
467 467 _perms = ('repository.admin', 'repository.write', 'repository.read',)
468 468 validate_repo_permissions(apiuser, repoid, repo, _perms)
469 469
470 470 ret_type = Optional.extract(ret_type)
471 471 details = Optional.extract(details)
472 472 max_file_bytes = Optional.extract(max_file_bytes)
473 473
474 474 _extended_types = ['basic', 'full']
475 475 if details not in _extended_types:
476 476 ret_types = ','.join(_extended_types)
477 477 raise JSONRPCError(f'ret_type must be one of {ret_types}')
478 478
479 479 extended_info = False
480 480 content = False
481 481 if details == 'basic':
482 482 extended_info = True
483 483
484 484 if details == 'full':
485 485 extended_info = content = True
486 486
487 487 _map = {}
488 488 try:
489 489 # check if repo is not empty by any chance, skip quicker if it is.
490 490 _scm = repo.scm_instance()
491 491 if _scm.is_empty():
492 492 return []
493 493
494 494 _d, _f = ScmModel().get_nodes(
495 495 repo, revision, root_path, flat=False,
496 496 extended_info=extended_info, content=content,
497 497 max_file_bytes=max_file_bytes)
498 498
499 499 _map = {
500 500 'all': _d + _f,
501 501 'files': _f,
502 502 'dirs': _d,
503 503 }
504 504
505 505 return _map[ret_type]
506 506 except KeyError:
507 507 keys = ','.join(sorted(_map.keys()))
508 508 raise JSONRPCError(f'ret_type must be one of {keys}')
509 509 except Exception:
510 510 log.exception("Exception occurred while trying to get repo nodes")
511 511 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
512 512
513 513
514 514 @jsonrpc_method()
515 515 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
516 516 max_file_bytes=Optional(0), details=Optional('basic'),
517 517 cache=Optional(True)):
518 518 """
519 519 Returns a single file from repository at given revision.
520 520
521 521 This command can only be run using an |authtoken| with admin rights,
522 522 or users with at least read rights to |repos|.
523 523
524 524 :param apiuser: This is filled automatically from the |authtoken|.
525 525 :type apiuser: AuthUser
526 526 :param repoid: The repository name or repository ID.
527 527 :type repoid: str or int
528 528 :param commit_id: The revision for which listing should be done.
529 529 :type commit_id: str
530 530 :param file_path: The path from which to start displaying.
531 531 :type file_path: str
532 532 :param details: Returns different set of information about nodes.
533 533 The valid options are ``minimal`` ``basic`` and ``full``.
534 534 :type details: Optional(str)
535 535 :param max_file_bytes: Only return file content under this file size bytes
536 536 :type max_file_bytes: Optional(int)
537 537 :param cache: Use internal caches for fetching files. If disabled fetching
538 538 files is slower but more memory efficient
539 539 :type cache: Optional(bool)
540 540
541 541 Example output:
542 542
543 543 .. code-block:: bash
544 544
545 545 id : <id_given_in_input>
546 546 result: {
547 547 "binary": false,
548 548 "extension": "py",
549 549 "lines": 35,
550 550 "content": "....",
551 551 "md5": "76318336366b0f17ee249e11b0c99c41",
552 552 "mimetype": "text/x-python",
553 553 "name": "python.py",
554 554 "size": 817,
555 555 "type": "file",
556 556 }
557 557 error: null
558 558 """
559 559
560 560 repo = get_repo_or_error(repoid)
561 561 if not has_superadmin_permission(apiuser):
562 562 _perms = ('repository.admin', 'repository.write', 'repository.read',)
563 563 validate_repo_permissions(apiuser, repoid, repo, _perms)
564 564
565 565 cache = Optional.extract(cache, binary=True)
566 566 details = Optional.extract(details)
567 567 max_file_bytes = Optional.extract(max_file_bytes)
568 568
569 569 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
570 570 if details not in _extended_types:
571 571 ret_types = ','.join(_extended_types)
572 572 raise JSONRPCError(f'ret_type must be one of %s, got {ret_types}', details)
573 573 extended_info = False
574 574 content = False
575 575
576 576 if details == 'minimal':
577 577 extended_info = False
578 578
579 579 elif details == 'basic':
580 580 extended_info = True
581 581
582 582 elif details == 'full':
583 583 extended_info = content = True
584 584
585 585 file_path = safe_str(file_path)
586 586 try:
587 587 # check if repo is not empty by any chance, skip quicker if it is.
588 588 _scm = repo.scm_instance()
589 589 if _scm.is_empty():
590 590 return None
591 591
592 592 node = ScmModel().get_node(
593 593 repo, commit_id, file_path, extended_info=extended_info,
594 594 content=content, max_file_bytes=max_file_bytes, cache=cache)
595 595
596 596 except NodeDoesNotExistError:
597 597 raise JSONRPCError(
598 598 f'There is no file in repo: `{repo.repo_name}` at path `{file_path}` for commit: `{commit_id}`')
599 599 except Exception:
600 600 log.exception("Exception occurred while trying to get repo %s file",
601 601 repo.repo_name)
602 602 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` file at path {file_path}')
603 603
604 604 return node
605 605
606 606
607 607 @jsonrpc_method()
608 608 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
609 609 """
610 610 Returns a list of tree nodes for path at given revision. This api is built
611 611 strictly for usage in full text search building, and shouldn't be consumed
612 612
613 613 This command can only be run using an |authtoken| with admin rights,
614 614 or users with at least read rights to |repos|.
615 615
616 616 """
617 617
618 618 repo = get_repo_or_error(repoid)
619 619 if not has_superadmin_permission(apiuser):
620 620 _perms = ('repository.admin', 'repository.write', 'repository.read',)
621 621 validate_repo_permissions(apiuser, repoid, repo, _perms)
622 622
623 623 repo_id = repo.repo_id
624 624 cache_seconds = rhodecode.ConfigGet().get_int('rc_cache.cache_repo.expiration_time')
625 625 cache_on = cache_seconds > 0
626 626
627 627 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
628 628 rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
629 629
630 630 def compute_fts_tree(repo_id, commit_id, root_path):
631 631 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
632 632
633 633 try:
634 634 # check if repo is not empty by any chance, skip quicker if it is.
635 635 _scm = repo.scm_instance()
636 636 if not _scm or _scm.is_empty():
637 637 return []
638 638 except RepositoryError:
639 639 log.exception("Exception occurred while trying to get repo nodes")
640 640 raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes')
641 641
642 642 try:
643 643 # we need to resolve commit_id to a FULL sha for cache to work correctly.
644 644 # sending 'master' is a pointer that needs to be translated to current commit.
645 645 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
646 646 log.debug(
647 647 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
648 648 'with caching: %s[TTL: %ss]' % (
649 649 repo_id, commit_id, cache_on, cache_seconds or 0))
650 650
651 651 tree_files = compute_fts_tree(repo_id, commit_id, root_path)
652 652
653 653 return tree_files
654 654
655 655 except Exception:
656 656 log.exception("Exception occurred while trying to get repo nodes")
657 657 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
658 658
659 659
660 660 @jsonrpc_method()
661 661 def get_repo_refs(request, apiuser, repoid):
662 662 """
663 663 Returns a dictionary of current references. It returns
664 664 bookmarks, branches, closed_branches, and tags for given repository
665 665
666 666 It's possible to specify ret_type to show only `files` or `dirs`.
667 667
668 668 This command can only be run using an |authtoken| with admin rights,
669 669 or users with at least read rights to |repos|.
670 670
671 671 :param apiuser: This is filled automatically from the |authtoken|.
672 672 :type apiuser: AuthUser
673 673 :param repoid: The repository name or repository ID.
674 674 :type repoid: str or int
675 675
676 676 Example output:
677 677
678 678 .. code-block:: bash
679 679
680 680 id : <id_given_in_input>
681 681 "result": {
682 682 "bookmarks": {
683 683 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
684 684 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
685 685 },
686 686 "branches": {
687 687 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
688 688 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
689 689 },
690 690 "branches_closed": {},
691 691 "tags": {
692 692 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
693 693 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
694 694 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
695 695 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
696 696 }
697 697 }
698 698 error: null
699 699 """
700 700
701 701 repo = get_repo_or_error(repoid)
702 702 if not has_superadmin_permission(apiuser):
703 703 _perms = ('repository.admin', 'repository.write', 'repository.read',)
704 704 validate_repo_permissions(apiuser, repoid, repo, _perms)
705 705
706 706 try:
707 707 # check if repo is not empty by any chance, skip quicker if it is.
708 708 vcs_instance = repo.scm_instance()
709 709 refs = vcs_instance.refs()
710 710 return refs
711 711 except Exception:
712 712 log.exception("Exception occurred while trying to get repo refs")
713 713 raise JSONRPCError(
714 714 'failed to get repo: `%s` references' % repo.repo_name
715 715 )
716 716
717 717
718 718 @jsonrpc_method()
719 719 def create_repo(
720 720 request, apiuser, repo_name, repo_type,
721 721 owner=Optional(OAttr('apiuser')),
722 722 description=Optional(''),
723 723 private=Optional(False),
724 724 clone_uri=Optional(None),
725 725 push_uri=Optional(None),
726 726 landing_rev=Optional(None),
727 727 enable_statistics=Optional(False),
728 728 enable_locking=Optional(False),
729 729 enable_downloads=Optional(False),
730 730 copy_permissions=Optional(False)):
731 731 """
732 732 Creates a repository.
733 733
734 734 * If the repository name contains "/", repository will be created inside
735 735 a repository group or nested repository groups
736 736
737 737 For example "foo/bar/repo1" will create |repo| called "repo1" inside
738 738 group "foo/bar". You have to have permissions to access and write to
739 739 the last repository group ("bar" in this example)
740 740
741 741 This command can only be run using an |authtoken| with at least
742 742 permissions to create repositories, or write permissions to
743 743 parent repository groups.
744 744
745 745 :param apiuser: This is filled automatically from the |authtoken|.
746 746 :type apiuser: AuthUser
747 747 :param repo_name: Set the repository name.
748 748 :type repo_name: str
749 749 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
750 750 :type repo_type: str
751 751 :param owner: user_id or username
752 752 :type owner: Optional(str)
753 753 :param description: Set the repository description.
754 754 :type description: Optional(str)
755 755 :param private: set repository as private
756 756 :type private: bool
757 757 :param clone_uri: set clone_uri
758 758 :type clone_uri: str
759 759 :param push_uri: set push_uri
760 760 :type push_uri: str
761 761 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
762 762 :type landing_rev: str
763 763 :param enable_locking:
764 764 :type enable_locking: bool
765 765 :param enable_downloads:
766 766 :type enable_downloads: bool
767 767 :param enable_statistics:
768 768 :type enable_statistics: bool
769 769 :param copy_permissions: Copy permission from group in which the
770 770 repository is being created.
771 771 :type copy_permissions: bool
772 772
773 773
774 774 Example output:
775 775
776 776 .. code-block:: bash
777 777
778 778 id : <id_given_in_input>
779 779 result: {
780 780 "msg": "Created new repository `<reponame>`",
781 781 "success": true,
782 782 "task": "<celery task id or None if done sync>"
783 783 }
784 784 error: null
785 785
786 786
787 787 Example error output:
788 788
789 789 .. code-block:: bash
790 790
791 791 id : <id_given_in_input>
792 792 result : null
793 793 error : {
794 794 'failed to create repository `<repo_name>`'
795 795 }
796 796
797 797 """
798 798
799 799 owner = validate_set_owner_permissions(apiuser, owner)
800 800
801 801 description = Optional.extract(description)
802 802 copy_permissions = Optional.extract(copy_permissions)
803 803 clone_uri = Optional.extract(clone_uri)
804 804 push_uri = Optional.extract(push_uri)
805 805
806 806 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
807 807 if isinstance(private, Optional):
808 808 private = defs.get('repo_private') or Optional.extract(private)
809 809 if isinstance(repo_type, Optional):
810 810 repo_type = defs.get('repo_type')
811 811 if isinstance(enable_statistics, Optional):
812 812 enable_statistics = defs.get('repo_enable_statistics')
813 813 if isinstance(enable_locking, Optional):
814 814 enable_locking = defs.get('repo_enable_locking')
815 815 if isinstance(enable_downloads, Optional):
816 816 enable_downloads = defs.get('repo_enable_downloads')
817 817
818 818 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
819 819 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
820 820 ref_choices = list(set(ref_choices + [landing_ref]))
821 821
822 822 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
823 823
824 824 schema = repo_schema.RepoSchema().bind(
825 825 repo_type_options=rhodecode.BACKENDS.keys(),
826 826 repo_ref_options=ref_choices,
827 827 repo_type=repo_type,
828 828 # user caller
829 829 user=apiuser)
830 830
831 831 try:
832 832 schema_data = schema.deserialize(dict(
833 833 repo_name=repo_name,
834 834 repo_type=repo_type,
835 835 repo_owner=owner.username,
836 836 repo_description=description,
837 837 repo_landing_commit_ref=landing_commit_ref,
838 838 repo_clone_uri=clone_uri,
839 839 repo_push_uri=push_uri,
840 840 repo_private=private,
841 841 repo_copy_permissions=copy_permissions,
842 842 repo_enable_statistics=enable_statistics,
843 843 repo_enable_downloads=enable_downloads,
844 844 repo_enable_locking=enable_locking))
845 845 except validation_schema.Invalid as err:
846 846 raise JSONRPCValidationError(colander_exc=err)
847 847
848 848 try:
849 849 data = {
850 850 'owner': owner,
851 851 'repo_name': schema_data['repo_group']['repo_name_without_group'],
852 852 'repo_name_full': schema_data['repo_name'],
853 853 'repo_group': schema_data['repo_group']['repo_group_id'],
854 854 'repo_type': schema_data['repo_type'],
855 855 'repo_description': schema_data['repo_description'],
856 856 'repo_private': schema_data['repo_private'],
857 857 'clone_uri': schema_data['repo_clone_uri'],
858 858 'push_uri': schema_data['repo_push_uri'],
859 859 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
860 860 'enable_statistics': schema_data['repo_enable_statistics'],
861 861 'enable_locking': schema_data['repo_enable_locking'],
862 862 'enable_downloads': schema_data['repo_enable_downloads'],
863 863 'repo_copy_permissions': schema_data['repo_copy_permissions'],
864 864 }
865 865
866 866 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
867 867 task_id = get_task_id(task)
868 868 # no commit, it's done in RepoModel, or async via celery
869 869 return {
870 870 'msg': "Created new repository `{}`".format(schema_data['repo_name']),
871 871 'success': True, # cannot return the repo data here since fork
872 872 # can be done async
873 873 'task': task_id
874 874 }
875 875 except Exception:
876 876 log.exception(
877 877 "Exception while trying to create the repository %s",
878 878 schema_data['repo_name'])
879 879 raise JSONRPCError(
880 880 'failed to create repository `{}`'.format(schema_data['repo_name']))
881 881
882 882
883 883 @jsonrpc_method()
884 884 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
885 885 description=Optional('')):
886 886 """
887 887 Adds an extra field to a repository.
888 888
889 889 This command can only be run using an |authtoken| with at least
890 890 write permissions to the |repo|.
891 891
892 892 :param apiuser: This is filled automatically from the |authtoken|.
893 893 :type apiuser: AuthUser
894 894 :param repoid: Set the repository name or repository id.
895 895 :type repoid: str or int
896 896 :param key: Create a unique field key for this repository.
897 897 :type key: str
898 898 :param label:
899 899 :type label: Optional(str)
900 900 :param description:
901 901 :type description: Optional(str)
902 902 """
903 903 repo = get_repo_or_error(repoid)
904 904 if not has_superadmin_permission(apiuser):
905 905 _perms = ('repository.admin',)
906 906 validate_repo_permissions(apiuser, repoid, repo, _perms)
907 907
908 908 label = Optional.extract(label) or key
909 909 description = Optional.extract(description)
910 910
911 911 field = RepositoryField.get_by_key_name(key, repo)
912 912 if field:
913 913 raise JSONRPCError(f'Field with key `{key}` exists for repo `{repoid}`')
914 914
915 915 try:
916 916 RepoModel().add_repo_field(repo, key, field_label=label,
917 917 field_desc=description)
918 918 Session().commit()
919 919 return {
920 920 'msg': f"Added new repository field `{key}`",
921 921 'success': True,
922 922 }
923 923 except Exception:
924 924 log.exception("Exception occurred while trying to add field to repo")
925 925 raise JSONRPCError(
926 926 f'failed to create new field for repository `{repoid}`')
927 927
928 928
929 929 @jsonrpc_method()
930 930 def remove_field_from_repo(request, apiuser, repoid, key):
931 931 """
932 932 Removes an extra field from a repository.
933 933
934 934 This command can only be run using an |authtoken| with at least
935 935 write permissions to the |repo|.
936 936
937 937 :param apiuser: This is filled automatically from the |authtoken|.
938 938 :type apiuser: AuthUser
939 939 :param repoid: Set the repository name or repository ID.
940 940 :type repoid: str or int
941 941 :param key: Set the unique field key for this repository.
942 942 :type key: str
943 943 """
944 944
945 945 repo = get_repo_or_error(repoid)
946 946 if not has_superadmin_permission(apiuser):
947 947 _perms = ('repository.admin',)
948 948 validate_repo_permissions(apiuser, repoid, repo, _perms)
949 949
950 950 field = RepositoryField.get_by_key_name(key, repo)
951 951 if not field:
952 952 raise JSONRPCError('Field with key `%s` does not '
953 953 'exists for repo `%s`' % (key, repoid))
954 954
955 955 try:
956 956 RepoModel().delete_repo_field(repo, field_key=key)
957 957 Session().commit()
958 958 return {
959 959 'msg': f"Deleted repository field `{key}`",
960 960 'success': True,
961 961 }
962 962 except Exception:
963 963 log.exception(
964 964 "Exception occurred while trying to delete field from repo")
965 965 raise JSONRPCError(
966 966 f'failed to delete field for repository `{repoid}`')
967 967
968 968
969 969 @jsonrpc_method()
970 970 def update_repo(
971 971 request, apiuser, repoid, repo_name=Optional(None),
972 972 owner=Optional(OAttr('apiuser')), description=Optional(''),
973 973 private=Optional(False),
974 974 clone_uri=Optional(None), push_uri=Optional(None),
975 975 landing_rev=Optional(None), fork_of=Optional(None),
976 976 enable_statistics=Optional(False),
977 977 enable_locking=Optional(False),
978 978 enable_downloads=Optional(False), fields=Optional('')):
979 979 r"""
980 980 Updates a repository with the given information.
981 981
982 982 This command can only be run using an |authtoken| with at least
983 983 admin permissions to the |repo|.
984 984
985 985 * If the repository name contains "/", repository will be updated
986 986 accordingly with a repository group or nested repository groups
987 987
988 988 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
989 989 called "repo-test" and place it inside group "foo/bar".
990 990 You have to have permissions to access and write to the last repository
991 991 group ("bar" in this example)
992 992
993 993 :param apiuser: This is filled automatically from the |authtoken|.
994 994 :type apiuser: AuthUser
995 995 :param repoid: repository name or repository ID.
996 996 :type repoid: str or int
997 997 :param repo_name: Update the |repo| name, including the
998 998 repository group it's in.
999 999 :type repo_name: str
1000 1000 :param owner: Set the |repo| owner.
1001 1001 :type owner: str
1002 1002 :param fork_of: Set the |repo| as fork of another |repo|.
1003 1003 :type fork_of: str
1004 1004 :param description: Update the |repo| description.
1005 1005 :type description: str
1006 1006 :param private: Set the |repo| as private. (True | False)
1007 1007 :type private: bool
1008 1008 :param clone_uri: Update the |repo| clone URI.
1009 1009 :type clone_uri: str
1010 1010 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1011 1011 :type landing_rev: str
1012 1012 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1013 1013 :type enable_statistics: bool
1014 1014 :param enable_locking: Enable |repo| locking.
1015 1015 :type enable_locking: bool
1016 1016 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1017 1017 :type enable_downloads: bool
1018 1018 :param fields: Add extra fields to the |repo|. Use the following
1019 1019 example format: ``field_key=field_val,field_key2=fieldval2``.
1020 1020 Escape ', ' with \,
1021 1021 :type fields: str
1022 1022 """
1023 1023
1024 1024 repo = get_repo_or_error(repoid)
1025 1025
1026 1026 include_secrets = False
1027 1027 if not has_superadmin_permission(apiuser):
1028 1028 _perms = ('repository.admin',)
1029 1029 validate_repo_permissions(apiuser, repoid, repo, _perms)
1030 1030 else:
1031 1031 include_secrets = True
1032 1032
1033 1033 updates = dict(
1034 1034 repo_name=repo_name
1035 1035 if not isinstance(repo_name, Optional) else repo.repo_name,
1036 1036
1037 1037 fork_id=fork_of
1038 1038 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1039 1039
1040 1040 user=owner
1041 1041 if not isinstance(owner, Optional) else repo.user.username,
1042 1042
1043 1043 repo_description=description
1044 1044 if not isinstance(description, Optional) else repo.description,
1045 1045
1046 1046 repo_private=private
1047 1047 if not isinstance(private, Optional) else repo.private,
1048 1048
1049 1049 clone_uri=clone_uri
1050 1050 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1051 1051
1052 1052 push_uri=push_uri
1053 1053 if not isinstance(push_uri, Optional) else repo.push_uri,
1054 1054
1055 1055 repo_landing_rev=landing_rev
1056 1056 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1057 1057
1058 1058 repo_enable_statistics=enable_statistics
1059 1059 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1060 1060
1061 1061 repo_enable_locking=enable_locking
1062 1062 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1063 1063
1064 1064 repo_enable_downloads=enable_downloads
1065 1065 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1066 1066
1067 1067 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1068 1068 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1069 1069 request.translate, repo=repo)
1070 1070 ref_choices = list(set(ref_choices + [landing_ref]))
1071 1071
1072 1072 old_values = repo.get_api_data()
1073 1073 repo_type = repo.repo_type
1074 1074 schema = repo_schema.RepoSchema().bind(
1075 1075 repo_type_options=rhodecode.BACKENDS.keys(),
1076 1076 repo_ref_options=ref_choices,
1077 1077 repo_type=repo_type,
1078 1078 # user caller
1079 1079 user=apiuser,
1080 1080 old_values=old_values)
1081 1081 try:
1082 1082 schema_data = schema.deserialize(dict(
1083 1083 # we save old value, users cannot change type
1084 1084 repo_type=repo_type,
1085 1085
1086 1086 repo_name=updates['repo_name'],
1087 1087 repo_owner=updates['user'],
1088 1088 repo_description=updates['repo_description'],
1089 1089 repo_clone_uri=updates['clone_uri'],
1090 1090 repo_push_uri=updates['push_uri'],
1091 1091 repo_fork_of=updates['fork_id'],
1092 1092 repo_private=updates['repo_private'],
1093 1093 repo_landing_commit_ref=updates['repo_landing_rev'],
1094 1094 repo_enable_statistics=updates['repo_enable_statistics'],
1095 1095 repo_enable_downloads=updates['repo_enable_downloads'],
1096 1096 repo_enable_locking=updates['repo_enable_locking']))
1097 1097 except validation_schema.Invalid as err:
1098 1098 raise JSONRPCValidationError(colander_exc=err)
1099 1099
1100 1100 # save validated data back into the updates dict
1101 1101 validated_updates = dict(
1102 1102 repo_name=schema_data['repo_group']['repo_name_without_group'],
1103 1103 repo_group=schema_data['repo_group']['repo_group_id'],
1104 1104
1105 1105 user=schema_data['repo_owner'],
1106 1106 repo_description=schema_data['repo_description'],
1107 1107 repo_private=schema_data['repo_private'],
1108 1108 clone_uri=schema_data['repo_clone_uri'],
1109 1109 push_uri=schema_data['repo_push_uri'],
1110 1110 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1111 1111 repo_enable_statistics=schema_data['repo_enable_statistics'],
1112 1112 repo_enable_locking=schema_data['repo_enable_locking'],
1113 1113 repo_enable_downloads=schema_data['repo_enable_downloads'],
1114 1114 )
1115 1115
1116 1116 if schema_data['repo_fork_of']:
1117 1117 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1118 1118 validated_updates['fork_id'] = fork_repo.repo_id
1119 1119
1120 1120 # extra fields
1121 1121 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1122 1122 if fields:
1123 1123 validated_updates.update(fields)
1124 1124
1125 1125 try:
1126 1126 RepoModel().update(repo, **validated_updates)
1127 1127 audit_logger.store_api(
1128 1128 'repo.edit', action_data={'old_data': old_values},
1129 1129 user=apiuser, repo=repo)
1130 1130 Session().commit()
1131 1131 return {
1132 1132 'msg': f'updated repo ID:{repo.repo_id} {repo.repo_name}',
1133 1133 'repository': repo.get_api_data(include_secrets=include_secrets)
1134 1134 }
1135 1135 except Exception:
1136 1136 log.exception(
1137 1137 "Exception while trying to update the repository %s",
1138 1138 repoid)
1139 1139 raise JSONRPCError('failed to update repo `%s`' % repoid)
1140 1140
1141 1141
1142 1142 @jsonrpc_method()
1143 1143 def fork_repo(request, apiuser, repoid, fork_name,
1144 1144 owner=Optional(OAttr('apiuser')),
1145 1145 description=Optional(''),
1146 1146 private=Optional(False),
1147 1147 clone_uri=Optional(None),
1148 1148 landing_rev=Optional(None),
1149 1149 copy_permissions=Optional(False)):
1150 1150 """
1151 1151 Creates a fork of the specified |repo|.
1152 1152
1153 1153 * If the fork_name contains "/", fork will be created inside
1154 1154 a repository group or nested repository groups
1155 1155
1156 1156 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1157 1157 inside group "foo/bar". You have to have permissions to access and
1158 1158 write to the last repository group ("bar" in this example)
1159 1159
1160 1160 This command can only be run using an |authtoken| with minimum
1161 1161 read permissions of the forked repo, create fork permissions for an user.
1162 1162
1163 1163 :param apiuser: This is filled automatically from the |authtoken|.
1164 1164 :type apiuser: AuthUser
1165 1165 :param repoid: Set repository name or repository ID.
1166 1166 :type repoid: str or int
1167 1167 :param fork_name: Set the fork name, including it's repository group membership.
1168 1168 :type fork_name: str
1169 1169 :param owner: Set the fork owner.
1170 1170 :type owner: str
1171 1171 :param description: Set the fork description.
1172 1172 :type description: str
1173 1173 :param copy_permissions: Copy permissions from parent |repo|. The
1174 1174 default is False.
1175 1175 :type copy_permissions: bool
1176 1176 :param private: Make the fork private. The default is False.
1177 1177 :type private: bool
1178 1178 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1179 1179
1180 1180 Example output:
1181 1181
1182 1182 .. code-block:: bash
1183 1183
1184 1184 id : <id_for_response>
1185 1185 api_key : "<api_key>"
1186 1186 args: {
1187 1187 "repoid" : "<reponame or repo_id>",
1188 1188 "fork_name": "<forkname>",
1189 1189 "owner": "<username or user_id = Optional(=apiuser)>",
1190 1190 "description": "<description>",
1191 1191 "copy_permissions": "<bool>",
1192 1192 "private": "<bool>",
1193 1193 "landing_rev": "<landing_rev>"
1194 1194 }
1195 1195
1196 1196 Example error output:
1197 1197
1198 1198 .. code-block:: bash
1199 1199
1200 1200 id : <id_given_in_input>
1201 1201 result: {
1202 1202 "msg": "Created fork of `<reponame>` as `<forkname>`",
1203 1203 "success": true,
1204 1204 "task": "<celery task id or None if done sync>"
1205 1205 }
1206 1206 error: null
1207 1207
1208 1208 """
1209 1209
1210 1210 repo = get_repo_or_error(repoid)
1211 1211 repo_name = repo.repo_name
1212 1212
1213 1213 if not has_superadmin_permission(apiuser):
1214 1214 # check if we have at least read permission for
1215 1215 # this repo that we fork !
1216 1216 _perms = ('repository.admin', 'repository.write', 'repository.read')
1217 1217 validate_repo_permissions(apiuser, repoid, repo, _perms)
1218 1218
1219 1219 # check if the regular user has at least fork permissions as well
1220 1220 if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser):
1221 1221 raise JSONRPCForbidden()
1222 1222
1223 1223 # check if user can set owner parameter
1224 1224 owner = validate_set_owner_permissions(apiuser, owner)
1225 1225
1226 1226 description = Optional.extract(description)
1227 1227 copy_permissions = Optional.extract(copy_permissions)
1228 1228 clone_uri = Optional.extract(clone_uri)
1229 1229
1230 1230 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1231 1231 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1232 1232 ref_choices = list(set(ref_choices + [landing_ref]))
1233 1233 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1234 1234
1235 1235 private = Optional.extract(private)
1236 1236
1237 1237 schema = repo_schema.RepoSchema().bind(
1238 1238 repo_type_options=rhodecode.BACKENDS.keys(),
1239 1239 repo_ref_options=ref_choices,
1240 1240 repo_type=repo.repo_type,
1241 1241 # user caller
1242 1242 user=apiuser)
1243 1243
1244 1244 try:
1245 1245 schema_data = schema.deserialize(dict(
1246 1246 repo_name=fork_name,
1247 1247 repo_type=repo.repo_type,
1248 1248 repo_owner=owner.username,
1249 1249 repo_description=description,
1250 1250 repo_landing_commit_ref=landing_commit_ref,
1251 1251 repo_clone_uri=clone_uri,
1252 1252 repo_private=private,
1253 1253 repo_copy_permissions=copy_permissions))
1254 1254 except validation_schema.Invalid as err:
1255 1255 raise JSONRPCValidationError(colander_exc=err)
1256 1256
1257 1257 try:
1258 1258 data = {
1259 1259 'fork_parent_id': repo.repo_id,
1260 1260
1261 1261 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1262 1262 'repo_name_full': schema_data['repo_name'],
1263 1263 'repo_group': schema_data['repo_group']['repo_group_id'],
1264 1264 'repo_type': schema_data['repo_type'],
1265 1265 'description': schema_data['repo_description'],
1266 1266 'private': schema_data['repo_private'],
1267 1267 'copy_permissions': schema_data['repo_copy_permissions'],
1268 1268 'landing_rev': schema_data['repo_landing_commit_ref'],
1269 1269 }
1270 1270
1271 1271 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1272 1272 # no commit, it's done in RepoModel, or async via celery
1273 1273 task_id = get_task_id(task)
1274 1274
1275 1275 return {
1276 1276 'msg': 'Created fork of `{}` as `{}`'.format(
1277 1277 repo.repo_name, schema_data['repo_name']),
1278 1278 'success': True, # cannot return the repo data here since fork
1279 1279 # can be done async
1280 1280 'task': task_id
1281 1281 }
1282 1282 except Exception:
1283 1283 log.exception(
1284 1284 "Exception while trying to create fork %s",
1285 1285 schema_data['repo_name'])
1286 1286 raise JSONRPCError(
1287 1287 'failed to fork repository `{}` as `{}`'.format(
1288 1288 repo_name, schema_data['repo_name']))
1289 1289
1290 1290
1291 1291 @jsonrpc_method()
1292 1292 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1293 1293 """
1294 1294 Deletes a repository.
1295 1295
1296 1296 * When the `forks` parameter is set it's possible to detach or delete
1297 1297 forks of deleted repository.
1298 1298
1299 1299 This command can only be run using an |authtoken| with admin
1300 1300 permissions on the |repo|.
1301 1301
1302 1302 :param apiuser: This is filled automatically from the |authtoken|.
1303 1303 :type apiuser: AuthUser
1304 1304 :param repoid: Set the repository name or repository ID.
1305 1305 :type repoid: str or int
1306 1306 :param forks: Set to `detach` or `delete` forks from the |repo|.
1307 1307 :type forks: Optional(str)
1308 1308
1309 1309 Example error output:
1310 1310
1311 1311 .. code-block:: bash
1312 1312
1313 1313 id : <id_given_in_input>
1314 1314 result: {
1315 1315 "msg": "Deleted repository `<reponame>`",
1316 1316 "success": true
1317 1317 }
1318 1318 error: null
1319 1319 """
1320 1320
1321 1321 repo = get_repo_or_error(repoid)
1322 1322 repo_name = repo.repo_name
1323 1323 if not has_superadmin_permission(apiuser):
1324 1324 _perms = ('repository.admin',)
1325 1325 validate_repo_permissions(apiuser, repoid, repo, _perms)
1326 1326
1327 1327 try:
1328 1328 handle_forks = Optional.extract(forks)
1329 1329 _forks_msg = ''
1330 1330 _forks = [f for f in repo.forks]
1331 1331 if handle_forks == 'detach':
1332 1332 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1333 1333 elif handle_forks == 'delete':
1334 1334 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1335 1335 elif _forks:
1336 1336 raise JSONRPCError(
1337 1337 'Cannot delete `%s` it still contains attached forks' %
1338 1338 (repo.repo_name,)
1339 1339 )
1340 1340 old_data = repo.get_api_data()
1341 1341 RepoModel().delete(repo, forks=forks)
1342 1342
1343 1343 repo = audit_logger.RepoWrap(repo_id=None,
1344 1344 repo_name=repo.repo_name)
1345 1345
1346 1346 audit_logger.store_api(
1347 1347 'repo.delete', action_data={'old_data': old_data},
1348 1348 user=apiuser, repo=repo)
1349 1349
1350 1350 ScmModel().mark_for_invalidation(repo_name, delete=True)
1351 1351 Session().commit()
1352 1352 return {
1353 1353 'msg': f'Deleted repository `{repo_name}`{_forks_msg}',
1354 1354 'success': True
1355 1355 }
1356 1356 except Exception:
1357 1357 log.exception("Exception occurred while trying to delete repo")
1358 1358 raise JSONRPCError(
1359 1359 f'failed to delete repository `{repo_name}`'
1360 1360 )
1361 1361
1362 1362
1363 1363 #TODO: marcink, change name ?
1364 1364 @jsonrpc_method()
1365 1365 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1366 1366 """
1367 1367 Invalidates the cache for the specified repository.
1368 1368
1369 1369 This command can only be run using an |authtoken| with admin rights to
1370 1370 the specified repository.
1371 1371
1372 1372 This command takes the following options:
1373 1373
1374 1374 :param apiuser: This is filled automatically from |authtoken|.
1375 1375 :type apiuser: AuthUser
1376 1376 :param repoid: Sets the repository name or repository ID.
1377 1377 :type repoid: str or int
1378 1378 :param delete_keys: This deletes the invalidated keys instead of
1379 1379 just flagging them.
1380 1380 :type delete_keys: Optional(``True`` | ``False``)
1381 1381
1382 1382 Example output:
1383 1383
1384 1384 .. code-block:: bash
1385 1385
1386 1386 id : <id_given_in_input>
1387 1387 result : {
1388 1388 'msg': Cache for repository `<repository name>` was invalidated,
1389 1389 'repository': <repository name>
1390 1390 }
1391 1391 error : null
1392 1392
1393 1393 Example error output:
1394 1394
1395 1395 .. code-block:: bash
1396 1396
1397 1397 id : <id_given_in_input>
1398 1398 result : null
1399 1399 error : {
1400 1400 'Error occurred during cache invalidation action'
1401 1401 }
1402 1402
1403 1403 """
1404 1404
1405 1405 repo = get_repo_or_error(repoid)
1406 1406 if not has_superadmin_permission(apiuser):
1407 1407 _perms = ('repository.admin', 'repository.write',)
1408 1408 validate_repo_permissions(apiuser, repoid, repo, _perms)
1409 1409
1410 1410 delete = Optional.extract(delete_keys)
1411 1411 try:
1412 1412 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1413 1413 return {
1414 1414 'msg': f'Cache for repository `{repoid}` was invalidated',
1415 1415 'repository': repo.repo_name
1416 1416 }
1417 1417 except Exception:
1418 1418 log.exception(
1419 1419 "Exception occurred while trying to invalidate repo cache")
1420 1420 raise JSONRPCError(
1421 1421 'Error occurred during cache invalidation action'
1422 1422 )
1423 1423
1424 1424
1425 1425 #TODO: marcink, change name ?
1426 1426 @jsonrpc_method()
1427 1427 def lock(request, apiuser, repoid, locked=Optional(None),
1428 1428 userid=Optional(OAttr('apiuser'))):
1429 1429 """
1430 1430 Sets the lock state of the specified |repo| by the given user.
1431 1431 From more information, see :ref:`repo-locking`.
1432 1432
1433 1433 * If the ``userid`` option is not set, the repository is locked to the
1434 1434 user who called the method.
1435 1435 * If the ``locked`` parameter is not set, the current lock state of the
1436 1436 repository is displayed.
1437 1437
1438 1438 This command can only be run using an |authtoken| with admin rights to
1439 1439 the specified repository.
1440 1440
1441 1441 This command takes the following options:
1442 1442
1443 1443 :param apiuser: This is filled automatically from the |authtoken|.
1444 1444 :type apiuser: AuthUser
1445 1445 :param repoid: Sets the repository name or repository ID.
1446 1446 :type repoid: str or int
1447 1447 :param locked: Sets the lock state.
1448 1448 :type locked: Optional(``True`` | ``False``)
1449 1449 :param userid: Set the repository lock to this user.
1450 1450 :type userid: Optional(str or int)
1451 1451
1452 1452 Example error output:
1453 1453
1454 1454 .. code-block:: bash
1455 1455
1456 1456 id : <id_given_in_input>
1457 1457 result : {
1458 1458 'repo': '<reponame>',
1459 1459 'locked': <bool: lock state>,
1460 1460 'locked_since': <int: lock timestamp>,
1461 1461 'locked_by': <username of person who made the lock>,
1462 1462 'lock_reason': <str: reason for locking>,
1463 1463 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1464 1464 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1465 1465 or
1466 1466 'msg': 'Repo `<repository name>` not locked.'
1467 1467 or
1468 1468 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1469 1469 }
1470 1470 error : null
1471 1471
1472 1472 Example error output:
1473 1473
1474 1474 .. code-block:: bash
1475 1475
1476 1476 id : <id_given_in_input>
1477 1477 result : null
1478 1478 error : {
1479 1479 'Error occurred locking repository `<reponame>`'
1480 1480 }
1481 1481 """
1482 1482
1483 1483 repo = get_repo_or_error(repoid)
1484 1484 if not has_superadmin_permission(apiuser):
1485 1485 # check if we have at least write permission for this repo !
1486 1486 _perms = ('repository.admin', 'repository.write',)
1487 1487 validate_repo_permissions(apiuser, repoid, repo, _perms)
1488 1488
1489 1489 # make sure normal user does not pass someone else userid,
1490 1490 # he is not allowed to do that
1491 1491 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1492 1492 raise JSONRPCError('userid is not the same as your user')
1493 1493
1494 1494 if isinstance(userid, Optional):
1495 1495 userid = apiuser.user_id
1496 1496
1497 1497 user = get_user_or_error(userid)
1498 1498
1499 1499 if isinstance(locked, Optional):
1500 1500 lockobj = repo.locked
1501 1501
1502 1502 if lockobj[0] is None:
1503 1503 _d = {
1504 1504 'repo': repo.repo_name,
1505 1505 'locked': False,
1506 1506 'locked_since': None,
1507 1507 'locked_by': None,
1508 1508 'lock_reason': None,
1509 1509 'lock_state_changed': False,
1510 1510 'msg': 'Repo `%s` not locked.' % repo.repo_name
1511 1511 }
1512 1512 return _d
1513 1513 else:
1514 1514 _user_id, _time, _reason = lockobj
1515 1515 lock_user = get_user_or_error(userid)
1516 1516 _d = {
1517 1517 'repo': repo.repo_name,
1518 1518 'locked': True,
1519 1519 'locked_since': _time,
1520 1520 'locked_by': lock_user.username,
1521 1521 'lock_reason': _reason,
1522 1522 'lock_state_changed': False,
1523 1523 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1524 1524 % (repo.repo_name, lock_user.username,
1525 1525 json.dumps(time_to_datetime(_time))))
1526 1526 }
1527 1527 return _d
1528 1528
1529 1529 # force locked state through a flag
1530 1530 else:
1531 1531 locked = str2bool(locked)
1532 1532 lock_reason = Repository.LOCK_API
1533 1533 try:
1534 1534 if locked:
1535 1535 lock_time = time.time()
1536 1536 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1537 1537 else:
1538 1538 lock_time = None
1539 1539 Repository.unlock(repo)
1540 1540 _d = {
1541 1541 'repo': repo.repo_name,
1542 1542 'locked': locked,
1543 1543 'locked_since': lock_time,
1544 1544 'locked_by': user.username,
1545 1545 'lock_reason': lock_reason,
1546 1546 'lock_state_changed': True,
1547 1547 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1548 1548 % (user.username, repo.repo_name, locked))
1549 1549 }
1550 1550 return _d
1551 1551 except Exception:
1552 1552 log.exception(
1553 1553 "Exception occurred while trying to lock repository")
1554 1554 raise JSONRPCError(
1555 1555 'Error occurred locking repository `%s`' % repo.repo_name
1556 1556 )
1557 1557
1558 1558
1559 1559 @jsonrpc_method()
1560 1560 def comment_commit(
1561 1561 request, apiuser, repoid, commit_id, message, status=Optional(None),
1562 1562 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1563 1563 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1564 1564 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1565 1565 """
1566 1566 Set a commit comment, and optionally change the status of the commit.
1567 1567
1568 1568 :param apiuser: This is filled automatically from the |authtoken|.
1569 1569 :type apiuser: AuthUser
1570 1570 :param repoid: Set the repository name or repository ID.
1571 1571 :type repoid: str or int
1572 1572 :param commit_id: Specify the commit_id for which to set a comment.
1573 1573 :type commit_id: str
1574 1574 :param message: The comment text.
1575 1575 :type message: str
1576 1576 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1577 1577 'approved', 'rejected', 'under_review'
1578 1578 :type status: str
1579 1579 :param comment_type: Comment type, one of: 'note', 'todo'
1580 1580 :type comment_type: Optional(str), default: 'note'
1581 1581 :param resolves_comment_id: id of comment which this one will resolve
1582 1582 :type resolves_comment_id: Optional(int)
1583 1583 :param extra_recipients: list of user ids or usernames to add
1584 1584 notifications for this comment. Acts like a CC for notification
1585 1585 :type extra_recipients: Optional(list)
1586 1586 :param userid: Set the user name of the comment creator.
1587 1587 :type userid: Optional(str or int)
1588 1588 :param send_email: Define if this comment should also send email notification
1589 1589 :type send_email: Optional(bool)
1590 1590
1591 1591 Example error output:
1592 1592
1593 1593 .. code-block:: bash
1594 1594
1595 1595 {
1596 1596 "id" : <id_given_in_input>,
1597 1597 "result" : {
1598 1598 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1599 1599 "status_change": null or <status>,
1600 1600 "success": true
1601 1601 },
1602 1602 "error" : null
1603 1603 }
1604 1604
1605 1605 """
1606 1606 _ = request.translate
1607 1607
1608 1608 repo = get_repo_or_error(repoid)
1609 1609 if not has_superadmin_permission(apiuser):
1610 1610 _perms = ('repository.read', 'repository.write', 'repository.admin')
1611 1611 validate_repo_permissions(apiuser, repoid, repo, _perms)
1612 1612 db_repo_name = repo.repo_name
1613 1613
1614 1614 try:
1615 1615 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1616 1616 commit_id = commit.raw_id
1617 1617 except Exception as e:
1618 1618 log.exception('Failed to fetch commit')
1619 1619 raise JSONRPCError(safe_str(e))
1620 1620
1621 1621 if isinstance(userid, Optional):
1622 1622 userid = apiuser.user_id
1623 1623
1624 1624 user = get_user_or_error(userid)
1625 1625 status = Optional.extract(status)
1626 1626 comment_type = Optional.extract(comment_type)
1627 1627 resolves_comment_id = Optional.extract(resolves_comment_id)
1628 1628 extra_recipients = Optional.extract(extra_recipients)
1629 1629 send_email = Optional.extract(send_email, binary=True)
1630 1630
1631 1631 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1632 1632 if status and status not in allowed_statuses:
1633 1633 raise JSONRPCError('Bad status, must be on '
1634 1634 'of %s got %s' % (allowed_statuses, status,))
1635 1635
1636 1636 if resolves_comment_id:
1637 1637 comment = ChangesetComment.get(resolves_comment_id)
1638 1638 if not comment:
1639 1639 raise JSONRPCError(
1640 1640 'Invalid resolves_comment_id `%s` for this commit.'
1641 1641 % resolves_comment_id)
1642 1642 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1643 1643 raise JSONRPCError(
1644 1644 'Comment `%s` is wrong type for setting status to resolved.'
1645 1645 % resolves_comment_id)
1646 1646
1647 1647 try:
1648 1648 rc_config = SettingsModel().get_all_settings()
1649 1649 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1650 1650 status_change_label = ChangesetStatus.get_status_lbl(status)
1651 1651 comment = CommentsModel().create(
1652 1652 message, repo, user, commit_id=commit_id,
1653 1653 status_change=status_change_label,
1654 1654 status_change_type=status,
1655 1655 renderer=renderer,
1656 1656 comment_type=comment_type,
1657 1657 resolves_comment_id=resolves_comment_id,
1658 1658 auth_user=apiuser,
1659 1659 extra_recipients=extra_recipients,
1660 1660 send_email=send_email
1661 1661 )
1662 1662 is_inline = comment.is_inline
1663 1663
1664 1664 if status:
1665 1665 # also do a status change
1666 1666 try:
1667 1667 ChangesetStatusModel().set_status(
1668 1668 repo, status, user, comment, revision=commit_id,
1669 1669 dont_allow_on_closed_pull_request=True
1670 1670 )
1671 1671 except StatusChangeOnClosedPullRequestError:
1672 1672 log.exception(
1673 1673 "Exception occurred while trying to change repo commit status")
1674 1674 msg = ('Changing status on a commit associated with '
1675 1675 'a closed pull request is not allowed')
1676 1676 raise JSONRPCError(msg)
1677 1677
1678 1678 CommentsModel().trigger_commit_comment_hook(
1679 1679 repo, apiuser, 'create',
1680 1680 data={'comment': comment, 'commit': commit})
1681 1681
1682 1682 Session().commit()
1683 1683
1684 1684 comment_broadcast_channel = channelstream.comment_channel(
1685 1685 db_repo_name, commit_obj=commit)
1686 1686
1687 1687 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1688 1688 comment_type = 'inline' if is_inline else 'general'
1689 1689 channelstream.comment_channelstream_push(
1690 1690 request, comment_broadcast_channel, apiuser,
1691 1691 _('posted a new {} comment').format(comment_type),
1692 1692 comment_data=comment_data)
1693 1693
1694 1694 return {
1695 1695 'msg': (
1696 1696 'Commented on commit `{}` for repository `{}`'.format(
1697 1697 comment.revision, repo.repo_name)),
1698 1698 'status_change': status,
1699 1699 'success': True,
1700 1700 }
1701 1701 except JSONRPCError:
1702 1702 # catch any inside errors, and re-raise them to prevent from
1703 1703 # below global catch to silence them
1704 1704 raise
1705 1705 except Exception:
1706 1706 log.exception("Exception occurred while trying to comment on commit")
1707 1707 raise JSONRPCError(
1708 1708 f'failed to set comment on repository `{repo.repo_name}`'
1709 1709 )
1710 1710
1711 1711
1712 1712 @jsonrpc_method()
1713 1713 def get_repo_comments(request, apiuser, repoid,
1714 1714 commit_id=Optional(None), comment_type=Optional(None),
1715 1715 userid=Optional(None)):
1716 1716 """
1717 1717 Get all comments for a repository
1718 1718
1719 1719 :param apiuser: This is filled automatically from the |authtoken|.
1720 1720 :type apiuser: AuthUser
1721 1721 :param repoid: Set the repository name or repository ID.
1722 1722 :type repoid: str or int
1723 1723 :param commit_id: Optionally filter the comments by the commit_id
1724 1724 :type commit_id: Optional(str), default: None
1725 1725 :param comment_type: Optionally filter the comments by the comment_type
1726 1726 one of: 'note', 'todo'
1727 1727 :type comment_type: Optional(str), default: None
1728 1728 :param userid: Optionally filter the comments by the author of comment
1729 1729 :type userid: Optional(str or int), Default: None
1730 1730
1731 1731 Example error output:
1732 1732
1733 1733 .. code-block:: bash
1734 1734
1735 1735 {
1736 1736 "id" : <id_given_in_input>,
1737 1737 "result" : [
1738 1738 {
1739 1739 "comment_author": <USER_DETAILS>,
1740 1740 "comment_created_on": "2017-02-01T14:38:16.309",
1741 1741 "comment_f_path": "file.txt",
1742 1742 "comment_id": 282,
1743 1743 "comment_lineno": "n1",
1744 1744 "comment_resolved_by": null,
1745 1745 "comment_status": [],
1746 1746 "comment_text": "This file needs a header",
1747 1747 "comment_type": "todo",
1748 1748 "comment_last_version: 0
1749 1749 }
1750 1750 ],
1751 1751 "error" : null
1752 1752 }
1753 1753
1754 1754 """
1755 1755 repo = get_repo_or_error(repoid)
1756 1756 if not has_superadmin_permission(apiuser):
1757 1757 _perms = ('repository.read', 'repository.write', 'repository.admin')
1758 1758 validate_repo_permissions(apiuser, repoid, repo, _perms)
1759 1759
1760 1760 commit_id = Optional.extract(commit_id)
1761 1761
1762 1762 userid = Optional.extract(userid)
1763 1763 if userid:
1764 1764 user = get_user_or_error(userid)
1765 1765 else:
1766 1766 user = None
1767 1767
1768 1768 comment_type = Optional.extract(comment_type)
1769 1769 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1770 1770 raise JSONRPCError(
1771 1771 'comment_type must be one of `{}` got {}'.format(
1772 1772 ChangesetComment.COMMENT_TYPES, comment_type)
1773 1773 )
1774 1774
1775 1775 comments = CommentsModel().get_repository_comments(
1776 1776 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1777 1777 return comments
1778 1778
1779 1779
1780 1780 @jsonrpc_method()
1781 1781 def get_comment(request, apiuser, comment_id):
1782 1782 """
1783 1783 Get single comment from repository or pull_request
1784 1784
1785 1785 :param apiuser: This is filled automatically from the |authtoken|.
1786 1786 :type apiuser: AuthUser
1787 1787 :param comment_id: comment id found in the URL of comment
1788 1788 :type comment_id: str or int
1789 1789
1790 1790 Example error output:
1791 1791
1792 1792 .. code-block:: bash
1793 1793
1794 1794 {
1795 1795 "id" : <id_given_in_input>,
1796 1796 "result" : {
1797 1797 "comment_author": <USER_DETAILS>,
1798 1798 "comment_created_on": "2017-02-01T14:38:16.309",
1799 1799 "comment_f_path": "file.txt",
1800 1800 "comment_id": 282,
1801 1801 "comment_lineno": "n1",
1802 1802 "comment_resolved_by": null,
1803 1803 "comment_status": [],
1804 1804 "comment_text": "This file needs a header",
1805 1805 "comment_type": "todo",
1806 1806 "comment_last_version: 0
1807 1807 },
1808 1808 "error" : null
1809 1809 }
1810 1810
1811 1811 """
1812 1812
1813 1813 comment = ChangesetComment.get(comment_id)
1814 1814 if not comment:
1815 1815 raise JSONRPCError(f'comment `{comment_id}` does not exist')
1816 1816
1817 1817 perms = ('repository.read', 'repository.write', 'repository.admin')
1818 1818 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1819 1819 (user=apiuser, repo_name=comment.repo.repo_name)
1820 1820
1821 1821 if not has_comment_perm:
1822 1822 raise JSONRPCError(f'comment `{comment_id}` does not exist')
1823 1823
1824 1824 return comment
1825 1825
1826 1826
1827 1827 @jsonrpc_method()
1828 1828 def edit_comment(request, apiuser, message, comment_id, version,
1829 1829 userid=Optional(OAttr('apiuser'))):
1830 1830 """
1831 1831 Edit comment on the pull request or commit,
1832 1832 specified by the `comment_id` and version. Initially version should be 0
1833 1833
1834 1834 :param apiuser: This is filled automatically from the |authtoken|.
1835 1835 :type apiuser: AuthUser
1836 1836 :param comment_id: Specify the comment_id for editing
1837 1837 :type comment_id: int
1838 1838 :param version: version of the comment that will be created, starts from 0
1839 1839 :type version: int
1840 1840 :param message: The text content of the comment.
1841 1841 :type message: str
1842 1842 :param userid: Comment on the pull request as this user
1843 1843 :type userid: Optional(str or int)
1844 1844
1845 1845 Example output:
1846 1846
1847 1847 .. code-block:: bash
1848 1848
1849 1849 id : <id_given_in_input>
1850 1850 result : {
1851 1851 "comment": "<comment data>",
1852 1852 "version": "<Integer>",
1853 1853 },
1854 1854 error : null
1855 1855 """
1856 1856
1857 1857 auth_user = apiuser
1858 1858 comment = ChangesetComment.get(comment_id)
1859 1859 if not comment:
1860 1860 raise JSONRPCError(f'comment `{comment_id}` does not exist')
1861 1861
1862 1862 is_super_admin = has_superadmin_permission(apiuser)
1863 1863 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1864 1864 (user=apiuser, repo_name=comment.repo.repo_name)
1865 1865
1866 1866 if not isinstance(userid, Optional):
1867 1867 if is_super_admin or is_repo_admin:
1868 1868 apiuser = get_user_or_error(userid)
1869 1869 auth_user = apiuser.AuthUser()
1870 1870 else:
1871 1871 raise JSONRPCError('userid is not the same as your user')
1872 1872
1873 1873 comment_author = comment.author.user_id == auth_user.user_id
1874 1874
1875 1875 if comment.immutable:
1876 1876 raise JSONRPCError("Immutable comment cannot be edited")
1877 1877
1878 1878 if not (is_super_admin or is_repo_admin or comment_author):
1879 1879 raise JSONRPCError("you don't have access to edit this comment")
1880 1880
1881 1881 try:
1882 1882 comment_history = CommentsModel().edit(
1883 1883 comment_id=comment_id,
1884 1884 text=message,
1885 1885 auth_user=auth_user,
1886 1886 version=version,
1887 1887 )
1888 1888 Session().commit()
1889 1889 except CommentVersionMismatch:
1890 1890 raise JSONRPCError(
1891 1891 f'comment ({comment_id}) version ({version}) mismatch'
1892 1892 )
1893 1893 if not comment_history and not message:
1894 1894 raise JSONRPCError(
1895 1895 f"comment ({comment_id}) can't be changed with empty string"
1896 1896 )
1897 1897
1898 1898 if comment.pull_request:
1899 1899 pull_request = comment.pull_request
1900 1900 PullRequestModel().trigger_pull_request_hook(
1901 1901 pull_request, apiuser, 'comment_edit',
1902 1902 data={'comment': comment})
1903 1903 else:
1904 1904 db_repo = comment.repo
1905 1905 commit_id = comment.revision
1906 1906 commit = db_repo.get_commit(commit_id)
1907 1907 CommentsModel().trigger_commit_comment_hook(
1908 1908 db_repo, apiuser, 'edit',
1909 1909 data={'comment': comment, 'commit': commit})
1910 1910
1911 1911 data = {
1912 1912 'comment': comment,
1913 1913 'version': comment_history.version if comment_history else None,
1914 1914 }
1915 1915 return data
1916 1916
1917 1917
1918 1918 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1919 1919 # @jsonrpc_method()
1920 1920 # def delete_comment(request, apiuser, comment_id):
1921 1921 # auth_user = apiuser
1922 1922 #
1923 1923 # comment = ChangesetComment.get(comment_id)
1924 1924 # if not comment:
1925 1925 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1926 1926 #
1927 1927 # is_super_admin = has_superadmin_permission(apiuser)
1928 1928 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1929 1929 # (user=apiuser, repo_name=comment.repo.repo_name)
1930 1930 #
1931 1931 # comment_author = comment.author.user_id == auth_user.user_id
1932 1932 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1933 1933 # raise JSONRPCError("you don't have access to edit this comment")
1934 1934
1935 1935 @jsonrpc_method()
1936 1936 def grant_user_permission(request, apiuser, repoid, userid, perm):
1937 1937 """
1938 1938 Grant permissions for the specified user on the given repository,
1939 1939 or update existing permissions if found.
1940 1940
1941 1941 This command can only be run using an |authtoken| with admin
1942 1942 permissions on the |repo|.
1943 1943
1944 1944 :param apiuser: This is filled automatically from the |authtoken|.
1945 1945 :type apiuser: AuthUser
1946 1946 :param repoid: Set the repository name or repository ID.
1947 1947 :type repoid: str or int
1948 1948 :param userid: Set the user name.
1949 1949 :type userid: str
1950 1950 :param perm: Set the user permissions, using the following format
1951 1951 ``(repository.(none|read|write|admin))``
1952 1952 :type perm: str
1953 1953
1954 1954 Example output:
1955 1955
1956 1956 .. code-block:: bash
1957 1957
1958 1958 id : <id_given_in_input>
1959 1959 result: {
1960 1960 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1961 1961 "success": true
1962 1962 }
1963 1963 error: null
1964 1964 """
1965 1965
1966 1966 repo = get_repo_or_error(repoid)
1967 1967 user = get_user_or_error(userid)
1968 1968 perm = get_perm_or_error(perm)
1969 1969 if not has_superadmin_permission(apiuser):
1970 1970 _perms = ('repository.admin',)
1971 1971 validate_repo_permissions(apiuser, repoid, repo, _perms)
1972 1972
1973 1973 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1974 1974 try:
1975 1975 changes = RepoModel().update_permissions(
1976 1976 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1977 1977
1978 1978 action_data = {
1979 1979 'added': changes['added'],
1980 1980 'updated': changes['updated'],
1981 1981 'deleted': changes['deleted'],
1982 1982 }
1983 1983 audit_logger.store_api(
1984 1984 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1985 1985 Session().commit()
1986 1986 PermissionModel().flush_user_permission_caches(changes)
1987 1987
1988 1988 return {
1989 1989 'msg': 'Granted perm: `{}` for user: `{}` in repo: `{}`'.format(
1990 1990 perm.permission_name, user.username, repo.repo_name
1991 1991 ),
1992 1992 'success': True
1993 1993 }
1994 1994 except Exception:
1995 1995 log.exception("Exception occurred while trying edit permissions for repo")
1996 1996 raise JSONRPCError(
1997 1997 'failed to edit permission for user: `{}` in repo: `{}`'.format(
1998 1998 userid, repoid
1999 1999 )
2000 2000 )
2001 2001
2002 2002
2003 2003 @jsonrpc_method()
2004 2004 def revoke_user_permission(request, apiuser, repoid, userid):
2005 2005 """
2006 2006 Revoke permission for a user on the specified repository.
2007 2007
2008 2008 This command can only be run using an |authtoken| with admin
2009 2009 permissions on the |repo|.
2010 2010
2011 2011 :param apiuser: This is filled automatically from the |authtoken|.
2012 2012 :type apiuser: AuthUser
2013 2013 :param repoid: Set the repository name or repository ID.
2014 2014 :type repoid: str or int
2015 2015 :param userid: Set the user name of revoked user.
2016 2016 :type userid: str or int
2017 2017
2018 2018 Example error output:
2019 2019
2020 2020 .. code-block:: bash
2021 2021
2022 2022 id : <id_given_in_input>
2023 2023 result: {
2024 2024 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
2025 2025 "success": true
2026 2026 }
2027 2027 error: null
2028 2028 """
2029 2029
2030 2030 repo = get_repo_or_error(repoid)
2031 2031 user = get_user_or_error(userid)
2032 2032 if not has_superadmin_permission(apiuser):
2033 2033 _perms = ('repository.admin',)
2034 2034 validate_repo_permissions(apiuser, repoid, repo, _perms)
2035 2035
2036 2036 perm_deletions = [[user.user_id, None, "user"]]
2037 2037 try:
2038 2038 changes = RepoModel().update_permissions(
2039 2039 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2040 2040
2041 2041 action_data = {
2042 2042 'added': changes['added'],
2043 2043 'updated': changes['updated'],
2044 2044 'deleted': changes['deleted'],
2045 2045 }
2046 2046 audit_logger.store_api(
2047 2047 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2048 2048 Session().commit()
2049 2049 PermissionModel().flush_user_permission_caches(changes)
2050 2050
2051 2051 return {
2052 2052 'msg': 'Revoked perm for user: `{}` in repo: `{}`'.format(
2053 2053 user.username, repo.repo_name
2054 2054 ),
2055 2055 'success': True
2056 2056 }
2057 2057 except Exception:
2058 2058 log.exception("Exception occurred while trying revoke permissions to repo")
2059 2059 raise JSONRPCError(
2060 2060 'failed to edit permission for user: `{}` in repo: `{}`'.format(
2061 2061 userid, repoid
2062 2062 )
2063 2063 )
2064 2064
2065 2065
2066 2066 @jsonrpc_method()
2067 2067 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2068 2068 """
2069 2069 Grant permission for a user group on the specified repository,
2070 2070 or update existing permissions.
2071 2071
2072 2072 This command can only be run using an |authtoken| with admin
2073 2073 permissions on the |repo|.
2074 2074
2075 2075 :param apiuser: This is filled automatically from the |authtoken|.
2076 2076 :type apiuser: AuthUser
2077 2077 :param repoid: Set the repository name or repository ID.
2078 2078 :type repoid: str or int
2079 2079 :param usergroupid: Specify the ID of the user group.
2080 2080 :type usergroupid: str or int
2081 2081 :param perm: Set the user group permissions using the following
2082 2082 format: (repository.(none|read|write|admin))
2083 2083 :type perm: str
2084 2084
2085 2085 Example output:
2086 2086
2087 2087 .. code-block:: bash
2088 2088
2089 2089 id : <id_given_in_input>
2090 2090 result : {
2091 2091 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2092 2092 "success": true
2093 2093
2094 2094 }
2095 2095 error : null
2096 2096
2097 2097 Example error output:
2098 2098
2099 2099 .. code-block:: bash
2100 2100
2101 2101 id : <id_given_in_input>
2102 2102 result : null
2103 2103 error : {
2104 2104 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2105 2105 }
2106 2106
2107 2107 """
2108 2108
2109 2109 repo = get_repo_or_error(repoid)
2110 2110 perm = get_perm_or_error(perm)
2111 2111 if not has_superadmin_permission(apiuser):
2112 2112 _perms = ('repository.admin',)
2113 2113 validate_repo_permissions(apiuser, repoid, repo, _perms)
2114 2114
2115 2115 user_group = get_user_group_or_error(usergroupid)
2116 2116 if not has_superadmin_permission(apiuser):
2117 2117 # check if we have at least read permission for this user group !
2118 2118 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2119 2119 if not HasUserGroupPermissionAnyApi(*_perms)(
2120 2120 user=apiuser, user_group_name=user_group.users_group_name):
2121 2121 raise JSONRPCError(
2122 2122 f'user group `{usergroupid}` does not exist')
2123 2123
2124 2124 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2125 2125 try:
2126 2126 changes = RepoModel().update_permissions(
2127 2127 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2128 2128 action_data = {
2129 2129 'added': changes['added'],
2130 2130 'updated': changes['updated'],
2131 2131 'deleted': changes['deleted'],
2132 2132 }
2133 2133 audit_logger.store_api(
2134 2134 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2135 2135 Session().commit()
2136 2136 PermissionModel().flush_user_permission_caches(changes)
2137 2137
2138 2138 return {
2139 2139 'msg': 'Granted perm: `%s` for user group: `%s` in '
2140 2140 'repo: `%s`' % (
2141 2141 perm.permission_name, user_group.users_group_name,
2142 2142 repo.repo_name
2143 2143 ),
2144 2144 'success': True
2145 2145 }
2146 2146 except Exception:
2147 2147 log.exception(
2148 2148 "Exception occurred while trying change permission on repo")
2149 2149 raise JSONRPCError(
2150 2150 'failed to edit permission for user group: `%s` in '
2151 2151 'repo: `%s`' % (
2152 2152 usergroupid, repo.repo_name
2153 2153 )
2154 2154 )
2155 2155
2156 2156
2157 2157 @jsonrpc_method()
2158 2158 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2159 2159 """
2160 2160 Revoke the permissions of a user group on a given repository.
2161 2161
2162 2162 This command can only be run using an |authtoken| with admin
2163 2163 permissions on the |repo|.
2164 2164
2165 2165 :param apiuser: This is filled automatically from the |authtoken|.
2166 2166 :type apiuser: AuthUser
2167 2167 :param repoid: Set the repository name or repository ID.
2168 2168 :type repoid: str or int
2169 2169 :param usergroupid: Specify the user group ID.
2170 2170 :type usergroupid: str or int
2171 2171
2172 2172 Example output:
2173 2173
2174 2174 .. code-block:: bash
2175 2175
2176 2176 id : <id_given_in_input>
2177 2177 result: {
2178 2178 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2179 2179 "success": true
2180 2180 }
2181 2181 error: null
2182 2182 """
2183 2183
2184 2184 repo = get_repo_or_error(repoid)
2185 2185 if not has_superadmin_permission(apiuser):
2186 2186 _perms = ('repository.admin',)
2187 2187 validate_repo_permissions(apiuser, repoid, repo, _perms)
2188 2188
2189 2189 user_group = get_user_group_or_error(usergroupid)
2190 2190 if not has_superadmin_permission(apiuser):
2191 2191 # check if we have at least read permission for this user group !
2192 2192 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2193 2193 if not HasUserGroupPermissionAnyApi(*_perms)(
2194 2194 user=apiuser, user_group_name=user_group.users_group_name):
2195 2195 raise JSONRPCError(
2196 2196 f'user group `{usergroupid}` does not exist')
2197 2197
2198 2198 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2199 2199 try:
2200 2200 changes = RepoModel().update_permissions(
2201 2201 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2202 2202 action_data = {
2203 2203 'added': changes['added'],
2204 2204 'updated': changes['updated'],
2205 2205 'deleted': changes['deleted'],
2206 2206 }
2207 2207 audit_logger.store_api(
2208 2208 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2209 2209 Session().commit()
2210 2210 PermissionModel().flush_user_permission_caches(changes)
2211 2211
2212 2212 return {
2213 2213 'msg': 'Revoked perm for user group: `{}` in repo: `{}`'.format(
2214 2214 user_group.users_group_name, repo.repo_name
2215 2215 ),
2216 2216 'success': True
2217 2217 }
2218 2218 except Exception:
2219 2219 log.exception("Exception occurred while trying revoke "
2220 2220 "user group permission on repo")
2221 2221 raise JSONRPCError(
2222 2222 'failed to edit permission for user group: `%s` in '
2223 2223 'repo: `%s`' % (
2224 2224 user_group.users_group_name, repo.repo_name
2225 2225 )
2226 2226 )
2227 2227
2228 2228
2229 2229 @jsonrpc_method()
2230 2230 def pull(request, apiuser, repoid, remote_uri=Optional(None), sync_large_objects=Optional(False)):
2231 2231 """
2232 2232 Triggers a pull on the given repository from a remote location. You
2233 2233 can use this to keep remote repositories up-to-date.
2234 2234
2235 2235 This command can only be run using an |authtoken| with admin
2236 2236 rights to the specified repository. For more information,
2237 2237 see :ref:`config-token-ref`.
2238 2238
2239 2239 This command takes the following options:
2240 2240
2241 2241 :param apiuser: This is filled automatically from the |authtoken|.
2242 2242 :type apiuser: AuthUser
2243 2243 :param repoid: The repository name or repository ID.
2244 2244 :type repoid: str or int
2245 2245 :param remote_uri: Optional remote URI to pass in for pull
2246 2246 :type remote_uri: str
2247 2247 :param sync_large_objects: Optional flag for pulling LFS objects.
2248 2248 :type sync_large_objects: bool
2249 2249
2250 2250 Example output:
2251 2251
2252 2252 .. code-block:: bash
2253 2253
2254 2254 id : <id_given_in_input>
2255 2255 result : {
2256 2256 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2257 2257 "repository": "<repository name>"
2258 2258 }
2259 2259 error : null
2260 2260
2261 2261 Example error output:
2262 2262
2263 2263 .. code-block:: bash
2264 2264
2265 2265 id : <id_given_in_input>
2266 2266 result : null
2267 2267 error : {
2268 2268 "Unable to push changes from `<remote_url>`"
2269 2269 }
2270 2270
2271 2271 """
2272 2272
2273 2273 repo = get_repo_or_error(repoid)
2274 2274 remote_uri = Optional.extract(remote_uri)
2275 2275 remote_uri_display = remote_uri or repo.clone_uri_hidden
2276 sync_large_objects = Optional.extract(sync_large_objects)
2276 2277 if not has_superadmin_permission(apiuser):
2277 2278 _perms = ('repository.admin',)
2278 2279 validate_repo_permissions(apiuser, repoid, repo, _perms)
2279 2280
2280 2281 try:
2281 2282 ScmModel().pull_changes(
2282 2283 repo.repo_name, apiuser.username, remote_uri=remote_uri, sync_large_objects=sync_large_objects)
2283 2284 return {
2284 2285 'msg': 'Pulled from url `{}` on repo `{}`'.format(
2285 2286 remote_uri_display, repo.repo_name),
2286 2287 'repository': repo.repo_name
2287 2288 }
2288 2289 except Exception:
2289 2290 log.exception("Exception occurred while trying to "
2290 2291 "pull changes from remote location")
2291 2292 raise JSONRPCError(
2292 2293 'Unable to pull changes from `%s`' % remote_uri_display
2293 2294 )
2294 2295
2295 2296
2296 2297 @jsonrpc_method()
2297 2298 def strip(request, apiuser, repoid, revision, branch):
2298 2299 """
2299 2300 Strips the given revision from the specified repository.
2300 2301
2301 2302 * This will remove the revision and all of its decendants.
2302 2303
2303 2304 This command can only be run using an |authtoken| with admin rights to
2304 2305 the specified repository.
2305 2306
2306 2307 This command takes the following options:
2307 2308
2308 2309 :param apiuser: This is filled automatically from the |authtoken|.
2309 2310 :type apiuser: AuthUser
2310 2311 :param repoid: The repository name or repository ID.
2311 2312 :type repoid: str or int
2312 2313 :param revision: The revision you wish to strip.
2313 2314 :type revision: str
2314 2315 :param branch: The branch from which to strip the revision.
2315 2316 :type branch: str
2316 2317
2317 2318 Example output:
2318 2319
2319 2320 .. code-block:: bash
2320 2321
2321 2322 id : <id_given_in_input>
2322 2323 result : {
2323 2324 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2324 2325 "repository": "<repository name>"
2325 2326 }
2326 2327 error : null
2327 2328
2328 2329 Example error output:
2329 2330
2330 2331 .. code-block:: bash
2331 2332
2332 2333 id : <id_given_in_input>
2333 2334 result : null
2334 2335 error : {
2335 2336 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2336 2337 }
2337 2338
2338 2339 """
2339 2340
2340 2341 repo = get_repo_or_error(repoid)
2341 2342 if not has_superadmin_permission(apiuser):
2342 2343 _perms = ('repository.admin',)
2343 2344 validate_repo_permissions(apiuser, repoid, repo, _perms)
2344 2345
2345 2346 try:
2346 2347 ScmModel().strip(repo, revision, branch)
2347 2348 audit_logger.store_api(
2348 2349 'repo.commit.strip', action_data={'commit_id': revision},
2349 2350 repo=repo,
2350 2351 user=apiuser, commit=True)
2351 2352
2352 2353 return {
2353 2354 'msg': 'Stripped commit {} from repo `{}`'.format(
2354 2355 revision, repo.repo_name),
2355 2356 'repository': repo.repo_name
2356 2357 }
2357 2358 except Exception:
2358 2359 log.exception("Exception while trying to strip")
2359 2360 raise JSONRPCError(
2360 2361 'Unable to strip commit {} from repo `{}`'.format(
2361 2362 revision, repo.repo_name)
2362 2363 )
2363 2364
2364 2365
2365 2366 @jsonrpc_method()
2366 2367 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2367 2368 """
2368 2369 Returns all settings for a repository. If key is given it only returns the
2369 2370 setting identified by the key or null.
2370 2371
2371 2372 :param apiuser: This is filled automatically from the |authtoken|.
2372 2373 :type apiuser: AuthUser
2373 2374 :param repoid: The repository name or repository id.
2374 2375 :type repoid: str or int
2375 2376 :param key: Key of the setting to return.
2376 2377 :type: key: Optional(str)
2377 2378
2378 2379 Example output:
2379 2380
2380 2381 .. code-block:: bash
2381 2382
2382 2383 {
2383 2384 "error": null,
2384 2385 "id": 237,
2385 2386 "result": {
2386 2387 "extensions_largefiles": true,
2387 2388 "extensions_evolve": true,
2388 2389 "hooks_changegroup_push_logger": true,
2389 2390 "hooks_changegroup_repo_size": false,
2390 2391 "hooks_outgoing_pull_logger": true,
2391 2392 "phases_publish": "True",
2392 2393 "rhodecode_hg_use_rebase_for_merging": true,
2393 2394 "rhodecode_pr_merge_enabled": true,
2394 2395 "rhodecode_use_outdated_comments": true
2395 2396 }
2396 2397 }
2397 2398 """
2398 2399
2399 2400 # Restrict access to this api method to super-admins, and repo admins only.
2400 2401 repo = get_repo_or_error(repoid)
2401 2402 if not has_superadmin_permission(apiuser):
2402 2403 _perms = ('repository.admin',)
2403 2404 validate_repo_permissions(apiuser, repoid, repo, _perms)
2404 2405
2405 2406 try:
2406 2407 settings_model = VcsSettingsModel(repo=repo)
2407 2408 settings = settings_model.get_global_settings()
2408 2409 settings.update(settings_model.get_repo_settings())
2409 2410
2410 2411 # If only a single setting is requested fetch it from all settings.
2411 2412 key = Optional.extract(key)
2412 2413 if key is not None:
2413 2414 settings = settings.get(key, None)
2414 2415 except Exception:
2415 2416 msg = f'Failed to fetch settings for repository `{repoid}`'
2416 2417 log.exception(msg)
2417 2418 raise JSONRPCError(msg)
2418 2419
2419 2420 return settings
2420 2421
2421 2422
2422 2423 @jsonrpc_method()
2423 2424 def set_repo_settings(request, apiuser, repoid, settings):
2424 2425 """
2425 2426 Update repository settings. Returns true on success.
2426 2427
2427 2428 :param apiuser: This is filled automatically from the |authtoken|.
2428 2429 :type apiuser: AuthUser
2429 2430 :param repoid: The repository name or repository id.
2430 2431 :type repoid: str or int
2431 2432 :param settings: The new settings for the repository.
2432 2433 :type: settings: dict
2433 2434
2434 2435 Example output:
2435 2436
2436 2437 .. code-block:: bash
2437 2438
2438 2439 {
2439 2440 "error": null,
2440 2441 "id": 237,
2441 2442 "result": true
2442 2443 }
2443 2444 """
2444 2445 # Restrict access to this api method to super-admins, and repo admins only.
2445 2446 repo = get_repo_or_error(repoid)
2446 2447 if not has_superadmin_permission(apiuser):
2447 2448 _perms = ('repository.admin',)
2448 2449 validate_repo_permissions(apiuser, repoid, repo, _perms)
2449 2450
2450 2451 if type(settings) is not dict:
2451 2452 raise JSONRPCError('Settings have to be a JSON Object.')
2452 2453
2453 2454 try:
2454 2455 settings_model = VcsSettingsModel(repo=repoid)
2455 2456
2456 2457 # Merge global, repo and incoming settings.
2457 2458 new_settings = settings_model.get_global_settings()
2458 2459 new_settings.update(settings_model.get_repo_settings())
2459 2460 new_settings.update(settings)
2460 2461
2461 2462 # Update the settings.
2462 2463 inherit_global_settings = new_settings.get(
2463 2464 'inherit_global_settings', False)
2464 2465 settings_model.create_or_update_repo_settings(
2465 2466 new_settings, inherit_global_settings=inherit_global_settings)
2466 2467 Session().commit()
2467 2468 except Exception:
2468 2469 msg = f'Failed to update settings for repository `{repoid}`'
2469 2470 log.exception(msg)
2470 2471 raise JSONRPCError(msg)
2471 2472
2472 2473 # Indicate success.
2473 2474 return True
2474 2475
2475 2476
2476 2477 @jsonrpc_method()
2477 2478 def maintenance(request, apiuser, repoid):
2478 2479 """
2479 2480 Triggers a maintenance on the given repository.
2480 2481
2481 2482 This command can only be run using an |authtoken| with admin
2482 2483 rights to the specified repository. For more information,
2483 2484 see :ref:`config-token-ref`.
2484 2485
2485 2486 This command takes the following options:
2486 2487
2487 2488 :param apiuser: This is filled automatically from the |authtoken|.
2488 2489 :type apiuser: AuthUser
2489 2490 :param repoid: The repository name or repository ID.
2490 2491 :type repoid: str or int
2491 2492
2492 2493 Example output:
2493 2494
2494 2495 .. code-block:: bash
2495 2496
2496 2497 id : <id_given_in_input>
2497 2498 result : {
2498 2499 "msg": "executed maintenance command",
2499 2500 "executed_actions": [
2500 2501 <action_message>, <action_message2>...
2501 2502 ],
2502 2503 "repository": "<repository name>"
2503 2504 }
2504 2505 error : null
2505 2506
2506 2507 Example error output:
2507 2508
2508 2509 .. code-block:: bash
2509 2510
2510 2511 id : <id_given_in_input>
2511 2512 result : null
2512 2513 error : {
2513 2514 "Unable to execute maintenance on `<reponame>`"
2514 2515 }
2515 2516
2516 2517 """
2517 2518
2518 2519 repo = get_repo_or_error(repoid)
2519 2520 if not has_superadmin_permission(apiuser):
2520 2521 _perms = ('repository.admin',)
2521 2522 validate_repo_permissions(apiuser, repoid, repo, _perms)
2522 2523
2523 2524 try:
2524 2525 maintenance = repo_maintenance.RepoMaintenance()
2525 2526 executed_actions = maintenance.execute(repo)
2526 2527
2527 2528 return {
2528 2529 'msg': 'executed maintenance command',
2529 2530 'executed_actions': executed_actions,
2530 2531 'repository': repo.repo_name
2531 2532 }
2532 2533 except Exception:
2533 2534 log.exception("Exception occurred while trying to run maintenance")
2534 2535 raise JSONRPCError(
2535 2536 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,1304 +1,1307 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import datetime
21 21 import mock
22 22 import os
23 23 import sys
24 24 import shutil
25 25
26 26 import pytest
27 27
28 28 from rhodecode.lib.utils import make_db_config
29 29 from rhodecode.lib.vcs.backends.base import Reference
30 30 from rhodecode.lib.vcs.backends.git import (
31 31 GitRepository, GitCommit, discover_git_version)
32 32 from rhodecode.lib.vcs.exceptions import (
33 33 RepositoryError, VCSError, NodeDoesNotExistError)
34 34 from rhodecode.lib.vcs.nodes import (
35 35 NodeKind, FileNode, DirNode, NodeState, SubModuleNode)
36 36 from rhodecode.tests import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, get_new_dir
37 37 from rhodecode.tests.vcs.conftest import BackendTestMixin
38 38
39 39
40 40 pytestmark = pytest.mark.backends("git")
41 41
42 42
43 43 DIFF_FROM_REMOTE = br"""diff --git a/foobar b/foobar
44 44 new file mode 100644
45 45 index 0000000..f6ea049
46 46 --- /dev/null
47 47 +++ b/foobar
48 48 @@ -0,0 +1 @@
49 49 +foobar
50 50 \ No newline at end of file
51 51 diff --git a/foobar2 b/foobar2
52 52 new file mode 100644
53 53 index 0000000..e8c9d6b
54 54 --- /dev/null
55 55 +++ b/foobar2
56 56 @@ -0,0 +1 @@
57 57 +foobar2
58 58 \ No newline at end of file
59 59 """
60 60
61 61
62 62 def callable_get_diff(*args, **kwargs):
63 63 return DIFF_FROM_REMOTE
64 64
65 65
66 66 class TestGitRepository(object):
67 67
68 68 @pytest.fixture(autouse=True)
69 69 def prepare(self, request, baseapp):
70 70 self.repo = GitRepository(TEST_GIT_REPO, bare=True)
71 71 self.repo.count()
72 72
73 73 def get_clone_repo(self, tmpdir):
74 74 """
75 75 Return a non bare clone of the base repo.
76 76 """
77 77 clone_path = str(tmpdir.join('clone-repo'))
78 78 repo_clone = GitRepository(
79 79 clone_path, create=True, src_url=self.repo.path, bare=False)
80 80
81 81 return repo_clone
82 82
83 83 def get_empty_repo(self, tmpdir, bare=False):
84 84 """
85 85 Return a non bare empty repo.
86 86 """
87 87 clone_path = str(tmpdir.join('empty-repo'))
88 88 return GitRepository(clone_path, create=True, bare=bare)
89 89
90 90 def test_wrong_repo_path(self):
91 91 wrong_repo_path = '/tmp/errorrepo_git'
92 92 with pytest.raises(RepositoryError):
93 93 GitRepository(wrong_repo_path)
94 94
95 95 def test_repo_clone(self, tmp_path_factory):
96 96 repo = GitRepository(TEST_GIT_REPO)
97 97 clone_path = '{}_{}'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE)
98 98 repo_clone = GitRepository(
99 99 clone_path,
100 100 src_url=TEST_GIT_REPO, create=True, do_workspace_checkout=True)
101 101
102 102 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
103 103 # Checking hashes of commits should be enough
104 104 for commit in repo.get_commits():
105 105 raw_id = commit.raw_id
106 106 assert raw_id == repo_clone.get_commit(raw_id).raw_id
107 107
108 108 def test_repo_clone_without_create(self):
109 109 with pytest.raises(RepositoryError):
110 110 GitRepository(
111 111 TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
112 112
113 113 def test_repo_clone_with_update(self, tmp_path_factory):
114 114 repo = GitRepository(TEST_GIT_REPO)
115 115 clone_path = '{}_{}_update'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE)
116 116
117 117 repo_clone = GitRepository(
118 118 clone_path,
119 119 create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=True)
120 120 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
121 121
122 122 # check if current workdir was updated
123 123 fpath = os.path.join(clone_path, 'MANIFEST.in')
124 124 assert os.path.isfile(fpath)
125 125
126 126 def test_repo_clone_without_update(self, tmp_path_factory):
127 127 repo = GitRepository(TEST_GIT_REPO)
128 128 clone_path = '{}_{}_without_update'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE)
129 129 repo_clone = GitRepository(
130 130 clone_path,
131 131 create=True, src_url=TEST_GIT_REPO, do_workspace_checkout=False)
132 132 assert len(repo.commit_ids) == len(repo_clone.commit_ids)
133 133 # check if current workdir was *NOT* updated
134 134 fpath = os.path.join(clone_path, 'MANIFEST.in')
135 135 # Make sure it's not bare repo
136 136 assert not repo_clone.bare
137 137 assert not os.path.isfile(fpath)
138 138
139 139 def test_repo_clone_into_bare_repo(self, tmp_path_factory):
140 140 repo = GitRepository(TEST_GIT_REPO)
141 141 clone_path = '{}_{}_bare.git'.format(tmp_path_factory.mktemp('_'), TEST_GIT_REPO_CLONE)
142 142 repo_clone = GitRepository(
143 143 clone_path, create=True, src_url=repo.path, bare=True)
144 144 assert repo_clone.bare
145 145
146 146 def test_create_repo_is_not_bare_by_default(self):
147 147 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
148 148 assert not repo.bare
149 149
150 150 def test_create_bare_repo(self):
151 151 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
152 152 assert repo.bare
153 153
154 154 def test_update_server_info(self):
155 155 self.repo._update_server_info()
156 156
157 157 def test_fetch(self, vcsbackend_git):
158 158 # Note: This is a git specific part of the API, it's only implemented
159 159 # by the git backend.
160 160 source_repo = vcsbackend_git.repo
161 161 target_repo = vcsbackend_git.create_repo(bare=True)
162 162 target_repo.fetch(source_repo.path)
163 163 # Note: Get a fresh instance, avoids caching trouble
164 164 target_repo = vcsbackend_git.backend(target_repo.path)
165 165 assert len(source_repo.commit_ids) == len(target_repo.commit_ids)
166 166
167 167 def test_commit_ids(self):
168 168 # there are 112 commits (by now)
169 169 # so we can assume they would be available from now on
170 170 subset = {'c1214f7e79e02fc37156ff215cd71275450cffc3',
171 171 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
172 172 'fa6600f6848800641328adbf7811fd2372c02ab2',
173 173 '102607b09cdd60e2793929c4f90478be29f85a17',
174 174 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
175 175 '2d1028c054665b962fa3d307adfc923ddd528038',
176 176 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
177 177 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
178 178 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
179 179 '8430a588b43b5d6da365400117c89400326e7992',
180 180 'd955cd312c17b02143c04fa1099a352b04368118',
181 181 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
182 182 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
183 183 'f298fe1189f1b69779a4423f40b48edf92a703fc',
184 184 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
185 185 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
186 186 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
187 187 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
188 188 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
189 189 '45223f8f114c64bf4d6f853e3c35a369a6305520',
190 190 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
191 191 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
192 192 '27d48942240f5b91dfda77accd2caac94708cc7d',
193 193 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
194 194 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'}
195 195 assert subset.issubset(set(self.repo.commit_ids))
196 196
197 197 def test_slicing(self):
198 198 # 4 1 5 10 95
199 199 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
200 200 (10, 20, 10), (5, 100, 95)]:
201 201 commit_ids = list(self.repo[sfrom:sto])
202 202 assert len(commit_ids) == size
203 203 assert commit_ids[0] == self.repo.get_commit(commit_idx=sfrom)
204 204 assert commit_ids[-1] == self.repo.get_commit(commit_idx=sto - 1)
205 205
206 206 def test_branches(self):
207 207 # TODO: Need more tests here
208 208 # Removed (those are 'remotes' branches for cloned repo)
209 209 # assert 'master' in self.repo.branches
210 210 # assert 'gittree' in self.repo.branches
211 211 # assert 'web-branch' in self.repo.branches
212 212 for __, commit_id in self.repo.branches.items():
213 213 assert isinstance(self.repo.get_commit(commit_id), GitCommit)
214 214
215 215 def test_tags(self):
216 216 # TODO: Need more tests here
217 217 assert 'v0.1.1' in self.repo.tags
218 218 assert 'v0.1.2' in self.repo.tags
219 219 for __, commit_id in self.repo.tags.items():
220 220 assert isinstance(self.repo.get_commit(commit_id), GitCommit)
221 221
222 222 def _test_single_commit_cache(self, commit_id):
223 223 commit = self.repo.get_commit(commit_id)
224 224 assert commit_id in self.repo.commits
225 225 assert commit is self.repo.commits[commit_id]
226 226
227 227 def test_initial_commit(self):
228 228 commit_id = self.repo.commit_ids[0]
229 229 init_commit = self.repo.get_commit(commit_id)
230 230 init_author = init_commit.author
231 231
232 232 assert init_commit.message == 'initial import\n'
233 233 assert init_author == 'Marcin Kuzminski <marcin@python-blog.com>'
234 234 assert init_author == init_commit.committer
235 235 for path in ('vcs/__init__.py',
236 236 'vcs/backends/BaseRepository.py',
237 237 'vcs/backends/__init__.py'):
238 238 assert isinstance(init_commit.get_node(path), FileNode)
239 239 for path in ('', 'vcs', 'vcs/backends'):
240 240 assert isinstance(init_commit.get_node(path), DirNode)
241 241
242 242 with pytest.raises(NodeDoesNotExistError):
243 243 init_commit.get_node(path='foobar')
244 244
245 245 node = init_commit.get_node('vcs/')
246 246 assert hasattr(node, 'kind')
247 247 assert node.kind == NodeKind.DIR
248 248
249 249 node = init_commit.get_node('vcs')
250 250 assert hasattr(node, 'kind')
251 251 assert node.kind == NodeKind.DIR
252 252
253 253 node = init_commit.get_node('vcs/__init__.py')
254 254 assert hasattr(node, 'kind')
255 255 assert node.kind == NodeKind.FILE
256 256
257 257 def test_not_existing_commit(self):
258 258 with pytest.raises(RepositoryError):
259 259 self.repo.get_commit('f' * 40)
260 260
261 261 def test_commit10(self):
262 262
263 263 commit10 = self.repo.get_commit(self.repo.commit_ids[9])
264 264 README = """===
265 265 VCS
266 266 ===
267 267
268 268 Various Version Control System management abstraction layer for Python.
269 269
270 270 Introduction
271 271 ------------
272 272
273 273 TODO: To be written...
274 274
275 275 """
276 276 node = commit10.get_node('README.rst')
277 277 assert node.kind == NodeKind.FILE
278 278 assert node.str_content == README
279 279
280 280 def test_head(self):
281 281 assert self.repo.head == self.repo.get_commit().raw_id
282 282
283 283 def test_checkout_with_create(self, tmpdir):
284 284 repo_clone = self.get_clone_repo(tmpdir)
285 285
286 286 new_branch = 'new_branch'
287 287 assert repo_clone._current_branch() == 'master'
288 288 assert set(repo_clone.branches) == {'master'}
289 289 repo_clone._checkout(new_branch, create=True)
290 290
291 291 # Branches is a lazy property so we need to recrete the Repo object.
292 292 repo_clone = GitRepository(repo_clone.path)
293 293 assert set(repo_clone.branches) == {'master', new_branch}
294 294 assert repo_clone._current_branch() == new_branch
295 295
296 296 def test_checkout(self, tmpdir):
297 297 repo_clone = self.get_clone_repo(tmpdir)
298 298
299 299 repo_clone._checkout('new_branch', create=True)
300 300 repo_clone._checkout('master')
301 301
302 302 assert repo_clone._current_branch() == 'master'
303 303
304 304 def test_checkout_same_branch(self, tmpdir):
305 305 repo_clone = self.get_clone_repo(tmpdir)
306 306
307 307 repo_clone._checkout('master')
308 308 assert repo_clone._current_branch() == 'master'
309 309
310 310 def test_checkout_branch_already_exists(self, tmpdir):
311 311 repo_clone = self.get_clone_repo(tmpdir)
312 312
313 313 with pytest.raises(RepositoryError):
314 314 repo_clone._checkout('master', create=True)
315 315
316 316 def test_checkout_bare_repo(self):
317 317 with pytest.raises(RepositoryError):
318 318 self.repo._checkout('master')
319 319
320 320 def test_current_branch_bare_repo(self):
321 321 with pytest.raises(RepositoryError):
322 322 self.repo._current_branch()
323 323
324 324 def test_current_branch_empty_repo(self, tmpdir):
325 325 repo = self.get_empty_repo(tmpdir)
326 326 assert repo._current_branch() is None
327 327
328 328 def test_local_clone(self, tmp_path_factory):
329 329 clone_path = str(tmp_path_factory.mktemp('test-local-clone'))
330 330 self.repo._local_clone(clone_path, 'master')
331 331 repo_clone = GitRepository(clone_path)
332 332
333 333 assert self.repo.commit_ids == repo_clone.commit_ids
334 334
335 335 def test_local_clone_with_specific_branch(self, tmpdir):
336 336 source_repo = self.get_clone_repo(tmpdir)
337 337
338 338 # Create a new branch in source repo
339 339 new_branch_commit = source_repo.commit_ids[-3]
340 340 source_repo._checkout(new_branch_commit)
341 341 source_repo._checkout('new_branch', create=True)
342 342
343 343 clone_path = str(tmpdir.join('git-clone-path-1'))
344 344 source_repo._local_clone(clone_path, 'new_branch')
345 345 repo_clone = GitRepository(clone_path)
346 346
347 347 assert source_repo.commit_ids[:-3 + 1] == repo_clone.commit_ids
348 348
349 349 clone_path = str(tmpdir.join('git-clone-path-2'))
350 350 source_repo._local_clone(clone_path, 'master')
351 351 repo_clone = GitRepository(clone_path)
352 352
353 353 assert source_repo.commit_ids == repo_clone.commit_ids
354 354
355 355 def test_local_clone_fails_if_target_exists(self):
356 356 with pytest.raises(RepositoryError):
357 357 self.repo._local_clone(self.repo.path, 'master')
358 358
359 359 def test_local_fetch(self, tmpdir):
360 360 target_repo = self.get_empty_repo(tmpdir)
361 361 source_repo = self.get_clone_repo(tmpdir)
362 362
363 363 # Create a new branch in source repo
364 364 master_commit = source_repo.commit_ids[-1]
365 365 new_branch_commit = source_repo.commit_ids[-3]
366 366 source_repo._checkout(new_branch_commit)
367 367 source_repo._checkout('new_branch', create=True)
368 368
369 369 target_repo._local_fetch(source_repo.path, 'new_branch')
370 370 assert target_repo._last_fetch_heads() == [new_branch_commit]
371 371
372 372 target_repo._local_fetch(source_repo.path, 'master')
373 373 assert target_repo._last_fetch_heads() == [master_commit]
374 374
375 375 def test_local_fetch_from_bare_repo(self, tmpdir):
376 376 target_repo = self.get_empty_repo(tmpdir)
377 377 target_repo._local_fetch(self.repo.path, 'master')
378 378
379 379 master_commit = self.repo.commit_ids[-1]
380 380 assert target_repo._last_fetch_heads() == [master_commit]
381 381
382 382 def test_local_fetch_from_same_repo(self):
383 383 with pytest.raises(ValueError):
384 384 self.repo._local_fetch(self.repo.path, 'master')
385 385
386 386 def test_local_fetch_branch_does_not_exist(self, tmpdir):
387 387 target_repo = self.get_empty_repo(tmpdir)
388 388
389 389 with pytest.raises(RepositoryError):
390 390 target_repo._local_fetch(self.repo.path, 'new_branch')
391 391
392 392 def test_local_pull(self, tmpdir):
393 393 target_repo = self.get_empty_repo(tmpdir)
394 394 source_repo = self.get_clone_repo(tmpdir)
395 395
396 396 # Create a new branch in source repo
397 397 master_commit = source_repo.commit_ids[-1]
398 398 new_branch_commit = source_repo.commit_ids[-3]
399 399 source_repo._checkout(new_branch_commit)
400 400 source_repo._checkout('new_branch', create=True)
401 401
402 402 target_repo._local_pull(source_repo.path, 'new_branch')
403 403 target_repo = GitRepository(target_repo.path)
404 404 assert target_repo.head == new_branch_commit
405 405
406 406 target_repo._local_pull(source_repo.path, 'master')
407 407 target_repo = GitRepository(target_repo.path)
408 408 assert target_repo.head == master_commit
409 409
410 410 def test_local_pull_in_bare_repo(self):
411 411 with pytest.raises(RepositoryError):
412 412 self.repo._local_pull(self.repo.path, 'master')
413 413
414 414 def test_local_merge(self, tmpdir):
415 415 target_repo = self.get_empty_repo(tmpdir)
416 416 source_repo = self.get_clone_repo(tmpdir)
417 417
418 418 # Create a new branch in source repo
419 419 master_commit = source_repo.commit_ids[-1]
420 420 new_branch_commit = source_repo.commit_ids[-3]
421 421 source_repo._checkout(new_branch_commit)
422 422 source_repo._checkout('new_branch', create=True)
423 423
424 424 # This is required as one cannot do a -ff-only merge in an empty repo.
425 425 target_repo._local_pull(source_repo.path, 'new_branch')
426 426
427 427 target_repo._local_fetch(source_repo.path, 'master')
428 428 merge_message = 'Merge message\n\nDescription:...'
429 429 user_name = 'Albert Einstein'
430 430 user_email = 'albert@einstein.com'
431 431 target_repo._local_merge(merge_message, user_name, user_email,
432 432 target_repo._last_fetch_heads())
433 433
434 434 target_repo = GitRepository(target_repo.path)
435 435 assert target_repo.commit_ids[-2] == master_commit
436 436 last_commit = target_repo.get_commit(target_repo.head)
437 437 assert last_commit.message.strip() == merge_message
438 438 assert last_commit.author == '%s <%s>' % (user_name, user_email)
439 439
440 440 assert not os.path.exists(
441 441 os.path.join(target_repo.path, '.git', 'MERGE_HEAD'))
442 442
443 443 def test_local_merge_raises_exception_on_conflict(self, vcsbackend_git):
444 444 target_repo = vcsbackend_git.create_repo(number_of_commits=1)
445 445 vcsbackend_git.ensure_file(b'README', b'I will conflict with you!!!')
446 446
447 447 target_repo._local_fetch(self.repo.path, 'master')
448 448 with pytest.raises(RepositoryError):
449 449 target_repo._local_merge(
450 450 'merge_message', 'user name', 'user@name.com',
451 451 target_repo._last_fetch_heads())
452 452
453 453 # Check we are not left in an intermediate merge state
454 454 assert not os.path.exists(
455 455 os.path.join(target_repo.path, '.git', 'MERGE_HEAD'))
456 456
457 457 def test_local_merge_into_empty_repo(self, tmpdir):
458 458 target_repo = self.get_empty_repo(tmpdir)
459 459
460 460 # This is required as one cannot do a -ff-only merge in an empty repo.
461 461 target_repo._local_fetch(self.repo.path, 'master')
462 462 with pytest.raises(RepositoryError):
463 463 target_repo._local_merge(
464 464 'merge_message', 'user name', 'user@name.com',
465 465 target_repo._last_fetch_heads())
466 466
467 467 def test_local_merge_in_bare_repo(self):
468 468 with pytest.raises(RepositoryError):
469 469 self.repo._local_merge(
470 470 'merge_message', 'user name', 'user@name.com', None)
471 471
472 472 def test_local_push_non_bare(self, tmpdir):
473 473 target_repo = self.get_empty_repo(tmpdir)
474 474
475 475 pushed_branch = 'pushed_branch'
476 476 self.repo._local_push('master', target_repo.path, pushed_branch)
477 477 # Fix the HEAD of the target repo, or otherwise GitRepository won't
478 478 # report any branches.
479 479 with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f:
480 480 f.write('ref: refs/heads/%s' % pushed_branch)
481 481
482 482 target_repo = GitRepository(target_repo.path)
483 483
484 484 assert (target_repo.branches[pushed_branch] ==
485 485 self.repo.branches['master'])
486 486
487 487 def test_local_push_bare(self, tmpdir):
488 488 target_repo = self.get_empty_repo(tmpdir, bare=True)
489 489
490 490 pushed_branch = 'pushed_branch'
491 491 self.repo._local_push('master', target_repo.path, pushed_branch)
492 492 # Fix the HEAD of the target repo, or otherwise GitRepository won't
493 493 # report any branches.
494 494 with open(os.path.join(target_repo.path, 'HEAD'), 'w') as f:
495 495 f.write('ref: refs/heads/%s' % pushed_branch)
496 496
497 497 target_repo = GitRepository(target_repo.path)
498 498
499 499 assert (target_repo.branches[pushed_branch] ==
500 500 self.repo.branches['master'])
501 501
502 502 def test_local_push_non_bare_target_branch_is_checked_out(self, tmpdir):
503 503 target_repo = self.get_clone_repo(tmpdir)
504 504
505 505 pushed_branch = 'pushed_branch'
506 506 # Create a new branch in source repo
507 507 new_branch_commit = target_repo.commit_ids[-3]
508 508 target_repo._checkout(new_branch_commit)
509 509 target_repo._checkout(pushed_branch, create=True)
510 510
511 511 self.repo._local_push('master', target_repo.path, pushed_branch)
512 512
513 513 target_repo = GitRepository(target_repo.path)
514 514
515 515 assert (target_repo.branches[pushed_branch] ==
516 516 self.repo.branches['master'])
517 517
518 518 def test_local_push_raises_exception_on_conflict(self, vcsbackend_git):
519 519 target_repo = vcsbackend_git.create_repo(number_of_commits=1)
520 520 with pytest.raises(RepositoryError):
521 521 self.repo._local_push('master', target_repo.path, 'master')
522 522
523 523 def test_hooks_can_be_enabled_via_env_variable_for_local_push(self, tmpdir):
524 524 target_repo = self.get_empty_repo(tmpdir, bare=True)
525 525
526 526 with mock.patch.object(self.repo, 'run_git_command') as run_mock:
527 527 self.repo._local_push(
528 528 'master', target_repo.path, 'master', enable_hooks=True)
529 529 env = run_mock.call_args[1]['extra_env']
530 530 assert 'RC_SKIP_HOOKS' not in env
531 531
532 532 def _add_failing_hook(self, repo_path, hook_name, bare=False):
533 533 path_components = (
534 534 ['hooks', hook_name] if bare else ['.git', 'hooks', hook_name])
535 535 hook_path = os.path.join(repo_path, *path_components)
536 536 with open(hook_path, 'w') as f:
537 537 script_lines = [
538 538 '#!%s' % sys.executable,
539 539 'import os',
540 540 'import sys',
541 541 'if os.environ.get("RC_SKIP_HOOKS"):',
542 542 ' sys.exit(0)',
543 543 'sys.exit(1)',
544 544 ]
545 545 f.write('\n'.join(script_lines))
546 546 os.chmod(hook_path, 0o755)
547 547
548 548 def test_local_push_does_not_execute_hook(self, tmpdir):
549 549 target_repo = self.get_empty_repo(tmpdir)
550 550
551 551 pushed_branch = 'pushed_branch'
552 552 self._add_failing_hook(target_repo.path, 'pre-receive')
553 553 self.repo._local_push('master', target_repo.path, pushed_branch)
554 554 # Fix the HEAD of the target repo, or otherwise GitRepository won't
555 555 # report any branches.
556 556 with open(os.path.join(target_repo.path, '.git', 'HEAD'), 'w') as f:
557 557 f.write('ref: refs/heads/%s' % pushed_branch)
558 558
559 559 target_repo = GitRepository(target_repo.path)
560 560
561 561 assert (target_repo.branches[pushed_branch] ==
562 562 self.repo.branches['master'])
563 563
564 564 def test_local_push_executes_hook(self, tmpdir):
565 565 target_repo = self.get_empty_repo(tmpdir, bare=True)
566 566 self._add_failing_hook(target_repo.path, 'pre-receive', bare=True)
567 567 with pytest.raises(RepositoryError):
568 568 self.repo._local_push(
569 569 'master', target_repo.path, 'master', enable_hooks=True)
570 570
571 571 def test_maybe_prepare_merge_workspace(self):
572 572 workspace = self.repo._maybe_prepare_merge_workspace(
573 573 2, 'pr2', Reference('branch', 'master', 'unused'),
574 574 Reference('branch', 'master', 'unused'))
575 575
576 576 assert os.path.isdir(workspace)
577 577 workspace_repo = GitRepository(workspace)
578 578 assert workspace_repo.branches == self.repo.branches
579 579
580 580 # Calling it a second time should also succeed
581 581 workspace = self.repo._maybe_prepare_merge_workspace(
582 582 2, 'pr2', Reference('branch', 'master', 'unused'),
583 583 Reference('branch', 'master', 'unused'))
584 584 assert os.path.isdir(workspace)
585 585
586 586 def test_maybe_prepare_merge_workspace_different_refs(self):
587 587 workspace = self.repo._maybe_prepare_merge_workspace(
588 588 2, 'pr2', Reference('branch', 'master', 'unused'),
589 589 Reference('branch', 'develop', 'unused'))
590 590
591 591 assert os.path.isdir(workspace)
592 592 workspace_repo = GitRepository(workspace)
593 593 assert workspace_repo.branches == self.repo.branches
594 594
595 595 # Calling it a second time should also succeed
596 596 workspace = self.repo._maybe_prepare_merge_workspace(
597 597 2, 'pr2', Reference('branch', 'master', 'unused'),
598 598 Reference('branch', 'develop', 'unused'))
599 599 assert os.path.isdir(workspace)
600 600
601 601 def test_cleanup_merge_workspace(self):
602 602 workspace = self.repo._maybe_prepare_merge_workspace(
603 603 2, 'pr3', Reference('branch', 'master', 'unused'),
604 604 Reference('branch', 'master', 'unused'))
605 605 self.repo.cleanup_merge_workspace(2, 'pr3')
606 606
607 607 assert not os.path.exists(workspace)
608 608
609 609 def test_cleanup_merge_workspace_invalid_workspace_id(self):
610 610 # No assert: because in case of an inexistent workspace this function
611 611 # should still succeed.
612 612 self.repo.cleanup_merge_workspace(1, 'pr4')
613 613
614 614 def test_set_refs(self):
615 615 test_ref = 'refs/test-refs/abcde'
616 616 test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623'
617 617
618 618 self.repo.set_refs(test_ref, test_commit_id)
619 619 stdout, _ = self.repo.run_git_command(['show-ref'])
620 620 assert test_ref in stdout
621 621 assert test_commit_id in stdout
622 622
623 623 def test_remove_ref(self):
624 624 test_ref = 'refs/test-refs/abcde'
625 625 test_commit_id = 'ecb86e1f424f2608262b130db174a7dfd25a6623'
626 626 self.repo.set_refs(test_ref, test_commit_id)
627 627 stdout, _ = self.repo.run_git_command(['show-ref'])
628 628 assert test_ref in stdout
629 629 assert test_commit_id in stdout
630 630
631 631 self.repo.remove_ref(test_ref)
632 632 stdout, _ = self.repo.run_git_command(['show-ref'])
633 633 assert test_ref not in stdout
634 634 assert test_commit_id not in stdout
635 635
636 636
637 637 class TestGitCommit(object):
638 638
639 639 @pytest.fixture(autouse=True)
640 640 def prepare(self):
641 641 self.repo = GitRepository(TEST_GIT_REPO)
642 642
643 643 def test_default_commit(self):
644 644 tip = self.repo.get_commit()
645 645 assert tip == self.repo.get_commit(None)
646 646 assert tip == self.repo.get_commit('tip')
647 647
648 648 def test_root_node(self):
649 649 tip = self.repo.get_commit()
650 650 assert tip.root is tip.get_node('')
651 651
652 652 def test_lazy_fetch(self):
653 653 """
654 654 Test if commit's nodes expands and are cached as we walk through
655 655 the commit. This test is somewhat hard to write as order of tests
656 656 is a key here. Written by running command after command in a shell.
657 657 """
658 658 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
659 659 assert commit_id in self.repo.commit_ids
660 660 commit = self.repo.get_commit(commit_id)
661 661 assert len(commit.nodes) == 0
662 662 root = commit.root
663 663 assert len(commit.nodes) == 1
664 664 assert len(root.nodes) == 8
665 665 # accessing root.nodes updates commit.nodes
666 666 assert len(commit.nodes) == 9
667 667
668 668 docs = root.get_node('docs')
669 669 # we haven't yet accessed anything new as docs dir was already cached
670 670 assert len(commit.nodes) == 9
671 671 assert len(docs.nodes) == 8
672 672 # accessing docs.nodes updates commit.nodes
673 673 assert len(commit.nodes) == 17
674 674
675 675 assert docs is commit.get_node('docs')
676 676 assert docs is root.nodes[0]
677 677 assert docs is root.dirs[0]
678 678 assert docs is commit.get_node('docs')
679 679
680 680 def test_nodes_with_commit(self):
681 681 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
682 682 commit = self.repo.get_commit(commit_id)
683 683 root = commit.root
684 684 docs = root.get_node('docs')
685 685 assert docs is commit.get_node('docs')
686 686 api = docs.get_node('api')
687 687 assert api is commit.get_node('docs/api')
688 688 index = api.get_node('index.rst')
689 689 assert index is commit.get_node('docs/api/index.rst')
690 690 assert index is commit.get_node('docs')\
691 691 .get_node('api')\
692 692 .get_node('index.rst')
693 693
694 694 def test_branch_and_tags(self):
695 695 """
696 696 rev0 = self.repo.commit_ids[0]
697 697 commit0 = self.repo.get_commit(rev0)
698 698 assert commit0.branch == 'master'
699 699 assert commit0.tags == []
700 700
701 701 rev10 = self.repo.commit_ids[10]
702 702 commit10 = self.repo.get_commit(rev10)
703 703 assert commit10.branch == 'master'
704 704 assert commit10.tags == []
705 705
706 706 rev44 = self.repo.commit_ids[44]
707 707 commit44 = self.repo.get_commit(rev44)
708 708 assert commit44.branch == 'web-branch'
709 709
710 710 tip = self.repo.get_commit('tip')
711 711 assert 'tip' in tip.tags
712 712 """
713 713 # Those tests would fail - branches are now going
714 714 # to be changed at main API in order to support git backend
715 715 pass
716 716
717 717 def test_file_size(self):
718 718 to_check = (
719 719 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
720 720 'vcs/backends/BaseRepository.py', 502),
721 721 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
722 722 'vcs/backends/hg.py', 854),
723 723 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
724 724 'setup.py', 1068),
725 725
726 726 ('d955cd312c17b02143c04fa1099a352b04368118',
727 727 'vcs/backends/base.py', 2921),
728 728 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
729 729 'vcs/backends/base.py', 3936),
730 730 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
731 731 'vcs/backends/base.py', 6189),
732 732 )
733 733 for commit_id, path, size in to_check:
734 734 node = self.repo.get_commit(commit_id).get_node(path)
735 735 assert node.is_file()
736 736 assert node.size == size
737 737
738 738 def test_file_history_from_commits(self):
739 739 node = self.repo[10].get_node('setup.py')
740 740 commit_ids = [commit.raw_id for commit in node.history]
741 741 assert ['ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == commit_ids
742 742
743 743 node = self.repo[20].get_node('setup.py')
744 744 node_ids = [commit.raw_id for commit in node.history]
745 745 assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
746 746 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids
747 747
748 748 # special case we check history from commit that has this particular
749 749 # file changed this means we check if it's included as well
750 750 node = self.repo.get_commit('191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e') \
751 751 .get_node('setup.py')
752 752 node_ids = [commit.raw_id for commit in node.history]
753 753 assert ['191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
754 754 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'] == node_ids
755 755
756 756 def test_file_history(self):
757 757 # we can only check if those commits are present in the history
758 758 # as we cannot update this test every time file is changed
759 759 files = {
760 760 'setup.py': [
761 761 '54386793436c938cff89326944d4c2702340037d',
762 762 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
763 763 '998ed409c795fec2012b1c0ca054d99888b22090',
764 764 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
765 765 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
766 766 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
767 767 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
768 768 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
769 769 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
770 770 ],
771 771 'vcs/nodes.py': [
772 772 '33fa3223355104431402a888fa77a4e9956feb3e',
773 773 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
774 774 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
775 775 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
776 776 'c877b68d18e792a66b7f4c529ea02c8f80801542',
777 777 '4313566d2e417cb382948f8d9d7c765330356054',
778 778 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
779 779 '54386793436c938cff89326944d4c2702340037d',
780 780 '54000345d2e78b03a99d561399e8e548de3f3203',
781 781 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
782 782 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
783 783 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
784 784 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
785 785 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
786 786 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
787 787 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
788 788 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
789 789 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
790 790 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
791 791 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
792 792 'f15c21f97864b4f071cddfbf2750ec2e23859414',
793 793 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
794 794 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
795 795 '84dec09632a4458f79f50ddbbd155506c460b4f9',
796 796 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
797 797 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
798 798 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
799 799 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
800 800 '6970b057cffe4aab0a792aa634c89f4bebf01441',
801 801 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
802 802 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
803 803 ],
804 804 'vcs/backends/git.py': [
805 805 '4cf116ad5a457530381135e2f4c453e68a1b0105',
806 806 '9a751d84d8e9408e736329767387f41b36935153',
807 807 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
808 808 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
809 809 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
810 810 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
811 811 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
812 812 '54000345d2e78b03a99d561399e8e548de3f3203',
813 813 ],
814 814 }
815 815 for path, commit_ids in files.items():
816 816 node = self.repo.get_commit(commit_ids[0]).get_node(path)
817 817 node_ids = [commit.raw_id for commit in node.history]
818 818 assert set(commit_ids).issubset(set(node_ids)), (
819 819 "We assumed that %s is subset of commit_ids for which file %s "
820 820 "has been changed, and history of that node returned: %s"
821 821 % (commit_ids, path, node_ids))
822 822
823 823 def test_file_annotate(self):
824 824 files = {
825 825 'vcs/backends/__init__.py': {
826 826 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
827 827 'lines_no': 1,
828 828 'commits': [
829 829 'c1214f7e79e02fc37156ff215cd71275450cffc3',
830 830 ],
831 831 },
832 832 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
833 833 'lines_no': 21,
834 834 'commits': [
835 835 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
836 836 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
837 837 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
838 838 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
839 839 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
840 840 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
841 841 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
842 842 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
843 843 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
844 844 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
845 845 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
846 846 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
847 847 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
848 848 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
849 849 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
850 850 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
851 851 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
852 852 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
853 853 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
854 854 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
855 855 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
856 856 ],
857 857 },
858 858 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
859 859 'lines_no': 32,
860 860 'commits': [
861 861 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
862 862 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
863 863 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
864 864 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
865 865 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
866 866 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
867 867 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
868 868 '54000345d2e78b03a99d561399e8e548de3f3203',
869 869 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
870 870 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
871 871 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
872 872 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
873 873 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
874 874 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
875 875 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
876 876 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
877 877 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
878 878 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
879 879 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
880 880 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
881 881 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
882 882 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
883 883 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
884 884 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
885 885 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
886 886 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
887 887 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
888 888 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
889 889 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
890 890 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
891 891 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
892 892 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
893 893 ],
894 894 },
895 895 },
896 896 }
897 897
898 898 for fname, commit_dict in files.items():
899 899 for commit_id, __ in commit_dict.items():
900 900 commit = self.repo.get_commit(commit_id)
901 901
902 902 l1_1 = [x[1] for x in commit.get_file_annotate(fname)]
903 903 l1_2 = [x[2]().raw_id for x in commit.get_file_annotate(fname)]
904 904 assert l1_1 == l1_2
905 905 l1 = l1_1
906 906 l2 = files[fname][commit_id]['commits']
907 907 assert l1 == l2, (
908 908 "The lists of commit_ids for %s@commit_id %s"
909 909 "from annotation list should match each other, "
910 910 "got \n%s \nvs \n%s " % (fname, commit_id, l1, l2))
911 911
912 912 def test_files_state(self):
913 913 """
914 914 Tests state of FileNodes.
915 915 """
916 916 node = self.repo\
917 917 .get_commit('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0')\
918 918 .get_node('vcs/utils/diffs.py')
919 919 assert node.state, NodeState.ADDED
920 920 assert node.added
921 921 assert not node.changed
922 922 assert not node.not_changed
923 923 assert not node.removed
924 924
925 925 node = self.repo\
926 926 .get_commit('33fa3223355104431402a888fa77a4e9956feb3e')\
927 927 .get_node('.hgignore')
928 928 assert node.state, NodeState.CHANGED
929 929 assert not node.added
930 930 assert node.changed
931 931 assert not node.not_changed
932 932 assert not node.removed
933 933
934 934 node = self.repo\
935 935 .get_commit('e29b67bd158580fc90fc5e9111240b90e6e86064')\
936 936 .get_node('setup.py')
937 937 assert node.state, NodeState.NOT_CHANGED
938 938 assert not node.added
939 939 assert not node.changed
940 940 assert node.not_changed
941 941 assert not node.removed
942 942
943 943 # If node has REMOVED state then trying to fetch it would raise
944 944 # CommitError exception
945 945 commit = self.repo.get_commit(
946 946 'fa6600f6848800641328adbf7811fd2372c02ab2')
947 947 path = 'vcs/backends/BaseRepository.py'
948 948 with pytest.raises(NodeDoesNotExistError):
949 949 commit.get_node(path)
950 950 # but it would be one of ``removed`` (commit's attribute)
951 951 assert path in [rf.path for rf in commit.removed]
952 952
953 953 commit = self.repo.get_commit(
954 954 '54386793436c938cff89326944d4c2702340037d')
955 955 changed = [
956 956 'setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
957 957 'vcs/nodes.py']
958 958 assert set(changed) == set([f.path for f in commit.changed])
959 959
960 960 def test_unicode_branch_refs(self):
961 961 unicode_branches = {
962 962 'refs/heads/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b',
963 963 u'refs/heads/uniΓ§ΓΆβˆ‚e': 'ΓΌrl',
964 964 }
965 965 with mock.patch(
966 966 ("rhodecode.lib.vcs.backends.git.repository"
967 967 ".GitRepository._refs"),
968 968 unicode_branches):
969 969 branches = self.repo.branches
970 970
971 971 assert 'unicode' in branches
972 972 assert u'uniΓ§ΓΆβˆ‚e' in branches
973 973
974 974 def test_unicode_tag_refs(self):
975 975 unicode_tags = {
976 976 'refs/tags/unicode': '6c0ce52b229aa978889e91b38777f800e85f330b',
977 977 u'refs/tags/uniΓ§ΓΆβˆ‚e': '6c0ce52b229aa978889e91b38777f800e85f330b',
978 978 }
979 979 with mock.patch(
980 980 ("rhodecode.lib.vcs.backends.git.repository"
981 981 ".GitRepository._refs"),
982 982 unicode_tags):
983 983 tags = self.repo.tags
984 984
985 985 assert 'unicode' in tags
986 986 assert u'uniΓ§ΓΆβˆ‚e' in tags
987 987
988 988 def test_commit_message_is_unicode(self):
989 989 for commit in self.repo:
990 990 assert type(commit.message) == str
991 991
992 992 def test_commit_author_is_unicode(self):
993 993 for commit in self.repo:
994 994 assert type(commit.author) == str
995 995
996 996 def test_repo_files_content_types(self):
997 997 commit = self.repo.get_commit()
998 998 for node in commit.get_node('/'):
999 999 if node.is_file():
1000 1000 assert type(node.content) == bytes
1001 1001 assert type(node.str_content) == str
1002 1002
1003 1003 def test_wrong_path(self):
1004 1004 # There is 'setup.py' in the root dir but not there:
1005 1005 path = 'foo/bar/setup.py'
1006 1006 tip = self.repo.get_commit()
1007 1007 with pytest.raises(VCSError):
1008 1008 tip.get_node(path)
1009 1009
1010 1010 @pytest.mark.parametrize("author_email, commit_id", [
1011 1011 ('marcin@python-blog.com', 'c1214f7e79e02fc37156ff215cd71275450cffc3'),
1012 1012 ('lukasz.balcerzak@python-center.pl',
1013 1013 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
1014 1014 ('none@none', '8430a588b43b5d6da365400117c89400326e7992'),
1015 1015 ])
1016 1016 def test_author_email(self, author_email, commit_id):
1017 1017 commit = self.repo.get_commit(commit_id)
1018 1018 assert author_email == commit.author_email
1019 1019
1020 1020 @pytest.mark.parametrize("author, commit_id", [
1021 1021 ('Marcin Kuzminski', 'c1214f7e79e02fc37156ff215cd71275450cffc3'),
1022 1022 ('Lukasz Balcerzak', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
1023 1023 ('marcink', '8430a588b43b5d6da365400117c89400326e7992'),
1024 1024 ])
1025 1025 def test_author_username(self, author, commit_id):
1026 1026 commit = self.repo.get_commit(commit_id)
1027 1027 assert author == commit.author_name
1028 1028
1029 1029
1030 1030 class TestLargeFileRepo(object):
1031 1031
1032 1032 def test_large_file(self, backend_git):
1033 1033 conf = make_db_config()
1034 1034 repo = backend_git.create_test_repo('largefiles', conf)
1035 1035
1036 1036 tip = repo.scm_instance().get_commit()
1037 1037
1038 1038 # extract stored LF node into the origin cache
1039 1039 lfs_store = os.path.join(repo.repo_path, repo.repo_name, 'lfs_store')
1040 1040
1041 1041 oid = '7b331c02e313c7599d5a90212e17e6d3cb729bd2e1c9b873c302a63c95a2f9bf'
1042 1042 oid_path = os.path.join(lfs_store, oid)
1043 # Todo: oid path depends on LFSOidStorage.store_suffix. Once it will be changed update below line accordingly
1043 1044 oid_destination = os.path.join(
1044 conf.get('vcs_git_lfs', 'store_location'), oid)
1045 conf.get('vcs_git_lfs', 'store_location'), f'objects/{oid[:2]}/{oid[2:4]}/{oid}')
1046
1047 os.makedirs(os.path.dirname(oid_destination))
1045 1048 shutil.copy(oid_path, oid_destination)
1046 1049
1047 1050 node = tip.get_node('1MB.zip')
1048 1051
1049 1052 lf_node = node.get_largefile_node()
1050 1053
1051 1054 assert lf_node.is_largefile() is True
1052 1055 assert lf_node.size == 1024000
1053 1056 assert lf_node.name == '1MB.zip'
1054 1057
1055 1058
1056 1059 @pytest.mark.usefixtures("vcs_repository_support")
1057 1060 class TestGitSpecificWithRepo(BackendTestMixin):
1058 1061
1059 1062 @classmethod
1060 1063 def _get_commits(cls):
1061 1064 return [
1062 1065 {
1063 1066 'message': 'Initial',
1064 1067 'author': 'Joe Doe <joe.doe@example.com>',
1065 1068 'date': datetime.datetime(2010, 1, 1, 20),
1066 1069 'added': [
1067 1070 FileNode(b'foobar/static/js/admin/base.js', content=b'base'),
1068 1071 FileNode(b'foobar/static/admin', content=b'admin', mode=0o120000), # this is a link
1069 1072 FileNode(b'foo', content=b'foo'),
1070 1073 ],
1071 1074 },
1072 1075 {
1073 1076 'message': 'Second',
1074 1077 'author': 'Joe Doe <joe.doe@example.com>',
1075 1078 'date': datetime.datetime(2010, 1, 1, 22),
1076 1079 'added': [
1077 1080 FileNode(b'foo2', content=b'foo2'),
1078 1081 ],
1079 1082 },
1080 1083 ]
1081 1084
1082 1085 def test_paths_slow_traversing(self):
1083 1086 commit = self.repo.get_commit()
1084 1087 assert commit.get_node('foobar').get_node('static').get_node('js')\
1085 1088 .get_node('admin').get_node('base.js').content == b'base'
1086 1089
1087 1090 def test_paths_fast_traversing(self):
1088 1091 commit = self.repo.get_commit()
1089 1092 assert commit.get_node('foobar/static/js/admin/base.js').content == b'base'
1090 1093
1091 1094 def test_get_diff_runs_git_command_with_hashes(self):
1092 1095 comm1 = self.repo[0]
1093 1096 comm2 = self.repo[1]
1094 1097
1095 1098 with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock:
1096 1099 remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff)
1097 1100 self.repo.get_diff(comm1, comm2)
1098 1101
1099 1102 remote_mock.diff.assert_called_once_with(
1100 1103 comm1.raw_id, comm2.raw_id,
1101 1104 file_filter=None, opt_ignorews=False, context=3)
1102 1105
1103 1106 def test_get_diff_runs_git_command_with_str_hashes(self):
1104 1107 comm2 = self.repo[1]
1105 1108
1106 1109 with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock:
1107 1110 remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff)
1108 1111 self.repo.get_diff(self.repo.EMPTY_COMMIT, comm2)
1109 1112
1110 1113 remote_mock.diff.assert_called_once_with(
1111 1114 self.repo.EMPTY_COMMIT.raw_id, comm2.raw_id,
1112 1115 file_filter=None, opt_ignorews=False, context=3)
1113 1116
1114 1117 def test_get_diff_runs_git_command_with_path_if_its_given(self):
1115 1118 comm1 = self.repo[0]
1116 1119 comm2 = self.repo[1]
1117 1120
1118 1121 with mock.patch.object(self.repo, '_remote', return_value=mock.Mock()) as remote_mock:
1119 1122 remote_mock.diff = mock.MagicMock(side_effect=callable_get_diff)
1120 1123 self.repo.get_diff(comm1, comm2, 'foo')
1121 1124
1122 1125 remote_mock.diff.assert_called_once_with(
1123 1126 self.repo._lookup_commit(0), comm2.raw_id,
1124 1127 file_filter='foo', opt_ignorews=False, context=3)
1125 1128
1126 1129
1127 1130 @pytest.mark.usefixtures("vcs_repository_support")
1128 1131 class TestGitRegression(BackendTestMixin):
1129 1132
1130 1133 @classmethod
1131 1134 def _get_commits(cls):
1132 1135 return [
1133 1136 {
1134 1137 'message': 'Initial',
1135 1138 'author': 'Joe Doe <joe.doe@example.com>',
1136 1139 'date': datetime.datetime(2010, 1, 1, 20),
1137 1140 'added': [
1138 1141 FileNode(b'bot/__init__.py', content=b'base'),
1139 1142 FileNode(b'bot/templates/404.html', content=b'base'),
1140 1143 FileNode(b'bot/templates/500.html', content=b'base'),
1141 1144 ],
1142 1145 },
1143 1146 {
1144 1147 'message': 'Second',
1145 1148 'author': 'Joe Doe <joe.doe@example.com>',
1146 1149 'date': datetime.datetime(2010, 1, 1, 22),
1147 1150 'added': [
1148 1151 FileNode(b'bot/build/migrations/1.py', content=b'foo2'),
1149 1152 FileNode(b'bot/build/migrations/2.py', content=b'foo2'),
1150 1153 FileNode(b'bot/build/static/templates/f.html', content=b'foo2'),
1151 1154 FileNode(b'bot/build/static/templates/f1.html', content=b'foo2'),
1152 1155 FileNode(b'bot/build/templates/err.html', content=b'foo2'),
1153 1156 FileNode(b'bot/build/templates/err2.html', content=b'foo2'),
1154 1157 ],
1155 1158 },
1156 1159 ]
1157 1160
1158 1161 @pytest.mark.parametrize("path, expected_paths", [
1159 1162 ('bot', [
1160 1163 'bot/build',
1161 1164 'bot/templates',
1162 1165 'bot/__init__.py']),
1163 1166 ('bot/build', [
1164 1167 'bot/build/migrations',
1165 1168 'bot/build/static',
1166 1169 'bot/build/templates']),
1167 1170 ('bot/build/static', [
1168 1171 'bot/build/static/templates']),
1169 1172 ('bot/build/static/templates', [
1170 1173 'bot/build/static/templates/f.html',
1171 1174 'bot/build/static/templates/f1.html']),
1172 1175 ('bot/build/templates', [
1173 1176 'bot/build/templates/err.html',
1174 1177 'bot/build/templates/err2.html']),
1175 1178 ('bot/templates/', [
1176 1179 'bot/templates/404.html',
1177 1180 'bot/templates/500.html']),
1178 1181 ])
1179 1182 def test_similar_paths(self, path, expected_paths):
1180 1183 commit = self.repo.get_commit()
1181 1184 paths = [n.path for n in commit.get_nodes(path)]
1182 1185 assert paths == expected_paths
1183 1186
1184 1187
1185 1188 class TestDiscoverGitVersion(object):
1186 1189
1187 1190 def test_returns_git_version(self, baseapp):
1188 1191 version = discover_git_version()
1189 1192 assert version
1190 1193
1191 1194 def test_returns_empty_string_without_vcsserver(self):
1192 1195 mock_connection = mock.Mock()
1193 1196 mock_connection.discover_git_version = mock.Mock(
1194 1197 side_effect=Exception)
1195 1198 with mock.patch('rhodecode.lib.vcs.connection.Git', mock_connection):
1196 1199 version = discover_git_version()
1197 1200 assert version == ''
1198 1201
1199 1202
1200 1203 class TestGetSubmoduleUrl(object):
1201 1204 def test_submodules_file_found(self):
1202 1205 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1203 1206 node = mock.Mock()
1204 1207
1205 1208 with mock.patch.object(
1206 1209 commit, 'get_node', return_value=node) as get_node_mock:
1207 1210 node.str_content = (
1208 1211 '[submodule "subrepo1"]\n'
1209 1212 '\tpath = subrepo1\n'
1210 1213 '\turl = https://code.rhodecode.com/dulwich\n'
1211 1214 )
1212 1215 result = commit._get_submodule_url('subrepo1')
1213 1216 get_node_mock.assert_called_once_with('.gitmodules')
1214 1217 assert result == 'https://code.rhodecode.com/dulwich'
1215 1218
1216 1219 def test_complex_submodule_path(self):
1217 1220 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1218 1221 node = mock.Mock()
1219 1222
1220 1223 with mock.patch.object(
1221 1224 commit, 'get_node', return_value=node) as get_node_mock:
1222 1225 node.str_content = (
1223 1226 '[submodule "complex/subrepo/path"]\n'
1224 1227 '\tpath = complex/subrepo/path\n'
1225 1228 '\turl = https://code.rhodecode.com/dulwich\n'
1226 1229 )
1227 1230 result = commit._get_submodule_url('complex/subrepo/path')
1228 1231 get_node_mock.assert_called_once_with('.gitmodules')
1229 1232 assert result == 'https://code.rhodecode.com/dulwich'
1230 1233
1231 1234 def test_submodules_file_not_found(self):
1232 1235 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1233 1236 with mock.patch.object(
1234 1237 commit, 'get_node', side_effect=NodeDoesNotExistError):
1235 1238 result = commit._get_submodule_url('complex/subrepo/path')
1236 1239 assert result is None
1237 1240
1238 1241 def test_path_not_found(self):
1239 1242 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1240 1243 node = mock.Mock()
1241 1244
1242 1245 with mock.patch.object(
1243 1246 commit, 'get_node', return_value=node) as get_node_mock:
1244 1247 node.str_content = (
1245 1248 '[submodule "subrepo1"]\n'
1246 1249 '\tpath = subrepo1\n'
1247 1250 '\turl = https://code.rhodecode.com/dulwich\n'
1248 1251 )
1249 1252 result = commit._get_submodule_url('subrepo2')
1250 1253 get_node_mock.assert_called_once_with('.gitmodules')
1251 1254 assert result is None
1252 1255
1253 1256 def test_returns_cached_values(self):
1254 1257 commit = GitCommit(repository=mock.Mock(), raw_id='abcdef12', idx=1)
1255 1258 node = mock.Mock()
1256 1259
1257 1260 with mock.patch.object(
1258 1261 commit, 'get_node', return_value=node) as get_node_mock:
1259 1262 node.str_content = (
1260 1263 '[submodule "subrepo1"]\n'
1261 1264 '\tpath = subrepo1\n'
1262 1265 '\turl = https://code.rhodecode.com/dulwich\n'
1263 1266 )
1264 1267 for _ in range(3):
1265 1268 commit._get_submodule_url('subrepo1')
1266 1269 get_node_mock.assert_called_once_with('.gitmodules')
1267 1270
1268 1271 def test_get_node_returns_a_link(self):
1269 1272 repository = mock.Mock()
1270 1273 repository.alias = 'git'
1271 1274 commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1)
1272 1275 submodule_url = 'https://code.rhodecode.com/dulwich'
1273 1276 get_id_patch = mock.patch.object(
1274 1277 commit, '_get_tree_id_for_path', return_value=(1, 'link'))
1275 1278 get_submodule_patch = mock.patch.object(
1276 1279 commit, '_get_submodule_url', return_value=submodule_url)
1277 1280
1278 1281 with get_id_patch, get_submodule_patch as submodule_mock:
1279 1282 node = commit.get_node('/abcde')
1280 1283
1281 1284 submodule_mock.assert_called_once_with('/abcde')
1282 1285 assert type(node) == SubModuleNode
1283 1286 assert node.url == submodule_url
1284 1287
1285 1288 def test_get_nodes_returns_links(self):
1286 1289 repository = mock.MagicMock()
1287 1290 repository.alias = 'git'
1288 1291 repository._remote.tree_items.return_value = [
1289 1292 ('subrepo', 'stat', 1, 'link')
1290 1293 ]
1291 1294 commit = GitCommit(repository=repository, raw_id='abcdef12', idx=1)
1292 1295 submodule_url = 'https://code.rhodecode.com/dulwich'
1293 1296 get_id_patch = mock.patch.object(
1294 1297 commit, '_get_tree_id_for_path', return_value=(1, 'tree'))
1295 1298 get_submodule_patch = mock.patch.object(
1296 1299 commit, '_get_submodule_url', return_value=submodule_url)
1297 1300
1298 1301 with get_id_patch, get_submodule_patch as submodule_mock:
1299 1302 nodes = commit.get_nodes('/abcde')
1300 1303
1301 1304 submodule_mock.assert_called_once_with('/abcde/subrepo')
1302 1305 assert len(nodes) == 1
1303 1306 assert type(nodes[0]) == SubModuleNode
1304 1307 assert nodes[0].url == submodule_url
General Comments 0
You need to be logged in to leave comments. Login now