##// END OF EJS Templates
vcs: commits, expose refs function to fetch commit refereces such as tags, bookmarks, etc.
marcink -
r2337:58e07bf5 default
parent child Browse files
Show More
@@ -1,2070 +1,2066 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.utils2 import str2bool, time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 38 from rhodecode.model.changeset_status import ChangesetStatusModel
39 39 from rhodecode.model.comment import CommentsModel
40 40 from rhodecode.model.db import (
41 41 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
42 42 ChangesetComment)
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.scm import ScmModel, RepoList
45 45 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 46 from rhodecode.model import validation_schema
47 47 from rhodecode.model.validation_schema.schemas import repo_schema
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 @jsonrpc_method()
53 53 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 54 """
55 55 Gets an existing repository by its name or repository_id.
56 56
57 57 The members section so the output returns users groups or users
58 58 associated with that repository.
59 59
60 60 This command can only be run using an |authtoken| with admin rights,
61 61 or users with at least read rights to the |repo|.
62 62
63 63 :param apiuser: This is filled automatically from the |authtoken|.
64 64 :type apiuser: AuthUser
65 65 :param repoid: The repository name or repository id.
66 66 :type repoid: str or int
67 67 :param cache: use the cached value for last changeset
68 68 :type: cache: Optional(bool)
69 69
70 70 Example output:
71 71
72 72 .. code-block:: bash
73 73
74 74 {
75 75 "error": null,
76 76 "id": <repo_id>,
77 77 "result": {
78 78 "clone_uri": null,
79 79 "created_on": "timestamp",
80 80 "description": "repo description",
81 81 "enable_downloads": false,
82 82 "enable_locking": false,
83 83 "enable_statistics": false,
84 84 "followers": [
85 85 {
86 86 "active": true,
87 87 "admin": false,
88 88 "api_key": "****************************************",
89 89 "api_keys": [
90 90 "****************************************"
91 91 ],
92 92 "email": "user@example.com",
93 93 "emails": [
94 94 "user@example.com"
95 95 ],
96 96 "extern_name": "rhodecode",
97 97 "extern_type": "rhodecode",
98 98 "firstname": "username",
99 99 "ip_addresses": [],
100 100 "language": null,
101 101 "last_login": "2015-09-16T17:16:35.854",
102 102 "lastname": "surname",
103 103 "user_id": <user_id>,
104 104 "username": "name"
105 105 }
106 106 ],
107 107 "fork_of": "parent-repo",
108 108 "landing_rev": [
109 109 "rev",
110 110 "tip"
111 111 ],
112 112 "last_changeset": {
113 113 "author": "User <user@example.com>",
114 114 "branch": "default",
115 115 "date": "timestamp",
116 116 "message": "last commit message",
117 117 "parents": [
118 118 {
119 119 "raw_id": "commit-id"
120 120 }
121 121 ],
122 122 "raw_id": "commit-id",
123 123 "revision": <revision number>,
124 124 "short_id": "short id"
125 125 },
126 126 "lock_reason": null,
127 127 "locked_by": null,
128 128 "locked_date": null,
129 129 "members": [
130 130 {
131 131 "name": "super-admin-name",
132 132 "origin": "super-admin",
133 133 "permission": "repository.admin",
134 134 "type": "user"
135 135 },
136 136 {
137 137 "name": "owner-name",
138 138 "origin": "owner",
139 139 "permission": "repository.admin",
140 140 "type": "user"
141 141 },
142 142 {
143 143 "name": "user-group-name",
144 144 "origin": "permission",
145 145 "permission": "repository.write",
146 146 "type": "user_group"
147 147 }
148 148 ],
149 149 "owner": "owner-name",
150 150 "permissions": [
151 151 {
152 152 "name": "super-admin-name",
153 153 "origin": "super-admin",
154 154 "permission": "repository.admin",
155 155 "type": "user"
156 156 },
157 157 {
158 158 "name": "owner-name",
159 159 "origin": "owner",
160 160 "permission": "repository.admin",
161 161 "type": "user"
162 162 },
163 163 {
164 164 "name": "user-group-name",
165 165 "origin": "permission",
166 166 "permission": "repository.write",
167 167 "type": "user_group"
168 168 }
169 169 ],
170 170 "private": true,
171 171 "repo_id": 676,
172 172 "repo_name": "user-group/repo-name",
173 173 "repo_type": "hg"
174 174 }
175 175 }
176 176 """
177 177
178 178 repo = get_repo_or_error(repoid)
179 179 cache = Optional.extract(cache)
180 180
181 181 include_secrets = False
182 182 if has_superadmin_permission(apiuser):
183 183 include_secrets = True
184 184 else:
185 185 # check if we have at least read permission for this repo !
186 186 _perms = (
187 187 'repository.admin', 'repository.write', 'repository.read',)
188 188 validate_repo_permissions(apiuser, repoid, repo, _perms)
189 189
190 190 permissions = []
191 191 for _user in repo.permissions():
192 192 user_data = {
193 193 'name': _user.username,
194 194 'permission': _user.permission,
195 195 'origin': get_origin(_user),
196 196 'type': "user",
197 197 }
198 198 permissions.append(user_data)
199 199
200 200 for _user_group in repo.permission_user_groups():
201 201 user_group_data = {
202 202 'name': _user_group.users_group_name,
203 203 'permission': _user_group.permission,
204 204 'origin': get_origin(_user_group),
205 205 'type': "user_group",
206 206 }
207 207 permissions.append(user_group_data)
208 208
209 209 following_users = [
210 210 user.user.get_api_data(include_secrets=include_secrets)
211 211 for user in repo.followers]
212 212
213 213 if not cache:
214 214 repo.update_commit_cache()
215 215 data = repo.get_api_data(include_secrets=include_secrets)
216 216 data['members'] = permissions # TODO: this should be deprecated soon
217 217 data['permissions'] = permissions
218 218 data['followers'] = following_users
219 219 return data
220 220
221 221
222 222 @jsonrpc_method()
223 223 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
224 224 """
225 225 Lists all existing repositories.
226 226
227 227 This command can only be run using an |authtoken| with admin rights,
228 228 or users with at least read rights to |repos|.
229 229
230 230 :param apiuser: This is filled automatically from the |authtoken|.
231 231 :type apiuser: AuthUser
232 232 :param root: specify root repository group to fetch repositories.
233 233 filters the returned repositories to be members of given root group.
234 234 :type root: Optional(None)
235 235 :param traverse: traverse given root into subrepositories. With this flag
236 236 set to False, it will only return top-level repositories from `root`.
237 237 if root is empty it will return just top-level repositories.
238 238 :type traverse: Optional(True)
239 239
240 240
241 241 Example output:
242 242
243 243 .. code-block:: bash
244 244
245 245 id : <id_given_in_input>
246 246 result: [
247 247 {
248 248 "repo_id" : "<repo_id>",
249 249 "repo_name" : "<reponame>"
250 250 "repo_type" : "<repo_type>",
251 251 "clone_uri" : "<clone_uri>",
252 252 "private": : "<bool>",
253 253 "created_on" : "<datetimecreated>",
254 254 "description" : "<description>",
255 255 "landing_rev": "<landing_rev>",
256 256 "owner": "<repo_owner>",
257 257 "fork_of": "<name_of_fork_parent>",
258 258 "enable_downloads": "<bool>",
259 259 "enable_locking": "<bool>",
260 260 "enable_statistics": "<bool>",
261 261 },
262 262 ...
263 263 ]
264 264 error: null
265 265 """
266 266
267 267 include_secrets = has_superadmin_permission(apiuser)
268 268 _perms = ('repository.read', 'repository.write', 'repository.admin',)
269 269 extras = {'user': apiuser}
270 270
271 271 root = Optional.extract(root)
272 272 traverse = Optional.extract(traverse, binary=True)
273 273
274 274 if root:
275 275 # verify parent existance, if it's empty return an error
276 276 parent = RepoGroup.get_by_group_name(root)
277 277 if not parent:
278 278 raise JSONRPCError(
279 279 'Root repository group `{}` does not exist'.format(root))
280 280
281 281 if traverse:
282 282 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
283 283 else:
284 284 repos = RepoModel().get_repos_for_root(root=parent)
285 285 else:
286 286 if traverse:
287 287 repos = RepoModel().get_all()
288 288 else:
289 289 # return just top-level
290 290 repos = RepoModel().get_repos_for_root(root=None)
291 291
292 292 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
293 293 return [repo.get_api_data(include_secrets=include_secrets)
294 294 for repo in repo_list]
295 295
296 296
297 297 @jsonrpc_method()
298 298 def get_repo_changeset(request, apiuser, repoid, revision,
299 299 details=Optional('basic')):
300 300 """
301 301 Returns information about a changeset.
302 302
303 303 Additionally parameters define the amount of details returned by
304 304 this function.
305 305
306 306 This command can only be run using an |authtoken| with admin rights,
307 307 or users with at least read rights to the |repo|.
308 308
309 309 :param apiuser: This is filled automatically from the |authtoken|.
310 310 :type apiuser: AuthUser
311 311 :param repoid: The repository name or repository id
312 312 :type repoid: str or int
313 313 :param revision: revision for which listing should be done
314 314 :type revision: str
315 315 :param details: details can be 'basic|extended|full' full gives diff
316 316 info details like the diff itself, and number of changed files etc.
317 317 :type details: Optional(str)
318 318
319 319 """
320 320 repo = get_repo_or_error(repoid)
321 321 if not has_superadmin_permission(apiuser):
322 322 _perms = (
323 323 'repository.admin', 'repository.write', 'repository.read',)
324 324 validate_repo_permissions(apiuser, repoid, repo, _perms)
325 325
326 326 changes_details = Optional.extract(details)
327 327 _changes_details_types = ['basic', 'extended', 'full']
328 328 if changes_details not in _changes_details_types:
329 329 raise JSONRPCError(
330 330 'ret_type must be one of %s' % (
331 331 ','.join(_changes_details_types)))
332 332
333 333 pre_load = ['author', 'branch', 'date', 'message', 'parents',
334 334 'status', '_commit', '_file_paths']
335 335
336 336 try:
337 337 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
338 338 except TypeError as e:
339 339 raise JSONRPCError(e.message)
340 340 _cs_json = cs.__json__()
341 341 _cs_json['diff'] = build_commit_data(cs, changes_details)
342 342 if changes_details == 'full':
343 _cs_json['refs'] = {
344 'branches': [cs.branch],
345 'bookmarks': getattr(cs, 'bookmarks', []),
346 'tags': cs.tags
347 }
343 _cs_json['refs'] = cs._get_refs()
348 344 return _cs_json
349 345
350 346
351 347 @jsonrpc_method()
352 348 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
353 349 details=Optional('basic')):
354 350 """
355 351 Returns a set of commits limited by the number starting
356 352 from the `start_rev` option.
357 353
358 354 Additional parameters define the amount of details returned by this
359 355 function.
360 356
361 357 This command can only be run using an |authtoken| with admin rights,
362 358 or users with at least read rights to |repos|.
363 359
364 360 :param apiuser: This is filled automatically from the |authtoken|.
365 361 :type apiuser: AuthUser
366 362 :param repoid: The repository name or repository ID.
367 363 :type repoid: str or int
368 364 :param start_rev: The starting revision from where to get changesets.
369 365 :type start_rev: str
370 366 :param limit: Limit the number of commits to this amount
371 367 :type limit: str or int
372 368 :param details: Set the level of detail returned. Valid option are:
373 369 ``basic``, ``extended`` and ``full``.
374 370 :type details: Optional(str)
375 371
376 372 .. note::
377 373
378 374 Setting the parameter `details` to the value ``full`` is extensive
379 375 and returns details like the diff itself, and the number
380 376 of changed files.
381 377
382 378 """
383 379 repo = get_repo_or_error(repoid)
384 380 if not has_superadmin_permission(apiuser):
385 381 _perms = (
386 382 'repository.admin', 'repository.write', 'repository.read',)
387 383 validate_repo_permissions(apiuser, repoid, repo, _perms)
388 384
389 385 changes_details = Optional.extract(details)
390 386 _changes_details_types = ['basic', 'extended', 'full']
391 387 if changes_details not in _changes_details_types:
392 388 raise JSONRPCError(
393 389 'ret_type must be one of %s' % (
394 390 ','.join(_changes_details_types)))
395 391
396 392 limit = int(limit)
397 393 pre_load = ['author', 'branch', 'date', 'message', 'parents',
398 394 'status', '_commit', '_file_paths']
399 395
400 396 vcs_repo = repo.scm_instance()
401 397 # SVN needs a special case to distinguish its index and commit id
402 398 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
403 399 start_rev = vcs_repo.commit_ids[0]
404 400
405 401 try:
406 402 commits = vcs_repo.get_commits(
407 403 start_id=start_rev, pre_load=pre_load)
408 404 except TypeError as e:
409 405 raise JSONRPCError(e.message)
410 406 except Exception:
411 407 log.exception('Fetching of commits failed')
412 408 raise JSONRPCError('Error occurred during commit fetching')
413 409
414 410 ret = []
415 411 for cnt, commit in enumerate(commits):
416 412 if cnt >= limit != -1:
417 413 break
418 414 _cs_json = commit.__json__()
419 415 _cs_json['diff'] = build_commit_data(commit, changes_details)
420 416 if changes_details == 'full':
421 417 _cs_json['refs'] = {
422 418 'branches': [commit.branch],
423 419 'bookmarks': getattr(commit, 'bookmarks', []),
424 420 'tags': commit.tags
425 421 }
426 422 ret.append(_cs_json)
427 423 return ret
428 424
429 425
430 426 @jsonrpc_method()
431 427 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
432 428 ret_type=Optional('all'), details=Optional('basic'),
433 429 max_file_bytes=Optional(None)):
434 430 """
435 431 Returns a list of nodes and children in a flat list for a given
436 432 path at given revision.
437 433
438 434 It's possible to specify ret_type to show only `files` or `dirs`.
439 435
440 436 This command can only be run using an |authtoken| with admin rights,
441 437 or users with at least read rights to |repos|.
442 438
443 439 :param apiuser: This is filled automatically from the |authtoken|.
444 440 :type apiuser: AuthUser
445 441 :param repoid: The repository name or repository ID.
446 442 :type repoid: str or int
447 443 :param revision: The revision for which listing should be done.
448 444 :type revision: str
449 445 :param root_path: The path from which to start displaying.
450 446 :type root_path: str
451 447 :param ret_type: Set the return type. Valid options are
452 448 ``all`` (default), ``files`` and ``dirs``.
453 449 :type ret_type: Optional(str)
454 450 :param details: Returns extended information about nodes, such as
455 451 md5, binary, and or content. The valid options are ``basic`` and
456 452 ``full``.
457 453 :type details: Optional(str)
458 454 :param max_file_bytes: Only return file content under this file size bytes
459 455 :type details: Optional(int)
460 456
461 457 Example output:
462 458
463 459 .. code-block:: bash
464 460
465 461 id : <id_given_in_input>
466 462 result: [
467 463 {
468 464 "name" : "<name>"
469 465 "type" : "<type>",
470 466 "binary": "<true|false>" (only in extended mode)
471 467 "md5" : "<md5 of file content>" (only in extended mode)
472 468 },
473 469 ...
474 470 ]
475 471 error: null
476 472 """
477 473
478 474 repo = get_repo_or_error(repoid)
479 475 if not has_superadmin_permission(apiuser):
480 476 _perms = (
481 477 'repository.admin', 'repository.write', 'repository.read',)
482 478 validate_repo_permissions(apiuser, repoid, repo, _perms)
483 479
484 480 ret_type = Optional.extract(ret_type)
485 481 details = Optional.extract(details)
486 482 _extended_types = ['basic', 'full']
487 483 if details not in _extended_types:
488 484 raise JSONRPCError(
489 485 'ret_type must be one of %s' % (','.join(_extended_types)))
490 486 extended_info = False
491 487 content = False
492 488 if details == 'basic':
493 489 extended_info = True
494 490
495 491 if details == 'full':
496 492 extended_info = content = True
497 493
498 494 _map = {}
499 495 try:
500 496 # check if repo is not empty by any chance, skip quicker if it is.
501 497 _scm = repo.scm_instance()
502 498 if _scm.is_empty():
503 499 return []
504 500
505 501 _d, _f = ScmModel().get_nodes(
506 502 repo, revision, root_path, flat=False,
507 503 extended_info=extended_info, content=content,
508 504 max_file_bytes=max_file_bytes)
509 505 _map = {
510 506 'all': _d + _f,
511 507 'files': _f,
512 508 'dirs': _d,
513 509 }
514 510 return _map[ret_type]
515 511 except KeyError:
516 512 raise JSONRPCError(
517 513 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
518 514 except Exception:
519 515 log.exception("Exception occurred while trying to get repo nodes")
520 516 raise JSONRPCError(
521 517 'failed to get repo: `%s` nodes' % repo.repo_name
522 518 )
523 519
524 520
525 521 @jsonrpc_method()
526 522 def get_repo_refs(request, apiuser, repoid):
527 523 """
528 524 Returns a dictionary of current references. It returns
529 525 bookmarks, branches, closed_branches, and tags for given repository
530 526
531 527 It's possible to specify ret_type to show only `files` or `dirs`.
532 528
533 529 This command can only be run using an |authtoken| with admin rights,
534 530 or users with at least read rights to |repos|.
535 531
536 532 :param apiuser: This is filled automatically from the |authtoken|.
537 533 :type apiuser: AuthUser
538 534 :param repoid: The repository name or repository ID.
539 535 :type repoid: str or int
540 536
541 537 Example output:
542 538
543 539 .. code-block:: bash
544 540
545 541 id : <id_given_in_input>
546 542 "result": {
547 543 "bookmarks": {
548 544 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
549 545 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
550 546 },
551 547 "branches": {
552 548 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
553 549 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
554 550 },
555 551 "branches_closed": {},
556 552 "tags": {
557 553 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
558 554 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
559 555 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
560 556 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
561 557 }
562 558 }
563 559 error: null
564 560 """
565 561
566 562 repo = get_repo_or_error(repoid)
567 563 if not has_superadmin_permission(apiuser):
568 564 _perms = ('repository.admin', 'repository.write', 'repository.read',)
569 565 validate_repo_permissions(apiuser, repoid, repo, _perms)
570 566
571 567 try:
572 568 # check if repo is not empty by any chance, skip quicker if it is.
573 569 vcs_instance = repo.scm_instance()
574 570 refs = vcs_instance.refs()
575 571 return refs
576 572 except Exception:
577 573 log.exception("Exception occurred while trying to get repo refs")
578 574 raise JSONRPCError(
579 575 'failed to get repo: `%s` references' % repo.repo_name
580 576 )
581 577
582 578
583 579 @jsonrpc_method()
584 580 def create_repo(
585 581 request, apiuser, repo_name, repo_type,
586 582 owner=Optional(OAttr('apiuser')),
587 583 description=Optional(''),
588 584 private=Optional(False),
589 585 clone_uri=Optional(None),
590 586 landing_rev=Optional('rev:tip'),
591 587 enable_statistics=Optional(False),
592 588 enable_locking=Optional(False),
593 589 enable_downloads=Optional(False),
594 590 copy_permissions=Optional(False)):
595 591 """
596 592 Creates a repository.
597 593
598 594 * If the repository name contains "/", repository will be created inside
599 595 a repository group or nested repository groups
600 596
601 597 For example "foo/bar/repo1" will create |repo| called "repo1" inside
602 598 group "foo/bar". You have to have permissions to access and write to
603 599 the last repository group ("bar" in this example)
604 600
605 601 This command can only be run using an |authtoken| with at least
606 602 permissions to create repositories, or write permissions to
607 603 parent repository groups.
608 604
609 605 :param apiuser: This is filled automatically from the |authtoken|.
610 606 :type apiuser: AuthUser
611 607 :param repo_name: Set the repository name.
612 608 :type repo_name: str
613 609 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
614 610 :type repo_type: str
615 611 :param owner: user_id or username
616 612 :type owner: Optional(str)
617 613 :param description: Set the repository description.
618 614 :type description: Optional(str)
619 615 :param private: set repository as private
620 616 :type private: bool
621 617 :param clone_uri: set clone_uri
622 618 :type clone_uri: str
623 619 :param landing_rev: <rev_type>:<rev>
624 620 :type landing_rev: str
625 621 :param enable_locking:
626 622 :type enable_locking: bool
627 623 :param enable_downloads:
628 624 :type enable_downloads: bool
629 625 :param enable_statistics:
630 626 :type enable_statistics: bool
631 627 :param copy_permissions: Copy permission from group in which the
632 628 repository is being created.
633 629 :type copy_permissions: bool
634 630
635 631
636 632 Example output:
637 633
638 634 .. code-block:: bash
639 635
640 636 id : <id_given_in_input>
641 637 result: {
642 638 "msg": "Created new repository `<reponame>`",
643 639 "success": true,
644 640 "task": "<celery task id or None if done sync>"
645 641 }
646 642 error: null
647 643
648 644
649 645 Example error output:
650 646
651 647 .. code-block:: bash
652 648
653 649 id : <id_given_in_input>
654 650 result : null
655 651 error : {
656 652 'failed to create repository `<repo_name>`'
657 653 }
658 654
659 655 """
660 656
661 657 owner = validate_set_owner_permissions(apiuser, owner)
662 658
663 659 description = Optional.extract(description)
664 660 copy_permissions = Optional.extract(copy_permissions)
665 661 clone_uri = Optional.extract(clone_uri)
666 662 landing_commit_ref = Optional.extract(landing_rev)
667 663
668 664 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
669 665 if isinstance(private, Optional):
670 666 private = defs.get('repo_private') or Optional.extract(private)
671 667 if isinstance(repo_type, Optional):
672 668 repo_type = defs.get('repo_type')
673 669 if isinstance(enable_statistics, Optional):
674 670 enable_statistics = defs.get('repo_enable_statistics')
675 671 if isinstance(enable_locking, Optional):
676 672 enable_locking = defs.get('repo_enable_locking')
677 673 if isinstance(enable_downloads, Optional):
678 674 enable_downloads = defs.get('repo_enable_downloads')
679 675
680 676 schema = repo_schema.RepoSchema().bind(
681 677 repo_type_options=rhodecode.BACKENDS.keys(),
682 678 # user caller
683 679 user=apiuser)
684 680
685 681 try:
686 682 schema_data = schema.deserialize(dict(
687 683 repo_name=repo_name,
688 684 repo_type=repo_type,
689 685 repo_owner=owner.username,
690 686 repo_description=description,
691 687 repo_landing_commit_ref=landing_commit_ref,
692 688 repo_clone_uri=clone_uri,
693 689 repo_private=private,
694 690 repo_copy_permissions=copy_permissions,
695 691 repo_enable_statistics=enable_statistics,
696 692 repo_enable_downloads=enable_downloads,
697 693 repo_enable_locking=enable_locking))
698 694 except validation_schema.Invalid as err:
699 695 raise JSONRPCValidationError(colander_exc=err)
700 696
701 697 try:
702 698 data = {
703 699 'owner': owner,
704 700 'repo_name': schema_data['repo_group']['repo_name_without_group'],
705 701 'repo_name_full': schema_data['repo_name'],
706 702 'repo_group': schema_data['repo_group']['repo_group_id'],
707 703 'repo_type': schema_data['repo_type'],
708 704 'repo_description': schema_data['repo_description'],
709 705 'repo_private': schema_data['repo_private'],
710 706 'clone_uri': schema_data['repo_clone_uri'],
711 707 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
712 708 'enable_statistics': schema_data['repo_enable_statistics'],
713 709 'enable_locking': schema_data['repo_enable_locking'],
714 710 'enable_downloads': schema_data['repo_enable_downloads'],
715 711 'repo_copy_permissions': schema_data['repo_copy_permissions'],
716 712 }
717 713
718 714 task = RepoModel().create(form_data=data, cur_user=owner)
719 715 from celery.result import BaseAsyncResult
720 716 task_id = None
721 717 if isinstance(task, BaseAsyncResult):
722 718 task_id = task.task_id
723 719 # no commit, it's done in RepoModel, or async via celery
724 720 return {
725 721 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
726 722 'success': True, # cannot return the repo data here since fork
727 723 # can be done async
728 724 'task': task_id
729 725 }
730 726 except Exception:
731 727 log.exception(
732 728 u"Exception while trying to create the repository %s",
733 729 schema_data['repo_name'])
734 730 raise JSONRPCError(
735 731 'failed to create repository `%s`' % (schema_data['repo_name'],))
736 732
737 733
738 734 @jsonrpc_method()
739 735 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
740 736 description=Optional('')):
741 737 """
742 738 Adds an extra field to a repository.
743 739
744 740 This command can only be run using an |authtoken| with at least
745 741 write permissions to the |repo|.
746 742
747 743 :param apiuser: This is filled automatically from the |authtoken|.
748 744 :type apiuser: AuthUser
749 745 :param repoid: Set the repository name or repository id.
750 746 :type repoid: str or int
751 747 :param key: Create a unique field key for this repository.
752 748 :type key: str
753 749 :param label:
754 750 :type label: Optional(str)
755 751 :param description:
756 752 :type description: Optional(str)
757 753 """
758 754 repo = get_repo_or_error(repoid)
759 755 if not has_superadmin_permission(apiuser):
760 756 _perms = ('repository.admin',)
761 757 validate_repo_permissions(apiuser, repoid, repo, _perms)
762 758
763 759 label = Optional.extract(label) or key
764 760 description = Optional.extract(description)
765 761
766 762 field = RepositoryField.get_by_key_name(key, repo)
767 763 if field:
768 764 raise JSONRPCError('Field with key '
769 765 '`%s` exists for repo `%s`' % (key, repoid))
770 766
771 767 try:
772 768 RepoModel().add_repo_field(repo, key, field_label=label,
773 769 field_desc=description)
774 770 Session().commit()
775 771 return {
776 772 'msg': "Added new repository field `%s`" % (key,),
777 773 'success': True,
778 774 }
779 775 except Exception:
780 776 log.exception("Exception occurred while trying to add field to repo")
781 777 raise JSONRPCError(
782 778 'failed to create new field for repository `%s`' % (repoid,))
783 779
784 780
785 781 @jsonrpc_method()
786 782 def remove_field_from_repo(request, apiuser, repoid, key):
787 783 """
788 784 Removes an extra field from a repository.
789 785
790 786 This command can only be run using an |authtoken| with at least
791 787 write permissions to the |repo|.
792 788
793 789 :param apiuser: This is filled automatically from the |authtoken|.
794 790 :type apiuser: AuthUser
795 791 :param repoid: Set the repository name or repository ID.
796 792 :type repoid: str or int
797 793 :param key: Set the unique field key for this repository.
798 794 :type key: str
799 795 """
800 796
801 797 repo = get_repo_or_error(repoid)
802 798 if not has_superadmin_permission(apiuser):
803 799 _perms = ('repository.admin',)
804 800 validate_repo_permissions(apiuser, repoid, repo, _perms)
805 801
806 802 field = RepositoryField.get_by_key_name(key, repo)
807 803 if not field:
808 804 raise JSONRPCError('Field with key `%s` does not '
809 805 'exists for repo `%s`' % (key, repoid))
810 806
811 807 try:
812 808 RepoModel().delete_repo_field(repo, field_key=key)
813 809 Session().commit()
814 810 return {
815 811 'msg': "Deleted repository field `%s`" % (key,),
816 812 'success': True,
817 813 }
818 814 except Exception:
819 815 log.exception(
820 816 "Exception occurred while trying to delete field from repo")
821 817 raise JSONRPCError(
822 818 'failed to delete field for repository `%s`' % (repoid,))
823 819
824 820
825 821 @jsonrpc_method()
826 822 def update_repo(
827 823 request, apiuser, repoid, repo_name=Optional(None),
828 824 owner=Optional(OAttr('apiuser')), description=Optional(''),
829 825 private=Optional(False), clone_uri=Optional(None),
830 826 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
831 827 enable_statistics=Optional(False),
832 828 enable_locking=Optional(False),
833 829 enable_downloads=Optional(False), fields=Optional('')):
834 830 """
835 831 Updates a repository with the given information.
836 832
837 833 This command can only be run using an |authtoken| with at least
838 834 admin permissions to the |repo|.
839 835
840 836 * If the repository name contains "/", repository will be updated
841 837 accordingly with a repository group or nested repository groups
842 838
843 839 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
844 840 called "repo-test" and place it inside group "foo/bar".
845 841 You have to have permissions to access and write to the last repository
846 842 group ("bar" in this example)
847 843
848 844 :param apiuser: This is filled automatically from the |authtoken|.
849 845 :type apiuser: AuthUser
850 846 :param repoid: repository name or repository ID.
851 847 :type repoid: str or int
852 848 :param repo_name: Update the |repo| name, including the
853 849 repository group it's in.
854 850 :type repo_name: str
855 851 :param owner: Set the |repo| owner.
856 852 :type owner: str
857 853 :param fork_of: Set the |repo| as fork of another |repo|.
858 854 :type fork_of: str
859 855 :param description: Update the |repo| description.
860 856 :type description: str
861 857 :param private: Set the |repo| as private. (True | False)
862 858 :type private: bool
863 859 :param clone_uri: Update the |repo| clone URI.
864 860 :type clone_uri: str
865 861 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
866 862 :type landing_rev: str
867 863 :param enable_statistics: Enable statistics on the |repo|, (True | False).
868 864 :type enable_statistics: bool
869 865 :param enable_locking: Enable |repo| locking.
870 866 :type enable_locking: bool
871 867 :param enable_downloads: Enable downloads from the |repo|, (True | False).
872 868 :type enable_downloads: bool
873 869 :param fields: Add extra fields to the |repo|. Use the following
874 870 example format: ``field_key=field_val,field_key2=fieldval2``.
875 871 Escape ', ' with \,
876 872 :type fields: str
877 873 """
878 874
879 875 repo = get_repo_or_error(repoid)
880 876
881 877 include_secrets = False
882 878 if not has_superadmin_permission(apiuser):
883 879 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
884 880 else:
885 881 include_secrets = True
886 882
887 883 updates = dict(
888 884 repo_name=repo_name
889 885 if not isinstance(repo_name, Optional) else repo.repo_name,
890 886
891 887 fork_id=fork_of
892 888 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
893 889
894 890 user=owner
895 891 if not isinstance(owner, Optional) else repo.user.username,
896 892
897 893 repo_description=description
898 894 if not isinstance(description, Optional) else repo.description,
899 895
900 896 repo_private=private
901 897 if not isinstance(private, Optional) else repo.private,
902 898
903 899 clone_uri=clone_uri
904 900 if not isinstance(clone_uri, Optional) else repo.clone_uri,
905 901
906 902 repo_landing_rev=landing_rev
907 903 if not isinstance(landing_rev, Optional) else repo._landing_revision,
908 904
909 905 repo_enable_statistics=enable_statistics
910 906 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
911 907
912 908 repo_enable_locking=enable_locking
913 909 if not isinstance(enable_locking, Optional) else repo.enable_locking,
914 910
915 911 repo_enable_downloads=enable_downloads
916 912 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
917 913
918 914 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
919 915
920 916 old_values = repo.get_api_data()
921 917 schema = repo_schema.RepoSchema().bind(
922 918 repo_type_options=rhodecode.BACKENDS.keys(),
923 919 repo_ref_options=ref_choices,
924 920 # user caller
925 921 user=apiuser,
926 922 old_values=old_values)
927 923 try:
928 924 schema_data = schema.deserialize(dict(
929 925 # we save old value, users cannot change type
930 926 repo_type=repo.repo_type,
931 927
932 928 repo_name=updates['repo_name'],
933 929 repo_owner=updates['user'],
934 930 repo_description=updates['repo_description'],
935 931 repo_clone_uri=updates['clone_uri'],
936 932 repo_fork_of=updates['fork_id'],
937 933 repo_private=updates['repo_private'],
938 934 repo_landing_commit_ref=updates['repo_landing_rev'],
939 935 repo_enable_statistics=updates['repo_enable_statistics'],
940 936 repo_enable_downloads=updates['repo_enable_downloads'],
941 937 repo_enable_locking=updates['repo_enable_locking']))
942 938 except validation_schema.Invalid as err:
943 939 raise JSONRPCValidationError(colander_exc=err)
944 940
945 941 # save validated data back into the updates dict
946 942 validated_updates = dict(
947 943 repo_name=schema_data['repo_group']['repo_name_without_group'],
948 944 repo_group=schema_data['repo_group']['repo_group_id'],
949 945
950 946 user=schema_data['repo_owner'],
951 947 repo_description=schema_data['repo_description'],
952 948 repo_private=schema_data['repo_private'],
953 949 clone_uri=schema_data['repo_clone_uri'],
954 950 repo_landing_rev=schema_data['repo_landing_commit_ref'],
955 951 repo_enable_statistics=schema_data['repo_enable_statistics'],
956 952 repo_enable_locking=schema_data['repo_enable_locking'],
957 953 repo_enable_downloads=schema_data['repo_enable_downloads'],
958 954 )
959 955
960 956 if schema_data['repo_fork_of']:
961 957 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
962 958 validated_updates['fork_id'] = fork_repo.repo_id
963 959
964 960 # extra fields
965 961 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
966 962 if fields:
967 963 validated_updates.update(fields)
968 964
969 965 try:
970 966 RepoModel().update(repo, **validated_updates)
971 967 audit_logger.store_api(
972 968 'repo.edit', action_data={'old_data': old_values},
973 969 user=apiuser, repo=repo)
974 970 Session().commit()
975 971 return {
976 972 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
977 973 'repository': repo.get_api_data(include_secrets=include_secrets)
978 974 }
979 975 except Exception:
980 976 log.exception(
981 977 u"Exception while trying to update the repository %s",
982 978 repoid)
983 979 raise JSONRPCError('failed to update repo `%s`' % repoid)
984 980
985 981
986 982 @jsonrpc_method()
987 983 def fork_repo(request, apiuser, repoid, fork_name,
988 984 owner=Optional(OAttr('apiuser')),
989 985 description=Optional(''),
990 986 private=Optional(False),
991 987 clone_uri=Optional(None),
992 988 landing_rev=Optional('rev:tip'),
993 989 copy_permissions=Optional(False)):
994 990 """
995 991 Creates a fork of the specified |repo|.
996 992
997 993 * If the fork_name contains "/", fork will be created inside
998 994 a repository group or nested repository groups
999 995
1000 996 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1001 997 inside group "foo/bar". You have to have permissions to access and
1002 998 write to the last repository group ("bar" in this example)
1003 999
1004 1000 This command can only be run using an |authtoken| with minimum
1005 1001 read permissions of the forked repo, create fork permissions for an user.
1006 1002
1007 1003 :param apiuser: This is filled automatically from the |authtoken|.
1008 1004 :type apiuser: AuthUser
1009 1005 :param repoid: Set repository name or repository ID.
1010 1006 :type repoid: str or int
1011 1007 :param fork_name: Set the fork name, including it's repository group membership.
1012 1008 :type fork_name: str
1013 1009 :param owner: Set the fork owner.
1014 1010 :type owner: str
1015 1011 :param description: Set the fork description.
1016 1012 :type description: str
1017 1013 :param copy_permissions: Copy permissions from parent |repo|. The
1018 1014 default is False.
1019 1015 :type copy_permissions: bool
1020 1016 :param private: Make the fork private. The default is False.
1021 1017 :type private: bool
1022 1018 :param landing_rev: Set the landing revision. The default is tip.
1023 1019
1024 1020 Example output:
1025 1021
1026 1022 .. code-block:: bash
1027 1023
1028 1024 id : <id_for_response>
1029 1025 api_key : "<api_key>"
1030 1026 args: {
1031 1027 "repoid" : "<reponame or repo_id>",
1032 1028 "fork_name": "<forkname>",
1033 1029 "owner": "<username or user_id = Optional(=apiuser)>",
1034 1030 "description": "<description>",
1035 1031 "copy_permissions": "<bool>",
1036 1032 "private": "<bool>",
1037 1033 "landing_rev": "<landing_rev>"
1038 1034 }
1039 1035
1040 1036 Example error output:
1041 1037
1042 1038 .. code-block:: bash
1043 1039
1044 1040 id : <id_given_in_input>
1045 1041 result: {
1046 1042 "msg": "Created fork of `<reponame>` as `<forkname>`",
1047 1043 "success": true,
1048 1044 "task": "<celery task id or None if done sync>"
1049 1045 }
1050 1046 error: null
1051 1047
1052 1048 """
1053 1049
1054 1050 repo = get_repo_or_error(repoid)
1055 1051 repo_name = repo.repo_name
1056 1052
1057 1053 if not has_superadmin_permission(apiuser):
1058 1054 # check if we have at least read permission for
1059 1055 # this repo that we fork !
1060 1056 _perms = (
1061 1057 'repository.admin', 'repository.write', 'repository.read')
1062 1058 validate_repo_permissions(apiuser, repoid, repo, _perms)
1063 1059
1064 1060 # check if the regular user has at least fork permissions as well
1065 1061 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1066 1062 raise JSONRPCForbidden()
1067 1063
1068 1064 # check if user can set owner parameter
1069 1065 owner = validate_set_owner_permissions(apiuser, owner)
1070 1066
1071 1067 description = Optional.extract(description)
1072 1068 copy_permissions = Optional.extract(copy_permissions)
1073 1069 clone_uri = Optional.extract(clone_uri)
1074 1070 landing_commit_ref = Optional.extract(landing_rev)
1075 1071 private = Optional.extract(private)
1076 1072
1077 1073 schema = repo_schema.RepoSchema().bind(
1078 1074 repo_type_options=rhodecode.BACKENDS.keys(),
1079 1075 # user caller
1080 1076 user=apiuser)
1081 1077
1082 1078 try:
1083 1079 schema_data = schema.deserialize(dict(
1084 1080 repo_name=fork_name,
1085 1081 repo_type=repo.repo_type,
1086 1082 repo_owner=owner.username,
1087 1083 repo_description=description,
1088 1084 repo_landing_commit_ref=landing_commit_ref,
1089 1085 repo_clone_uri=clone_uri,
1090 1086 repo_private=private,
1091 1087 repo_copy_permissions=copy_permissions))
1092 1088 except validation_schema.Invalid as err:
1093 1089 raise JSONRPCValidationError(colander_exc=err)
1094 1090
1095 1091 try:
1096 1092 data = {
1097 1093 'fork_parent_id': repo.repo_id,
1098 1094
1099 1095 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1100 1096 'repo_name_full': schema_data['repo_name'],
1101 1097 'repo_group': schema_data['repo_group']['repo_group_id'],
1102 1098 'repo_type': schema_data['repo_type'],
1103 1099 'description': schema_data['repo_description'],
1104 1100 'private': schema_data['repo_private'],
1105 1101 'copy_permissions': schema_data['repo_copy_permissions'],
1106 1102 'landing_rev': schema_data['repo_landing_commit_ref'],
1107 1103 }
1108 1104
1109 1105 task = RepoModel().create_fork(data, cur_user=owner)
1110 1106 # no commit, it's done in RepoModel, or async via celery
1111 1107 from celery.result import BaseAsyncResult
1112 1108 task_id = None
1113 1109 if isinstance(task, BaseAsyncResult):
1114 1110 task_id = task.task_id
1115 1111 return {
1116 1112 'msg': 'Created fork of `%s` as `%s`' % (
1117 1113 repo.repo_name, schema_data['repo_name']),
1118 1114 'success': True, # cannot return the repo data here since fork
1119 1115 # can be done async
1120 1116 'task': task_id
1121 1117 }
1122 1118 except Exception:
1123 1119 log.exception(
1124 1120 u"Exception while trying to create fork %s",
1125 1121 schema_data['repo_name'])
1126 1122 raise JSONRPCError(
1127 1123 'failed to fork repository `%s` as `%s`' % (
1128 1124 repo_name, schema_data['repo_name']))
1129 1125
1130 1126
1131 1127 @jsonrpc_method()
1132 1128 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1133 1129 """
1134 1130 Deletes a repository.
1135 1131
1136 1132 * When the `forks` parameter is set it's possible to detach or delete
1137 1133 forks of deleted repository.
1138 1134
1139 1135 This command can only be run using an |authtoken| with admin
1140 1136 permissions on the |repo|.
1141 1137
1142 1138 :param apiuser: This is filled automatically from the |authtoken|.
1143 1139 :type apiuser: AuthUser
1144 1140 :param repoid: Set the repository name or repository ID.
1145 1141 :type repoid: str or int
1146 1142 :param forks: Set to `detach` or `delete` forks from the |repo|.
1147 1143 :type forks: Optional(str)
1148 1144
1149 1145 Example error output:
1150 1146
1151 1147 .. code-block:: bash
1152 1148
1153 1149 id : <id_given_in_input>
1154 1150 result: {
1155 1151 "msg": "Deleted repository `<reponame>`",
1156 1152 "success": true
1157 1153 }
1158 1154 error: null
1159 1155 """
1160 1156
1161 1157 repo = get_repo_or_error(repoid)
1162 1158 repo_name = repo.repo_name
1163 1159 if not has_superadmin_permission(apiuser):
1164 1160 _perms = ('repository.admin',)
1165 1161 validate_repo_permissions(apiuser, repoid, repo, _perms)
1166 1162
1167 1163 try:
1168 1164 handle_forks = Optional.extract(forks)
1169 1165 _forks_msg = ''
1170 1166 _forks = [f for f in repo.forks]
1171 1167 if handle_forks == 'detach':
1172 1168 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1173 1169 elif handle_forks == 'delete':
1174 1170 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1175 1171 elif _forks:
1176 1172 raise JSONRPCError(
1177 1173 'Cannot delete `%s` it still contains attached forks' %
1178 1174 (repo.repo_name,)
1179 1175 )
1180 1176 old_data = repo.get_api_data()
1181 1177 RepoModel().delete(repo, forks=forks)
1182 1178
1183 1179 repo = audit_logger.RepoWrap(repo_id=None,
1184 1180 repo_name=repo.repo_name)
1185 1181
1186 1182 audit_logger.store_api(
1187 1183 'repo.delete', action_data={'old_data': old_data},
1188 1184 user=apiuser, repo=repo)
1189 1185
1190 1186 ScmModel().mark_for_invalidation(repo_name, delete=True)
1191 1187 Session().commit()
1192 1188 return {
1193 1189 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1194 1190 'success': True
1195 1191 }
1196 1192 except Exception:
1197 1193 log.exception("Exception occurred while trying to delete repo")
1198 1194 raise JSONRPCError(
1199 1195 'failed to delete repository `%s`' % (repo_name,)
1200 1196 )
1201 1197
1202 1198
1203 1199 #TODO: marcink, change name ?
1204 1200 @jsonrpc_method()
1205 1201 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1206 1202 """
1207 1203 Invalidates the cache for the specified repository.
1208 1204
1209 1205 This command can only be run using an |authtoken| with admin rights to
1210 1206 the specified repository.
1211 1207
1212 1208 This command takes the following options:
1213 1209
1214 1210 :param apiuser: This is filled automatically from |authtoken|.
1215 1211 :type apiuser: AuthUser
1216 1212 :param repoid: Sets the repository name or repository ID.
1217 1213 :type repoid: str or int
1218 1214 :param delete_keys: This deletes the invalidated keys instead of
1219 1215 just flagging them.
1220 1216 :type delete_keys: Optional(``True`` | ``False``)
1221 1217
1222 1218 Example output:
1223 1219
1224 1220 .. code-block:: bash
1225 1221
1226 1222 id : <id_given_in_input>
1227 1223 result : {
1228 1224 'msg': Cache for repository `<repository name>` was invalidated,
1229 1225 'repository': <repository name>
1230 1226 }
1231 1227 error : null
1232 1228
1233 1229 Example error output:
1234 1230
1235 1231 .. code-block:: bash
1236 1232
1237 1233 id : <id_given_in_input>
1238 1234 result : null
1239 1235 error : {
1240 1236 'Error occurred during cache invalidation action'
1241 1237 }
1242 1238
1243 1239 """
1244 1240
1245 1241 repo = get_repo_or_error(repoid)
1246 1242 if not has_superadmin_permission(apiuser):
1247 1243 _perms = ('repository.admin', 'repository.write',)
1248 1244 validate_repo_permissions(apiuser, repoid, repo, _perms)
1249 1245
1250 1246 delete = Optional.extract(delete_keys)
1251 1247 try:
1252 1248 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1253 1249 return {
1254 1250 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1255 1251 'repository': repo.repo_name
1256 1252 }
1257 1253 except Exception:
1258 1254 log.exception(
1259 1255 "Exception occurred while trying to invalidate repo cache")
1260 1256 raise JSONRPCError(
1261 1257 'Error occurred during cache invalidation action'
1262 1258 )
1263 1259
1264 1260
1265 1261 #TODO: marcink, change name ?
1266 1262 @jsonrpc_method()
1267 1263 def lock(request, apiuser, repoid, locked=Optional(None),
1268 1264 userid=Optional(OAttr('apiuser'))):
1269 1265 """
1270 1266 Sets the lock state of the specified |repo| by the given user.
1271 1267 From more information, see :ref:`repo-locking`.
1272 1268
1273 1269 * If the ``userid`` option is not set, the repository is locked to the
1274 1270 user who called the method.
1275 1271 * If the ``locked`` parameter is not set, the current lock state of the
1276 1272 repository is displayed.
1277 1273
1278 1274 This command can only be run using an |authtoken| with admin rights to
1279 1275 the specified repository.
1280 1276
1281 1277 This command takes the following options:
1282 1278
1283 1279 :param apiuser: This is filled automatically from the |authtoken|.
1284 1280 :type apiuser: AuthUser
1285 1281 :param repoid: Sets the repository name or repository ID.
1286 1282 :type repoid: str or int
1287 1283 :param locked: Sets the lock state.
1288 1284 :type locked: Optional(``True`` | ``False``)
1289 1285 :param userid: Set the repository lock to this user.
1290 1286 :type userid: Optional(str or int)
1291 1287
1292 1288 Example error output:
1293 1289
1294 1290 .. code-block:: bash
1295 1291
1296 1292 id : <id_given_in_input>
1297 1293 result : {
1298 1294 'repo': '<reponame>',
1299 1295 'locked': <bool: lock state>,
1300 1296 'locked_since': <int: lock timestamp>,
1301 1297 'locked_by': <username of person who made the lock>,
1302 1298 'lock_reason': <str: reason for locking>,
1303 1299 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1304 1300 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1305 1301 or
1306 1302 'msg': 'Repo `<repository name>` not locked.'
1307 1303 or
1308 1304 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1309 1305 }
1310 1306 error : null
1311 1307
1312 1308 Example error output:
1313 1309
1314 1310 .. code-block:: bash
1315 1311
1316 1312 id : <id_given_in_input>
1317 1313 result : null
1318 1314 error : {
1319 1315 'Error occurred locking repository `<reponame>`'
1320 1316 }
1321 1317 """
1322 1318
1323 1319 repo = get_repo_or_error(repoid)
1324 1320 if not has_superadmin_permission(apiuser):
1325 1321 # check if we have at least write permission for this repo !
1326 1322 _perms = ('repository.admin', 'repository.write',)
1327 1323 validate_repo_permissions(apiuser, repoid, repo, _perms)
1328 1324
1329 1325 # make sure normal user does not pass someone else userid,
1330 1326 # he is not allowed to do that
1331 1327 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1332 1328 raise JSONRPCError('userid is not the same as your user')
1333 1329
1334 1330 if isinstance(userid, Optional):
1335 1331 userid = apiuser.user_id
1336 1332
1337 1333 user = get_user_or_error(userid)
1338 1334
1339 1335 if isinstance(locked, Optional):
1340 1336 lockobj = repo.locked
1341 1337
1342 1338 if lockobj[0] is None:
1343 1339 _d = {
1344 1340 'repo': repo.repo_name,
1345 1341 'locked': False,
1346 1342 'locked_since': None,
1347 1343 'locked_by': None,
1348 1344 'lock_reason': None,
1349 1345 'lock_state_changed': False,
1350 1346 'msg': 'Repo `%s` not locked.' % repo.repo_name
1351 1347 }
1352 1348 return _d
1353 1349 else:
1354 1350 _user_id, _time, _reason = lockobj
1355 1351 lock_user = get_user_or_error(userid)
1356 1352 _d = {
1357 1353 'repo': repo.repo_name,
1358 1354 'locked': True,
1359 1355 'locked_since': _time,
1360 1356 'locked_by': lock_user.username,
1361 1357 'lock_reason': _reason,
1362 1358 'lock_state_changed': False,
1363 1359 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1364 1360 % (repo.repo_name, lock_user.username,
1365 1361 json.dumps(time_to_datetime(_time))))
1366 1362 }
1367 1363 return _d
1368 1364
1369 1365 # force locked state through a flag
1370 1366 else:
1371 1367 locked = str2bool(locked)
1372 1368 lock_reason = Repository.LOCK_API
1373 1369 try:
1374 1370 if locked:
1375 1371 lock_time = time.time()
1376 1372 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1377 1373 else:
1378 1374 lock_time = None
1379 1375 Repository.unlock(repo)
1380 1376 _d = {
1381 1377 'repo': repo.repo_name,
1382 1378 'locked': locked,
1383 1379 'locked_since': lock_time,
1384 1380 'locked_by': user.username,
1385 1381 'lock_reason': lock_reason,
1386 1382 'lock_state_changed': True,
1387 1383 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1388 1384 % (user.username, repo.repo_name, locked))
1389 1385 }
1390 1386 return _d
1391 1387 except Exception:
1392 1388 log.exception(
1393 1389 "Exception occurred while trying to lock repository")
1394 1390 raise JSONRPCError(
1395 1391 'Error occurred locking repository `%s`' % repo.repo_name
1396 1392 )
1397 1393
1398 1394
1399 1395 @jsonrpc_method()
1400 1396 def comment_commit(
1401 1397 request, apiuser, repoid, commit_id, message, status=Optional(None),
1402 1398 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1403 1399 resolves_comment_id=Optional(None),
1404 1400 userid=Optional(OAttr('apiuser'))):
1405 1401 """
1406 1402 Set a commit comment, and optionally change the status of the commit.
1407 1403
1408 1404 :param apiuser: This is filled automatically from the |authtoken|.
1409 1405 :type apiuser: AuthUser
1410 1406 :param repoid: Set the repository name or repository ID.
1411 1407 :type repoid: str or int
1412 1408 :param commit_id: Specify the commit_id for which to set a comment.
1413 1409 :type commit_id: str
1414 1410 :param message: The comment text.
1415 1411 :type message: str
1416 1412 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1417 1413 'approved', 'rejected', 'under_review'
1418 1414 :type status: str
1419 1415 :param comment_type: Comment type, one of: 'note', 'todo'
1420 1416 :type comment_type: Optional(str), default: 'note'
1421 1417 :param userid: Set the user name of the comment creator.
1422 1418 :type userid: Optional(str or int)
1423 1419
1424 1420 Example error output:
1425 1421
1426 1422 .. code-block:: bash
1427 1423
1428 1424 {
1429 1425 "id" : <id_given_in_input>,
1430 1426 "result" : {
1431 1427 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1432 1428 "status_change": null or <status>,
1433 1429 "success": true
1434 1430 },
1435 1431 "error" : null
1436 1432 }
1437 1433
1438 1434 """
1439 1435 repo = get_repo_or_error(repoid)
1440 1436 if not has_superadmin_permission(apiuser):
1441 1437 _perms = ('repository.read', 'repository.write', 'repository.admin')
1442 1438 validate_repo_permissions(apiuser, repoid, repo, _perms)
1443 1439
1444 1440 try:
1445 1441 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1446 1442 except Exception as e:
1447 1443 log.exception('Failed to fetch commit')
1448 1444 raise JSONRPCError(e.message)
1449 1445
1450 1446 if isinstance(userid, Optional):
1451 1447 userid = apiuser.user_id
1452 1448
1453 1449 user = get_user_or_error(userid)
1454 1450 status = Optional.extract(status)
1455 1451 comment_type = Optional.extract(comment_type)
1456 1452 resolves_comment_id = Optional.extract(resolves_comment_id)
1457 1453
1458 1454 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1459 1455 if status and status not in allowed_statuses:
1460 1456 raise JSONRPCError('Bad status, must be on '
1461 1457 'of %s got %s' % (allowed_statuses, status,))
1462 1458
1463 1459 if resolves_comment_id:
1464 1460 comment = ChangesetComment.get(resolves_comment_id)
1465 1461 if not comment:
1466 1462 raise JSONRPCError(
1467 1463 'Invalid resolves_comment_id `%s` for this commit.'
1468 1464 % resolves_comment_id)
1469 1465 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1470 1466 raise JSONRPCError(
1471 1467 'Comment `%s` is wrong type for setting status to resolved.'
1472 1468 % resolves_comment_id)
1473 1469
1474 1470 try:
1475 1471 rc_config = SettingsModel().get_all_settings()
1476 1472 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1477 1473 status_change_label = ChangesetStatus.get_status_lbl(status)
1478 1474 comment = CommentsModel().create(
1479 1475 message, repo, user, commit_id=commit_id,
1480 1476 status_change=status_change_label,
1481 1477 status_change_type=status,
1482 1478 renderer=renderer,
1483 1479 comment_type=comment_type,
1484 1480 resolves_comment_id=resolves_comment_id
1485 1481 )
1486 1482 if status:
1487 1483 # also do a status change
1488 1484 try:
1489 1485 ChangesetStatusModel().set_status(
1490 1486 repo, status, user, comment, revision=commit_id,
1491 1487 dont_allow_on_closed_pull_request=True
1492 1488 )
1493 1489 except StatusChangeOnClosedPullRequestError:
1494 1490 log.exception(
1495 1491 "Exception occurred while trying to change repo commit status")
1496 1492 msg = ('Changing status on a changeset associated with '
1497 1493 'a closed pull request is not allowed')
1498 1494 raise JSONRPCError(msg)
1499 1495
1500 1496 Session().commit()
1501 1497 return {
1502 1498 'msg': (
1503 1499 'Commented on commit `%s` for repository `%s`' % (
1504 1500 comment.revision, repo.repo_name)),
1505 1501 'status_change': status,
1506 1502 'success': True,
1507 1503 }
1508 1504 except JSONRPCError:
1509 1505 # catch any inside errors, and re-raise them to prevent from
1510 1506 # below global catch to silence them
1511 1507 raise
1512 1508 except Exception:
1513 1509 log.exception("Exception occurred while trying to comment on commit")
1514 1510 raise JSONRPCError(
1515 1511 'failed to set comment on repository `%s`' % (repo.repo_name,)
1516 1512 )
1517 1513
1518 1514
1519 1515 @jsonrpc_method()
1520 1516 def grant_user_permission(request, apiuser, repoid, userid, perm):
1521 1517 """
1522 1518 Grant permissions for the specified user on the given repository,
1523 1519 or update existing permissions if found.
1524 1520
1525 1521 This command can only be run using an |authtoken| with admin
1526 1522 permissions on the |repo|.
1527 1523
1528 1524 :param apiuser: This is filled automatically from the |authtoken|.
1529 1525 :type apiuser: AuthUser
1530 1526 :param repoid: Set the repository name or repository ID.
1531 1527 :type repoid: str or int
1532 1528 :param userid: Set the user name.
1533 1529 :type userid: str
1534 1530 :param perm: Set the user permissions, using the following format
1535 1531 ``(repository.(none|read|write|admin))``
1536 1532 :type perm: str
1537 1533
1538 1534 Example output:
1539 1535
1540 1536 .. code-block:: bash
1541 1537
1542 1538 id : <id_given_in_input>
1543 1539 result: {
1544 1540 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1545 1541 "success": true
1546 1542 }
1547 1543 error: null
1548 1544 """
1549 1545
1550 1546 repo = get_repo_or_error(repoid)
1551 1547 user = get_user_or_error(userid)
1552 1548 perm = get_perm_or_error(perm)
1553 1549 if not has_superadmin_permission(apiuser):
1554 1550 _perms = ('repository.admin',)
1555 1551 validate_repo_permissions(apiuser, repoid, repo, _perms)
1556 1552
1557 1553 try:
1558 1554
1559 1555 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1560 1556
1561 1557 Session().commit()
1562 1558 return {
1563 1559 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1564 1560 perm.permission_name, user.username, repo.repo_name
1565 1561 ),
1566 1562 'success': True
1567 1563 }
1568 1564 except Exception:
1569 1565 log.exception(
1570 1566 "Exception occurred while trying edit permissions for repo")
1571 1567 raise JSONRPCError(
1572 1568 'failed to edit permission for user: `%s` in repo: `%s`' % (
1573 1569 userid, repoid
1574 1570 )
1575 1571 )
1576 1572
1577 1573
1578 1574 @jsonrpc_method()
1579 1575 def revoke_user_permission(request, apiuser, repoid, userid):
1580 1576 """
1581 1577 Revoke permission for a user on the specified repository.
1582 1578
1583 1579 This command can only be run using an |authtoken| with admin
1584 1580 permissions on the |repo|.
1585 1581
1586 1582 :param apiuser: This is filled automatically from the |authtoken|.
1587 1583 :type apiuser: AuthUser
1588 1584 :param repoid: Set the repository name or repository ID.
1589 1585 :type repoid: str or int
1590 1586 :param userid: Set the user name of revoked user.
1591 1587 :type userid: str or int
1592 1588
1593 1589 Example error output:
1594 1590
1595 1591 .. code-block:: bash
1596 1592
1597 1593 id : <id_given_in_input>
1598 1594 result: {
1599 1595 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1600 1596 "success": true
1601 1597 }
1602 1598 error: null
1603 1599 """
1604 1600
1605 1601 repo = get_repo_or_error(repoid)
1606 1602 user = get_user_or_error(userid)
1607 1603 if not has_superadmin_permission(apiuser):
1608 1604 _perms = ('repository.admin',)
1609 1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1610 1606
1611 1607 try:
1612 1608 RepoModel().revoke_user_permission(repo=repo, user=user)
1613 1609 Session().commit()
1614 1610 return {
1615 1611 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1616 1612 user.username, repo.repo_name
1617 1613 ),
1618 1614 'success': True
1619 1615 }
1620 1616 except Exception:
1621 1617 log.exception(
1622 1618 "Exception occurred while trying revoke permissions to repo")
1623 1619 raise JSONRPCError(
1624 1620 'failed to edit permission for user: `%s` in repo: `%s`' % (
1625 1621 userid, repoid
1626 1622 )
1627 1623 )
1628 1624
1629 1625
1630 1626 @jsonrpc_method()
1631 1627 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1632 1628 """
1633 1629 Grant permission for a user group on the specified repository,
1634 1630 or update existing permissions.
1635 1631
1636 1632 This command can only be run using an |authtoken| with admin
1637 1633 permissions on the |repo|.
1638 1634
1639 1635 :param apiuser: This is filled automatically from the |authtoken|.
1640 1636 :type apiuser: AuthUser
1641 1637 :param repoid: Set the repository name or repository ID.
1642 1638 :type repoid: str or int
1643 1639 :param usergroupid: Specify the ID of the user group.
1644 1640 :type usergroupid: str or int
1645 1641 :param perm: Set the user group permissions using the following
1646 1642 format: (repository.(none|read|write|admin))
1647 1643 :type perm: str
1648 1644
1649 1645 Example output:
1650 1646
1651 1647 .. code-block:: bash
1652 1648
1653 1649 id : <id_given_in_input>
1654 1650 result : {
1655 1651 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1656 1652 "success": true
1657 1653
1658 1654 }
1659 1655 error : null
1660 1656
1661 1657 Example error output:
1662 1658
1663 1659 .. code-block:: bash
1664 1660
1665 1661 id : <id_given_in_input>
1666 1662 result : null
1667 1663 error : {
1668 1664 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1669 1665 }
1670 1666
1671 1667 """
1672 1668
1673 1669 repo = get_repo_or_error(repoid)
1674 1670 perm = get_perm_or_error(perm)
1675 1671 if not has_superadmin_permission(apiuser):
1676 1672 _perms = ('repository.admin',)
1677 1673 validate_repo_permissions(apiuser, repoid, repo, _perms)
1678 1674
1679 1675 user_group = get_user_group_or_error(usergroupid)
1680 1676 if not has_superadmin_permission(apiuser):
1681 1677 # check if we have at least read permission for this user group !
1682 1678 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1683 1679 if not HasUserGroupPermissionAnyApi(*_perms)(
1684 1680 user=apiuser, user_group_name=user_group.users_group_name):
1685 1681 raise JSONRPCError(
1686 1682 'user group `%s` does not exist' % (usergroupid,))
1687 1683
1688 1684 try:
1689 1685 RepoModel().grant_user_group_permission(
1690 1686 repo=repo, group_name=user_group, perm=perm)
1691 1687
1692 1688 Session().commit()
1693 1689 return {
1694 1690 'msg': 'Granted perm: `%s` for user group: `%s` in '
1695 1691 'repo: `%s`' % (
1696 1692 perm.permission_name, user_group.users_group_name,
1697 1693 repo.repo_name
1698 1694 ),
1699 1695 'success': True
1700 1696 }
1701 1697 except Exception:
1702 1698 log.exception(
1703 1699 "Exception occurred while trying change permission on repo")
1704 1700 raise JSONRPCError(
1705 1701 'failed to edit permission for user group: `%s` in '
1706 1702 'repo: `%s`' % (
1707 1703 usergroupid, repo.repo_name
1708 1704 )
1709 1705 )
1710 1706
1711 1707
1712 1708 @jsonrpc_method()
1713 1709 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1714 1710 """
1715 1711 Revoke the permissions of a user group on a given repository.
1716 1712
1717 1713 This command can only be run using an |authtoken| with admin
1718 1714 permissions on the |repo|.
1719 1715
1720 1716 :param apiuser: This is filled automatically from the |authtoken|.
1721 1717 :type apiuser: AuthUser
1722 1718 :param repoid: Set the repository name or repository ID.
1723 1719 :type repoid: str or int
1724 1720 :param usergroupid: Specify the user group ID.
1725 1721 :type usergroupid: str or int
1726 1722
1727 1723 Example output:
1728 1724
1729 1725 .. code-block:: bash
1730 1726
1731 1727 id : <id_given_in_input>
1732 1728 result: {
1733 1729 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1734 1730 "success": true
1735 1731 }
1736 1732 error: null
1737 1733 """
1738 1734
1739 1735 repo = get_repo_or_error(repoid)
1740 1736 if not has_superadmin_permission(apiuser):
1741 1737 _perms = ('repository.admin',)
1742 1738 validate_repo_permissions(apiuser, repoid, repo, _perms)
1743 1739
1744 1740 user_group = get_user_group_or_error(usergroupid)
1745 1741 if not has_superadmin_permission(apiuser):
1746 1742 # check if we have at least read permission for this user group !
1747 1743 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1748 1744 if not HasUserGroupPermissionAnyApi(*_perms)(
1749 1745 user=apiuser, user_group_name=user_group.users_group_name):
1750 1746 raise JSONRPCError(
1751 1747 'user group `%s` does not exist' % (usergroupid,))
1752 1748
1753 1749 try:
1754 1750 RepoModel().revoke_user_group_permission(
1755 1751 repo=repo, group_name=user_group)
1756 1752
1757 1753 Session().commit()
1758 1754 return {
1759 1755 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1760 1756 user_group.users_group_name, repo.repo_name
1761 1757 ),
1762 1758 'success': True
1763 1759 }
1764 1760 except Exception:
1765 1761 log.exception("Exception occurred while trying revoke "
1766 1762 "user group permission on repo")
1767 1763 raise JSONRPCError(
1768 1764 'failed to edit permission for user group: `%s` in '
1769 1765 'repo: `%s`' % (
1770 1766 user_group.users_group_name, repo.repo_name
1771 1767 )
1772 1768 )
1773 1769
1774 1770
1775 1771 @jsonrpc_method()
1776 1772 def pull(request, apiuser, repoid):
1777 1773 """
1778 1774 Triggers a pull on the given repository from a remote location. You
1779 1775 can use this to keep remote repositories up-to-date.
1780 1776
1781 1777 This command can only be run using an |authtoken| with admin
1782 1778 rights to the specified repository. For more information,
1783 1779 see :ref:`config-token-ref`.
1784 1780
1785 1781 This command takes the following options:
1786 1782
1787 1783 :param apiuser: This is filled automatically from the |authtoken|.
1788 1784 :type apiuser: AuthUser
1789 1785 :param repoid: The repository name or repository ID.
1790 1786 :type repoid: str or int
1791 1787
1792 1788 Example output:
1793 1789
1794 1790 .. code-block:: bash
1795 1791
1796 1792 id : <id_given_in_input>
1797 1793 result : {
1798 1794 "msg": "Pulled from `<repository name>`"
1799 1795 "repository": "<repository name>"
1800 1796 }
1801 1797 error : null
1802 1798
1803 1799 Example error output:
1804 1800
1805 1801 .. code-block:: bash
1806 1802
1807 1803 id : <id_given_in_input>
1808 1804 result : null
1809 1805 error : {
1810 1806 "Unable to pull changes from `<reponame>`"
1811 1807 }
1812 1808
1813 1809 """
1814 1810
1815 1811 repo = get_repo_or_error(repoid)
1816 1812 if not has_superadmin_permission(apiuser):
1817 1813 _perms = ('repository.admin',)
1818 1814 validate_repo_permissions(apiuser, repoid, repo, _perms)
1819 1815
1820 1816 try:
1821 1817 ScmModel().pull_changes(repo.repo_name, apiuser.username)
1822 1818 return {
1823 1819 'msg': 'Pulled from `%s`' % repo.repo_name,
1824 1820 'repository': repo.repo_name
1825 1821 }
1826 1822 except Exception:
1827 1823 log.exception("Exception occurred while trying to "
1828 1824 "pull changes from remote location")
1829 1825 raise JSONRPCError(
1830 1826 'Unable to pull changes from `%s`' % repo.repo_name
1831 1827 )
1832 1828
1833 1829
1834 1830 @jsonrpc_method()
1835 1831 def strip(request, apiuser, repoid, revision, branch):
1836 1832 """
1837 1833 Strips the given revision from the specified repository.
1838 1834
1839 1835 * This will remove the revision and all of its decendants.
1840 1836
1841 1837 This command can only be run using an |authtoken| with admin rights to
1842 1838 the specified repository.
1843 1839
1844 1840 This command takes the following options:
1845 1841
1846 1842 :param apiuser: This is filled automatically from the |authtoken|.
1847 1843 :type apiuser: AuthUser
1848 1844 :param repoid: The repository name or repository ID.
1849 1845 :type repoid: str or int
1850 1846 :param revision: The revision you wish to strip.
1851 1847 :type revision: str
1852 1848 :param branch: The branch from which to strip the revision.
1853 1849 :type branch: str
1854 1850
1855 1851 Example output:
1856 1852
1857 1853 .. code-block:: bash
1858 1854
1859 1855 id : <id_given_in_input>
1860 1856 result : {
1861 1857 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1862 1858 "repository": "<repository name>"
1863 1859 }
1864 1860 error : null
1865 1861
1866 1862 Example error output:
1867 1863
1868 1864 .. code-block:: bash
1869 1865
1870 1866 id : <id_given_in_input>
1871 1867 result : null
1872 1868 error : {
1873 1869 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1874 1870 }
1875 1871
1876 1872 """
1877 1873
1878 1874 repo = get_repo_or_error(repoid)
1879 1875 if not has_superadmin_permission(apiuser):
1880 1876 _perms = ('repository.admin',)
1881 1877 validate_repo_permissions(apiuser, repoid, repo, _perms)
1882 1878
1883 1879 try:
1884 1880 ScmModel().strip(repo, revision, branch)
1885 1881 audit_logger.store_api(
1886 1882 'repo.commit.strip', action_data={'commit_id': revision},
1887 1883 repo=repo,
1888 1884 user=apiuser, commit=True)
1889 1885
1890 1886 return {
1891 1887 'msg': 'Stripped commit %s from repo `%s`' % (
1892 1888 revision, repo.repo_name),
1893 1889 'repository': repo.repo_name
1894 1890 }
1895 1891 except Exception:
1896 1892 log.exception("Exception while trying to strip")
1897 1893 raise JSONRPCError(
1898 1894 'Unable to strip commit %s from repo `%s`' % (
1899 1895 revision, repo.repo_name)
1900 1896 )
1901 1897
1902 1898
1903 1899 @jsonrpc_method()
1904 1900 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1905 1901 """
1906 1902 Returns all settings for a repository. If key is given it only returns the
1907 1903 setting identified by the key or null.
1908 1904
1909 1905 :param apiuser: This is filled automatically from the |authtoken|.
1910 1906 :type apiuser: AuthUser
1911 1907 :param repoid: The repository name or repository id.
1912 1908 :type repoid: str or int
1913 1909 :param key: Key of the setting to return.
1914 1910 :type: key: Optional(str)
1915 1911
1916 1912 Example output:
1917 1913
1918 1914 .. code-block:: bash
1919 1915
1920 1916 {
1921 1917 "error": null,
1922 1918 "id": 237,
1923 1919 "result": {
1924 1920 "extensions_largefiles": true,
1925 1921 "extensions_evolve": true,
1926 1922 "hooks_changegroup_push_logger": true,
1927 1923 "hooks_changegroup_repo_size": false,
1928 1924 "hooks_outgoing_pull_logger": true,
1929 1925 "phases_publish": "True",
1930 1926 "rhodecode_hg_use_rebase_for_merging": true,
1931 1927 "rhodecode_pr_merge_enabled": true,
1932 1928 "rhodecode_use_outdated_comments": true
1933 1929 }
1934 1930 }
1935 1931 """
1936 1932
1937 1933 # Restrict access to this api method to admins only.
1938 1934 if not has_superadmin_permission(apiuser):
1939 1935 raise JSONRPCForbidden()
1940 1936
1941 1937 try:
1942 1938 repo = get_repo_or_error(repoid)
1943 1939 settings_model = VcsSettingsModel(repo=repo)
1944 1940 settings = settings_model.get_global_settings()
1945 1941 settings.update(settings_model.get_repo_settings())
1946 1942
1947 1943 # If only a single setting is requested fetch it from all settings.
1948 1944 key = Optional.extract(key)
1949 1945 if key is not None:
1950 1946 settings = settings.get(key, None)
1951 1947 except Exception:
1952 1948 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1953 1949 log.exception(msg)
1954 1950 raise JSONRPCError(msg)
1955 1951
1956 1952 return settings
1957 1953
1958 1954
1959 1955 @jsonrpc_method()
1960 1956 def set_repo_settings(request, apiuser, repoid, settings):
1961 1957 """
1962 1958 Update repository settings. Returns true on success.
1963 1959
1964 1960 :param apiuser: This is filled automatically from the |authtoken|.
1965 1961 :type apiuser: AuthUser
1966 1962 :param repoid: The repository name or repository id.
1967 1963 :type repoid: str or int
1968 1964 :param settings: The new settings for the repository.
1969 1965 :type: settings: dict
1970 1966
1971 1967 Example output:
1972 1968
1973 1969 .. code-block:: bash
1974 1970
1975 1971 {
1976 1972 "error": null,
1977 1973 "id": 237,
1978 1974 "result": true
1979 1975 }
1980 1976 """
1981 1977 # Restrict access to this api method to admins only.
1982 1978 if not has_superadmin_permission(apiuser):
1983 1979 raise JSONRPCForbidden()
1984 1980
1985 1981 if type(settings) is not dict:
1986 1982 raise JSONRPCError('Settings have to be a JSON Object.')
1987 1983
1988 1984 try:
1989 1985 settings_model = VcsSettingsModel(repo=repoid)
1990 1986
1991 1987 # Merge global, repo and incoming settings.
1992 1988 new_settings = settings_model.get_global_settings()
1993 1989 new_settings.update(settings_model.get_repo_settings())
1994 1990 new_settings.update(settings)
1995 1991
1996 1992 # Update the settings.
1997 1993 inherit_global_settings = new_settings.get(
1998 1994 'inherit_global_settings', False)
1999 1995 settings_model.create_or_update_repo_settings(
2000 1996 new_settings, inherit_global_settings=inherit_global_settings)
2001 1997 Session().commit()
2002 1998 except Exception:
2003 1999 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2004 2000 log.exception(msg)
2005 2001 raise JSONRPCError(msg)
2006 2002
2007 2003 # Indicate success.
2008 2004 return True
2009 2005
2010 2006
2011 2007 @jsonrpc_method()
2012 2008 def maintenance(request, apiuser, repoid):
2013 2009 """
2014 2010 Triggers a maintenance on the given repository.
2015 2011
2016 2012 This command can only be run using an |authtoken| with admin
2017 2013 rights to the specified repository. For more information,
2018 2014 see :ref:`config-token-ref`.
2019 2015
2020 2016 This command takes the following options:
2021 2017
2022 2018 :param apiuser: This is filled automatically from the |authtoken|.
2023 2019 :type apiuser: AuthUser
2024 2020 :param repoid: The repository name or repository ID.
2025 2021 :type repoid: str or int
2026 2022
2027 2023 Example output:
2028 2024
2029 2025 .. code-block:: bash
2030 2026
2031 2027 id : <id_given_in_input>
2032 2028 result : {
2033 2029 "msg": "executed maintenance command",
2034 2030 "executed_actions": [
2035 2031 <action_message>, <action_message2>...
2036 2032 ],
2037 2033 "repository": "<repository name>"
2038 2034 }
2039 2035 error : null
2040 2036
2041 2037 Example error output:
2042 2038
2043 2039 .. code-block:: bash
2044 2040
2045 2041 id : <id_given_in_input>
2046 2042 result : null
2047 2043 error : {
2048 2044 "Unable to execute maintenance on `<reponame>`"
2049 2045 }
2050 2046
2051 2047 """
2052 2048
2053 2049 repo = get_repo_or_error(repoid)
2054 2050 if not has_superadmin_permission(apiuser):
2055 2051 _perms = ('repository.admin',)
2056 2052 validate_repo_permissions(apiuser, repoid, repo, _perms)
2057 2053
2058 2054 try:
2059 2055 maintenance = repo_maintenance.RepoMaintenance()
2060 2056 executed_actions = maintenance.execute(repo)
2061 2057
2062 2058 return {
2063 2059 'msg': 'executed maintenance command',
2064 2060 'executed_actions': executed_actions,
2065 2061 'repository': repo.repo_name
2066 2062 }
2067 2063 except Exception:
2068 2064 log.exception("Exception occurred while trying to run maintenance")
2069 2065 raise JSONRPCError(
2070 2066 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,1591 +1,1598 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24
25 25 import collections
26 26 import datetime
27 27 import itertools
28 28 import logging
29 29 import os
30 30 import time
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 36 from rhodecode.lib.vcs import connection
37 37 from rhodecode.lib.vcs.utils import author_name, author_email
38 38 from rhodecode.lib.vcs.conf import settings
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 44 RepositoryError)
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 FILEMODE_DEFAULT = 0100644
51 51 FILEMODE_EXECUTABLE = 0100755
52 52
53 53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 54 MergeResponse = collections.namedtuple(
55 55 'MergeResponse',
56 56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57 57
58 58
59 59 class MergeFailureReason(object):
60 60 """
61 61 Enumeration with all the reasons why the server side merge could fail.
62 62
63 63 DO NOT change the number of the reasons, as they may be stored in the
64 64 database.
65 65
66 66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 67 reasons.
68 68 """
69 69
70 70 # Everything went well.
71 71 NONE = 0
72 72
73 73 # An unexpected exception was raised. Check the logs for more details.
74 74 UNKNOWN = 1
75 75
76 76 # The merge was not successful, there are conflicts.
77 77 MERGE_FAILED = 2
78 78
79 79 # The merge succeeded but we could not push it to the target repository.
80 80 PUSH_FAILED = 3
81 81
82 82 # The specified target is not a head in the target repository.
83 83 TARGET_IS_NOT_HEAD = 4
84 84
85 85 # The source repository contains more branches than the target. Pushing
86 86 # the merge will create additional branches in the target.
87 87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 88
89 89 # The target reference has multiple heads. That does not allow to correctly
90 90 # identify the target location. This could only happen for mercurial
91 91 # branches.
92 92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 93
94 94 # The target repository is locked
95 95 TARGET_IS_LOCKED = 7
96 96
97 97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 98 # A involved commit could not be found.
99 99 _DEPRECATED_MISSING_COMMIT = 8
100 100
101 101 # The target repo reference is missing.
102 102 MISSING_TARGET_REF = 9
103 103
104 104 # The source repo reference is missing.
105 105 MISSING_SOURCE_REF = 10
106 106
107 107 # The merge was not successful, there are conflicts related to sub
108 108 # repositories.
109 109 SUBREPO_MERGE_FAILED = 11
110 110
111 111
112 112 class UpdateFailureReason(object):
113 113 """
114 114 Enumeration with all the reasons why the pull request update could fail.
115 115
116 116 DO NOT change the number of the reasons, as they may be stored in the
117 117 database.
118 118
119 119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 120 reasons.
121 121 """
122 122
123 123 # Everything went well.
124 124 NONE = 0
125 125
126 126 # An unexpected exception was raised. Check the logs for more details.
127 127 UNKNOWN = 1
128 128
129 129 # The pull request is up to date.
130 130 NO_CHANGE = 2
131 131
132 132 # The pull request has a reference type that is not supported for update.
133 133 WRONG_REF_TYPE = 3
134 134
135 135 # Update failed because the target reference is missing.
136 136 MISSING_TARGET_REF = 4
137 137
138 138 # Update failed because the source reference is missing.
139 139 MISSING_SOURCE_REF = 5
140 140
141 141
142 142 class BaseRepository(object):
143 143 """
144 144 Base Repository for final backends
145 145
146 146 .. attribute:: DEFAULT_BRANCH_NAME
147 147
148 148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149 149
150 150 .. attribute:: commit_ids
151 151
152 152 list of all available commit ids, in ascending order
153 153
154 154 .. attribute:: path
155 155
156 156 absolute path to the repository
157 157
158 158 .. attribute:: bookmarks
159 159
160 160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 161 there are no bookmarks or the backend implementation does not support
162 162 bookmarks.
163 163
164 164 .. attribute:: tags
165 165
166 166 Mapping from name to :term:`Commit ID` of the tag.
167 167
168 168 """
169 169
170 170 DEFAULT_BRANCH_NAME = None
171 171 DEFAULT_CONTACT = u"Unknown"
172 172 DEFAULT_DESCRIPTION = u"unknown"
173 173 EMPTY_COMMIT_ID = '0' * 40
174 174
175 175 path = None
176 176
177 177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 178 """
179 179 Initializes repository. Raises RepositoryError if repository could
180 180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 181 exists and ``create`` is set to True.
182 182
183 183 :param repo_path: local path of the repository
184 184 :param config: repository configuration
185 185 :param create=False: if set to True, would try to create repository.
186 186 :param src_url=None: if set, should be proper url from which repository
187 187 would be cloned; requires ``create`` parameter to be set to True -
188 188 raises RepositoryError if src_url is set and create evaluates to
189 189 False
190 190 """
191 191 raise NotImplementedError
192 192
193 193 def __repr__(self):
194 194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195 195
196 196 def __len__(self):
197 197 return self.count()
198 198
199 199 def __eq__(self, other):
200 200 same_instance = isinstance(other, self.__class__)
201 201 return same_instance and other.path == self.path
202 202
203 203 def __ne__(self, other):
204 204 return not self.__eq__(other)
205 205
206 206 @LazyProperty
207 207 def EMPTY_COMMIT(self):
208 208 return EmptyCommit(self.EMPTY_COMMIT_ID)
209 209
210 210 @LazyProperty
211 211 def alias(self):
212 212 for k, v in settings.BACKENDS.items():
213 213 if v.split('.')[-1] == str(self.__class__.__name__):
214 214 return k
215 215
216 216 @LazyProperty
217 217 def name(self):
218 218 return safe_unicode(os.path.basename(self.path))
219 219
220 220 @LazyProperty
221 221 def description(self):
222 222 raise NotImplementedError
223 223
224 224 def refs(self):
225 225 """
226 226 returns a `dict` with branches, bookmarks, tags, and closed_branches
227 227 for this repository
228 228 """
229 229 return dict(
230 230 branches=self.branches,
231 231 branches_closed=self.branches_closed,
232 232 tags=self.tags,
233 233 bookmarks=self.bookmarks
234 234 )
235 235
236 236 @LazyProperty
237 237 def branches(self):
238 238 """
239 239 A `dict` which maps branch names to commit ids.
240 240 """
241 241 raise NotImplementedError
242 242
243 243 @LazyProperty
244 244 def tags(self):
245 245 """
246 246 A `dict` which maps tags names to commit ids.
247 247 """
248 248 raise NotImplementedError
249 249
250 250 @LazyProperty
251 251 def size(self):
252 252 """
253 253 Returns combined size in bytes for all repository files
254 254 """
255 255 tip = self.get_commit()
256 256 return tip.size
257 257
258 258 def size_at_commit(self, commit_id):
259 259 commit = self.get_commit(commit_id)
260 260 return commit.size
261 261
262 262 def is_empty(self):
263 263 return not bool(self.commit_ids)
264 264
265 265 @staticmethod
266 266 def check_url(url, config):
267 267 """
268 268 Function will check given url and try to verify if it's a valid
269 269 link.
270 270 """
271 271 raise NotImplementedError
272 272
273 273 @staticmethod
274 274 def is_valid_repository(path):
275 275 """
276 276 Check if given `path` contains a valid repository of this backend
277 277 """
278 278 raise NotImplementedError
279 279
280 280 # ==========================================================================
281 281 # COMMITS
282 282 # ==========================================================================
283 283
284 284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
285 285 """
286 286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
287 287 are both None, most recent commit is returned.
288 288
289 289 :param pre_load: Optional. List of commit attributes to load.
290 290
291 291 :raises ``EmptyRepositoryError``: if there are no commits
292 292 """
293 293 raise NotImplementedError
294 294
295 295 def __iter__(self):
296 296 for commit_id in self.commit_ids:
297 297 yield self.get_commit(commit_id=commit_id)
298 298
299 299 def get_commits(
300 300 self, start_id=None, end_id=None, start_date=None, end_date=None,
301 301 branch_name=None, show_hidden=False, pre_load=None):
302 302 """
303 303 Returns iterator of `BaseCommit` objects from start to end
304 304 not inclusive. This should behave just like a list, ie. end is not
305 305 inclusive.
306 306
307 307 :param start_id: None or str, must be a valid commit id
308 308 :param end_id: None or str, must be a valid commit id
309 309 :param start_date:
310 310 :param end_date:
311 311 :param branch_name:
312 312 :param show_hidden:
313 313 :param pre_load:
314 314 """
315 315 raise NotImplementedError
316 316
317 317 def __getitem__(self, key):
318 318 """
319 319 Allows index based access to the commit objects of this repository.
320 320 """
321 321 pre_load = ["author", "branch", "date", "message", "parents"]
322 322 if isinstance(key, slice):
323 323 return self._get_range(key, pre_load)
324 324 return self.get_commit(commit_idx=key, pre_load=pre_load)
325 325
326 326 def _get_range(self, slice_obj, pre_load):
327 327 for commit_id in self.commit_ids.__getitem__(slice_obj):
328 328 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
329 329
330 330 def count(self):
331 331 return len(self.commit_ids)
332 332
333 333 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
334 334 """
335 335 Creates and returns a tag for the given ``commit_id``.
336 336
337 337 :param name: name for new tag
338 338 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
339 339 :param commit_id: commit id for which new tag would be created
340 340 :param message: message of the tag's commit
341 341 :param date: date of tag's commit
342 342
343 343 :raises TagAlreadyExistError: if tag with same name already exists
344 344 """
345 345 raise NotImplementedError
346 346
347 347 def remove_tag(self, name, user, message=None, date=None):
348 348 """
349 349 Removes tag with the given ``name``.
350 350
351 351 :param name: name of the tag to be removed
352 352 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
353 353 :param message: message of the tag's removal commit
354 354 :param date: date of tag's removal commit
355 355
356 356 :raises TagDoesNotExistError: if tag with given name does not exists
357 357 """
358 358 raise NotImplementedError
359 359
360 360 def get_diff(
361 361 self, commit1, commit2, path=None, ignore_whitespace=False,
362 362 context=3, path1=None):
363 363 """
364 364 Returns (git like) *diff*, as plain text. Shows changes introduced by
365 365 `commit2` since `commit1`.
366 366
367 367 :param commit1: Entry point from which diff is shown. Can be
368 368 ``self.EMPTY_COMMIT`` - in this case, patch showing all
369 369 the changes since empty state of the repository until `commit2`
370 370 :param commit2: Until which commit changes should be shown.
371 371 :param path: Can be set to a path of a file to create a diff of that
372 372 file. If `path1` is also set, this value is only associated to
373 373 `commit2`.
374 374 :param ignore_whitespace: If set to ``True``, would not show whitespace
375 375 changes. Defaults to ``False``.
376 376 :param context: How many lines before/after changed lines should be
377 377 shown. Defaults to ``3``.
378 378 :param path1: Can be set to a path to associate with `commit1`. This
379 379 parameter works only for backends which support diff generation for
380 380 different paths. Other backends will raise a `ValueError` if `path1`
381 381 is set and has a different value than `path`.
382 382 :param file_path: filter this diff by given path pattern
383 383 """
384 384 raise NotImplementedError
385 385
386 386 def strip(self, commit_id, branch=None):
387 387 """
388 388 Strip given commit_id from the repository
389 389 """
390 390 raise NotImplementedError
391 391
392 392 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
393 393 """
394 394 Return a latest common ancestor commit if one exists for this repo
395 395 `commit_id1` vs `commit_id2` from `repo2`.
396 396
397 397 :param commit_id1: Commit it from this repository to use as a
398 398 target for the comparison.
399 399 :param commit_id2: Source commit id to use for comparison.
400 400 :param repo2: Source repository to use for comparison.
401 401 """
402 402 raise NotImplementedError
403 403
404 404 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
405 405 """
406 406 Compare this repository's revision `commit_id1` with `commit_id2`.
407 407
408 408 Returns a tuple(commits, ancestor) that would be merged from
409 409 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
410 410 will be returned as ancestor.
411 411
412 412 :param commit_id1: Commit it from this repository to use as a
413 413 target for the comparison.
414 414 :param commit_id2: Source commit id to use for comparison.
415 415 :param repo2: Source repository to use for comparison.
416 416 :param merge: If set to ``True`` will do a merge compare which also
417 417 returns the common ancestor.
418 418 :param pre_load: Optional. List of commit attributes to load.
419 419 """
420 420 raise NotImplementedError
421 421
422 422 def merge(self, target_ref, source_repo, source_ref, workspace_id,
423 423 user_name='', user_email='', message='', dry_run=False,
424 424 use_rebase=False, close_branch=False):
425 425 """
426 426 Merge the revisions specified in `source_ref` from `source_repo`
427 427 onto the `target_ref` of this repository.
428 428
429 429 `source_ref` and `target_ref` are named tupls with the following
430 430 fields `type`, `name` and `commit_id`.
431 431
432 432 Returns a MergeResponse named tuple with the following fields
433 433 'possible', 'executed', 'source_commit', 'target_commit',
434 434 'merge_commit'.
435 435
436 436 :param target_ref: `target_ref` points to the commit on top of which
437 437 the `source_ref` should be merged.
438 438 :param source_repo: The repository that contains the commits to be
439 439 merged.
440 440 :param source_ref: `source_ref` points to the topmost commit from
441 441 the `source_repo` which should be merged.
442 442 :param workspace_id: `workspace_id` unique identifier.
443 443 :param user_name: Merge commit `user_name`.
444 444 :param user_email: Merge commit `user_email`.
445 445 :param message: Merge commit `message`.
446 446 :param dry_run: If `True` the merge will not take place.
447 447 :param use_rebase: If `True` commits from the source will be rebased
448 448 on top of the target instead of being merged.
449 449 :param close_branch: If `True` branch will be close before merging it
450 450 """
451 451 if dry_run:
452 452 message = message or 'dry_run_merge_message'
453 453 user_email = user_email or 'dry-run-merge@rhodecode.com'
454 454 user_name = user_name or 'Dry-Run User'
455 455 else:
456 456 if not user_name:
457 457 raise ValueError('user_name cannot be empty')
458 458 if not user_email:
459 459 raise ValueError('user_email cannot be empty')
460 460 if not message:
461 461 raise ValueError('message cannot be empty')
462 462
463 463 shadow_repository_path = self._maybe_prepare_merge_workspace(
464 464 workspace_id, target_ref)
465 465
466 466 try:
467 467 return self._merge_repo(
468 468 shadow_repository_path, target_ref, source_repo,
469 469 source_ref, message, user_name, user_email, dry_run=dry_run,
470 470 use_rebase=use_rebase, close_branch=close_branch)
471 471 except RepositoryError:
472 472 log.exception(
473 473 'Unexpected failure when running merge, dry-run=%s',
474 474 dry_run)
475 475 return MergeResponse(
476 476 False, False, None, MergeFailureReason.UNKNOWN)
477 477
478 478 def _merge_repo(self, shadow_repository_path, target_ref,
479 479 source_repo, source_ref, merge_message,
480 480 merger_name, merger_email, dry_run=False,
481 481 use_rebase=False, close_branch=False):
482 482 """Internal implementation of merge."""
483 483 raise NotImplementedError
484 484
485 485 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
486 486 """
487 487 Create the merge workspace.
488 488
489 489 :param workspace_id: `workspace_id` unique identifier.
490 490 """
491 491 raise NotImplementedError
492 492
493 493 def cleanup_merge_workspace(self, workspace_id):
494 494 """
495 495 Remove merge workspace.
496 496
497 497 This function MUST not fail in case there is no workspace associated to
498 498 the given `workspace_id`.
499 499
500 500 :param workspace_id: `workspace_id` unique identifier.
501 501 """
502 502 raise NotImplementedError
503 503
504 504 # ========== #
505 505 # COMMIT API #
506 506 # ========== #
507 507
508 508 @LazyProperty
509 509 def in_memory_commit(self):
510 510 """
511 511 Returns :class:`InMemoryCommit` object for this repository.
512 512 """
513 513 raise NotImplementedError
514 514
515 515 # ======================== #
516 516 # UTILITIES FOR SUBCLASSES #
517 517 # ======================== #
518 518
519 519 def _validate_diff_commits(self, commit1, commit2):
520 520 """
521 521 Validates that the given commits are related to this repository.
522 522
523 523 Intended as a utility for sub classes to have a consistent validation
524 524 of input parameters in methods like :meth:`get_diff`.
525 525 """
526 526 self._validate_commit(commit1)
527 527 self._validate_commit(commit2)
528 528 if (isinstance(commit1, EmptyCommit) and
529 529 isinstance(commit2, EmptyCommit)):
530 530 raise ValueError("Cannot compare two empty commits")
531 531
532 532 def _validate_commit(self, commit):
533 533 if not isinstance(commit, BaseCommit):
534 534 raise TypeError(
535 535 "%s is not of type BaseCommit" % repr(commit))
536 536 if commit.repository != self and not isinstance(commit, EmptyCommit):
537 537 raise ValueError(
538 538 "Commit %s must be a valid commit from this repository %s, "
539 539 "related to this repository instead %s." %
540 540 (commit, self, commit.repository))
541 541
542 542 def _validate_commit_id(self, commit_id):
543 543 if not isinstance(commit_id, basestring):
544 544 raise TypeError("commit_id must be a string value")
545 545
546 546 def _validate_commit_idx(self, commit_idx):
547 547 if not isinstance(commit_idx, (int, long)):
548 548 raise TypeError("commit_idx must be a numeric value")
549 549
550 550 def _validate_branch_name(self, branch_name):
551 551 if branch_name and branch_name not in self.branches_all:
552 552 msg = ("Branch %s not found in %s" % (branch_name, self))
553 553 raise BranchDoesNotExistError(msg)
554 554
555 555 #
556 556 # Supporting deprecated API parts
557 557 # TODO: johbo: consider to move this into a mixin
558 558 #
559 559
560 560 @property
561 561 def EMPTY_CHANGESET(self):
562 562 warnings.warn(
563 563 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
564 564 return self.EMPTY_COMMIT_ID
565 565
566 566 @property
567 567 def revisions(self):
568 568 warnings.warn("Use commits attribute instead", DeprecationWarning)
569 569 return self.commit_ids
570 570
571 571 @revisions.setter
572 572 def revisions(self, value):
573 573 warnings.warn("Use commits attribute instead", DeprecationWarning)
574 574 self.commit_ids = value
575 575
576 576 def get_changeset(self, revision=None, pre_load=None):
577 577 warnings.warn("Use get_commit instead", DeprecationWarning)
578 578 commit_id = None
579 579 commit_idx = None
580 580 if isinstance(revision, basestring):
581 581 commit_id = revision
582 582 else:
583 583 commit_idx = revision
584 584 return self.get_commit(
585 585 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
586 586
587 587 def get_changesets(
588 588 self, start=None, end=None, start_date=None, end_date=None,
589 589 branch_name=None, pre_load=None):
590 590 warnings.warn("Use get_commits instead", DeprecationWarning)
591 591 start_id = self._revision_to_commit(start)
592 592 end_id = self._revision_to_commit(end)
593 593 return self.get_commits(
594 594 start_id=start_id, end_id=end_id, start_date=start_date,
595 595 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
596 596
597 597 def _revision_to_commit(self, revision):
598 598 """
599 599 Translates a revision to a commit_id
600 600
601 601 Helps to support the old changeset based API which allows to use
602 602 commit ids and commit indices interchangeable.
603 603 """
604 604 if revision is None:
605 605 return revision
606 606
607 607 if isinstance(revision, basestring):
608 608 commit_id = revision
609 609 else:
610 610 commit_id = self.commit_ids[revision]
611 611 return commit_id
612 612
613 613 @property
614 614 def in_memory_changeset(self):
615 615 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
616 616 return self.in_memory_commit
617 617
618 618
619 619 class BaseCommit(object):
620 620 """
621 621 Each backend should implement it's commit representation.
622 622
623 623 **Attributes**
624 624
625 625 ``repository``
626 626 repository object within which commit exists
627 627
628 628 ``id``
629 629 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
630 630 just ``tip``.
631 631
632 632 ``raw_id``
633 633 raw commit representation (i.e. full 40 length sha for git
634 634 backend)
635 635
636 636 ``short_id``
637 637 shortened (if apply) version of ``raw_id``; it would be simple
638 638 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
639 639 as ``raw_id`` for subversion
640 640
641 641 ``idx``
642 642 commit index
643 643
644 644 ``files``
645 645 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
646 646
647 647 ``dirs``
648 648 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
649 649
650 650 ``nodes``
651 651 combined list of ``Node`` objects
652 652
653 653 ``author``
654 654 author of the commit, as unicode
655 655
656 656 ``message``
657 657 message of the commit, as unicode
658 658
659 659 ``parents``
660 660 list of parent commits
661 661
662 662 """
663 663
664 664 branch = None
665 665 """
666 666 Depending on the backend this should be set to the branch name of the
667 667 commit. Backends not supporting branches on commits should leave this
668 668 value as ``None``.
669 669 """
670 670
671 671 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
672 672 """
673 673 This template is used to generate a default prefix for repository archives
674 674 if no prefix has been specified.
675 675 """
676 676
677 677 def __str__(self):
678 678 return '<%s at %s:%s>' % (
679 679 self.__class__.__name__, self.idx, self.short_id)
680 680
681 681 def __repr__(self):
682 682 return self.__str__()
683 683
684 684 def __unicode__(self):
685 685 return u'%s:%s' % (self.idx, self.short_id)
686 686
687 687 def __eq__(self, other):
688 688 same_instance = isinstance(other, self.__class__)
689 689 return same_instance and self.raw_id == other.raw_id
690 690
691 691 def __json__(self):
692 692 parents = []
693 693 try:
694 694 for parent in self.parents:
695 695 parents.append({'raw_id': parent.raw_id})
696 696 except NotImplementedError:
697 697 # empty commit doesn't have parents implemented
698 698 pass
699 699
700 700 return {
701 701 'short_id': self.short_id,
702 702 'raw_id': self.raw_id,
703 703 'revision': self.idx,
704 704 'message': self.message,
705 705 'date': self.date,
706 706 'author': self.author,
707 707 'parents': parents,
708 708 'branch': self.branch
709 709 }
710 710
711 def _get_refs(self):
712 return {
713 'branches': [self.branch],
714 'bookmarks': getattr(self, 'bookmarks', []),
715 'tags': self.tags
716 }
717
711 718 @LazyProperty
712 719 def last(self):
713 720 """
714 721 ``True`` if this is last commit in repository, ``False``
715 722 otherwise; trying to access this attribute while there is no
716 723 commits would raise `EmptyRepositoryError`
717 724 """
718 725 if self.repository is None:
719 726 raise CommitError("Cannot check if it's most recent commit")
720 727 return self.raw_id == self.repository.commit_ids[-1]
721 728
722 729 @LazyProperty
723 730 def parents(self):
724 731 """
725 732 Returns list of parent commits.
726 733 """
727 734 raise NotImplementedError
728 735
729 736 @property
730 737 def merge(self):
731 738 """
732 739 Returns boolean if commit is a merge.
733 740 """
734 741 return len(self.parents) > 1
735 742
736 743 @LazyProperty
737 744 def children(self):
738 745 """
739 746 Returns list of child commits.
740 747 """
741 748 raise NotImplementedError
742 749
743 750 @LazyProperty
744 751 def id(self):
745 752 """
746 753 Returns string identifying this commit.
747 754 """
748 755 raise NotImplementedError
749 756
750 757 @LazyProperty
751 758 def raw_id(self):
752 759 """
753 760 Returns raw string identifying this commit.
754 761 """
755 762 raise NotImplementedError
756 763
757 764 @LazyProperty
758 765 def short_id(self):
759 766 """
760 767 Returns shortened version of ``raw_id`` attribute, as string,
761 768 identifying this commit, useful for presentation to users.
762 769 """
763 770 raise NotImplementedError
764 771
765 772 @LazyProperty
766 773 def idx(self):
767 774 """
768 775 Returns integer identifying this commit.
769 776 """
770 777 raise NotImplementedError
771 778
772 779 @LazyProperty
773 780 def committer(self):
774 781 """
775 782 Returns committer for this commit
776 783 """
777 784 raise NotImplementedError
778 785
779 786 @LazyProperty
780 787 def committer_name(self):
781 788 """
782 789 Returns committer name for this commit
783 790 """
784 791
785 792 return author_name(self.committer)
786 793
787 794 @LazyProperty
788 795 def committer_email(self):
789 796 """
790 797 Returns committer email address for this commit
791 798 """
792 799
793 800 return author_email(self.committer)
794 801
795 802 @LazyProperty
796 803 def author(self):
797 804 """
798 805 Returns author for this commit
799 806 """
800 807
801 808 raise NotImplementedError
802 809
803 810 @LazyProperty
804 811 def author_name(self):
805 812 """
806 813 Returns author name for this commit
807 814 """
808 815
809 816 return author_name(self.author)
810 817
811 818 @LazyProperty
812 819 def author_email(self):
813 820 """
814 821 Returns author email address for this commit
815 822 """
816 823
817 824 return author_email(self.author)
818 825
819 826 def get_file_mode(self, path):
820 827 """
821 828 Returns stat mode of the file at `path`.
822 829 """
823 830 raise NotImplementedError
824 831
825 832 def is_link(self, path):
826 833 """
827 834 Returns ``True`` if given `path` is a symlink
828 835 """
829 836 raise NotImplementedError
830 837
831 838 def get_file_content(self, path):
832 839 """
833 840 Returns content of the file at the given `path`.
834 841 """
835 842 raise NotImplementedError
836 843
837 844 def get_file_size(self, path):
838 845 """
839 846 Returns size of the file at the given `path`.
840 847 """
841 848 raise NotImplementedError
842 849
843 850 def get_file_commit(self, path, pre_load=None):
844 851 """
845 852 Returns last commit of the file at the given `path`.
846 853
847 854 :param pre_load: Optional. List of commit attributes to load.
848 855 """
849 856 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
850 857 if not commits:
851 858 raise RepositoryError(
852 859 'Failed to fetch history for path {}. '
853 860 'Please check if such path exists in your repository'.format(
854 861 path))
855 862 return commits[0]
856 863
857 864 def get_file_history(self, path, limit=None, pre_load=None):
858 865 """
859 866 Returns history of file as reversed list of :class:`BaseCommit`
860 867 objects for which file at given `path` has been modified.
861 868
862 869 :param limit: Optional. Allows to limit the size of the returned
863 870 history. This is intended as a hint to the underlying backend, so
864 871 that it can apply optimizations depending on the limit.
865 872 :param pre_load: Optional. List of commit attributes to load.
866 873 """
867 874 raise NotImplementedError
868 875
869 876 def get_file_annotate(self, path, pre_load=None):
870 877 """
871 878 Returns a generator of four element tuples with
872 879 lineno, sha, commit lazy loader and line
873 880
874 881 :param pre_load: Optional. List of commit attributes to load.
875 882 """
876 883 raise NotImplementedError
877 884
878 885 def get_nodes(self, path):
879 886 """
880 887 Returns combined ``DirNode`` and ``FileNode`` objects list representing
881 888 state of commit at the given ``path``.
882 889
883 890 :raises ``CommitError``: if node at the given ``path`` is not
884 891 instance of ``DirNode``
885 892 """
886 893 raise NotImplementedError
887 894
888 895 def get_node(self, path):
889 896 """
890 897 Returns ``Node`` object from the given ``path``.
891 898
892 899 :raises ``NodeDoesNotExistError``: if there is no node at the given
893 900 ``path``
894 901 """
895 902 raise NotImplementedError
896 903
897 904 def get_largefile_node(self, path):
898 905 """
899 906 Returns the path to largefile from Mercurial/Git-lfs storage.
900 907 or None if it's not a largefile node
901 908 """
902 909 return None
903 910
904 911 def archive_repo(self, file_path, kind='tgz', subrepos=None,
905 912 prefix=None, write_metadata=False, mtime=None):
906 913 """
907 914 Creates an archive containing the contents of the repository.
908 915
909 916 :param file_path: path to the file which to create the archive.
910 917 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
911 918 :param prefix: name of root directory in archive.
912 919 Default is repository name and commit's short_id joined with dash:
913 920 ``"{repo_name}-{short_id}"``.
914 921 :param write_metadata: write a metadata file into archive.
915 922 :param mtime: custom modification time for archive creation, defaults
916 923 to time.time() if not given.
917 924
918 925 :raise VCSError: If prefix has a problem.
919 926 """
920 927 allowed_kinds = settings.ARCHIVE_SPECS.keys()
921 928 if kind not in allowed_kinds:
922 929 raise ImproperArchiveTypeError(
923 930 'Archive kind (%s) not supported use one of %s' %
924 931 (kind, allowed_kinds))
925 932
926 933 prefix = self._validate_archive_prefix(prefix)
927 934
928 935 mtime = mtime or time.mktime(self.date.timetuple())
929 936
930 937 file_info = []
931 938 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
932 939 for _r, _d, files in cur_rev.walk('/'):
933 940 for f in files:
934 941 f_path = os.path.join(prefix, f.path)
935 942 file_info.append(
936 943 (f_path, f.mode, f.is_link(), f.raw_bytes))
937 944
938 945 if write_metadata:
939 946 metadata = [
940 947 ('repo_name', self.repository.name),
941 948 ('rev', self.raw_id),
942 949 ('create_time', mtime),
943 950 ('branch', self.branch),
944 951 ('tags', ','.join(self.tags)),
945 952 ]
946 953 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
947 954 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
948 955
949 956 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
950 957
951 958 def _validate_archive_prefix(self, prefix):
952 959 if prefix is None:
953 960 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
954 961 repo_name=safe_str(self.repository.name),
955 962 short_id=self.short_id)
956 963 elif not isinstance(prefix, str):
957 964 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
958 965 elif prefix.startswith('/'):
959 966 raise VCSError("Prefix cannot start with leading slash")
960 967 elif prefix.strip() == '':
961 968 raise VCSError("Prefix cannot be empty")
962 969 return prefix
963 970
964 971 @LazyProperty
965 972 def root(self):
966 973 """
967 974 Returns ``RootNode`` object for this commit.
968 975 """
969 976 return self.get_node('')
970 977
971 978 def next(self, branch=None):
972 979 """
973 980 Returns next commit from current, if branch is gives it will return
974 981 next commit belonging to this branch
975 982
976 983 :param branch: show commits within the given named branch
977 984 """
978 985 indexes = xrange(self.idx + 1, self.repository.count())
979 986 return self._find_next(indexes, branch)
980 987
981 988 def prev(self, branch=None):
982 989 """
983 990 Returns previous commit from current, if branch is gives it will
984 991 return previous commit belonging to this branch
985 992
986 993 :param branch: show commit within the given named branch
987 994 """
988 995 indexes = xrange(self.idx - 1, -1, -1)
989 996 return self._find_next(indexes, branch)
990 997
991 998 def _find_next(self, indexes, branch=None):
992 999 if branch and self.branch != branch:
993 1000 raise VCSError('Branch option used on commit not belonging '
994 1001 'to that branch')
995 1002
996 1003 for next_idx in indexes:
997 1004 commit = self.repository.get_commit(commit_idx=next_idx)
998 1005 if branch and branch != commit.branch:
999 1006 continue
1000 1007 return commit
1001 1008 raise CommitDoesNotExistError
1002 1009
1003 1010 def diff(self, ignore_whitespace=True, context=3):
1004 1011 """
1005 1012 Returns a `Diff` object representing the change made by this commit.
1006 1013 """
1007 1014 parent = (
1008 1015 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1009 1016 diff = self.repository.get_diff(
1010 1017 parent, self,
1011 1018 ignore_whitespace=ignore_whitespace,
1012 1019 context=context)
1013 1020 return diff
1014 1021
1015 1022 @LazyProperty
1016 1023 def added(self):
1017 1024 """
1018 1025 Returns list of added ``FileNode`` objects.
1019 1026 """
1020 1027 raise NotImplementedError
1021 1028
1022 1029 @LazyProperty
1023 1030 def changed(self):
1024 1031 """
1025 1032 Returns list of modified ``FileNode`` objects.
1026 1033 """
1027 1034 raise NotImplementedError
1028 1035
1029 1036 @LazyProperty
1030 1037 def removed(self):
1031 1038 """
1032 1039 Returns list of removed ``FileNode`` objects.
1033 1040 """
1034 1041 raise NotImplementedError
1035 1042
1036 1043 @LazyProperty
1037 1044 def size(self):
1038 1045 """
1039 1046 Returns total number of bytes from contents of all filenodes.
1040 1047 """
1041 1048 return sum((node.size for node in self.get_filenodes_generator()))
1042 1049
1043 1050 def walk(self, topurl=''):
1044 1051 """
1045 1052 Similar to os.walk method. Insted of filesystem it walks through
1046 1053 commit starting at given ``topurl``. Returns generator of tuples
1047 1054 (topnode, dirnodes, filenodes).
1048 1055 """
1049 1056 topnode = self.get_node(topurl)
1050 1057 if not topnode.is_dir():
1051 1058 return
1052 1059 yield (topnode, topnode.dirs, topnode.files)
1053 1060 for dirnode in topnode.dirs:
1054 1061 for tup in self.walk(dirnode.path):
1055 1062 yield tup
1056 1063
1057 1064 def get_filenodes_generator(self):
1058 1065 """
1059 1066 Returns generator that yields *all* file nodes.
1060 1067 """
1061 1068 for topnode, dirs, files in self.walk():
1062 1069 for node in files:
1063 1070 yield node
1064 1071
1065 1072 #
1066 1073 # Utilities for sub classes to support consistent behavior
1067 1074 #
1068 1075
1069 1076 def no_node_at_path(self, path):
1070 1077 return NodeDoesNotExistError(
1071 1078 u"There is no file nor directory at the given path: "
1072 1079 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1073 1080
1074 1081 def _fix_path(self, path):
1075 1082 """
1076 1083 Paths are stored without trailing slash so we need to get rid off it if
1077 1084 needed.
1078 1085 """
1079 1086 return path.rstrip('/')
1080 1087
1081 1088 #
1082 1089 # Deprecated API based on changesets
1083 1090 #
1084 1091
1085 1092 @property
1086 1093 def revision(self):
1087 1094 warnings.warn("Use idx instead", DeprecationWarning)
1088 1095 return self.idx
1089 1096
1090 1097 @revision.setter
1091 1098 def revision(self, value):
1092 1099 warnings.warn("Use idx instead", DeprecationWarning)
1093 1100 self.idx = value
1094 1101
1095 1102 def get_file_changeset(self, path):
1096 1103 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1097 1104 return self.get_file_commit(path)
1098 1105
1099 1106
1100 1107 class BaseChangesetClass(type):
1101 1108
1102 1109 def __instancecheck__(self, instance):
1103 1110 return isinstance(instance, BaseCommit)
1104 1111
1105 1112
1106 1113 class BaseChangeset(BaseCommit):
1107 1114
1108 1115 __metaclass__ = BaseChangesetClass
1109 1116
1110 1117 def __new__(cls, *args, **kwargs):
1111 1118 warnings.warn(
1112 1119 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1113 1120 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1114 1121
1115 1122
1116 1123 class BaseInMemoryCommit(object):
1117 1124 """
1118 1125 Represents differences between repository's state (most recent head) and
1119 1126 changes made *in place*.
1120 1127
1121 1128 **Attributes**
1122 1129
1123 1130 ``repository``
1124 1131 repository object for this in-memory-commit
1125 1132
1126 1133 ``added``
1127 1134 list of ``FileNode`` objects marked as *added*
1128 1135
1129 1136 ``changed``
1130 1137 list of ``FileNode`` objects marked as *changed*
1131 1138
1132 1139 ``removed``
1133 1140 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1134 1141 *removed*
1135 1142
1136 1143 ``parents``
1137 1144 list of :class:`BaseCommit` instances representing parents of
1138 1145 in-memory commit. Should always be 2-element sequence.
1139 1146
1140 1147 """
1141 1148
1142 1149 def __init__(self, repository):
1143 1150 self.repository = repository
1144 1151 self.added = []
1145 1152 self.changed = []
1146 1153 self.removed = []
1147 1154 self.parents = []
1148 1155
1149 1156 def add(self, *filenodes):
1150 1157 """
1151 1158 Marks given ``FileNode`` objects as *to be committed*.
1152 1159
1153 1160 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1154 1161 latest commit
1155 1162 :raises ``NodeAlreadyAddedError``: if node with same path is already
1156 1163 marked as *added*
1157 1164 """
1158 1165 # Check if not already marked as *added* first
1159 1166 for node in filenodes:
1160 1167 if node.path in (n.path for n in self.added):
1161 1168 raise NodeAlreadyAddedError(
1162 1169 "Such FileNode %s is already marked for addition"
1163 1170 % node.path)
1164 1171 for node in filenodes:
1165 1172 self.added.append(node)
1166 1173
1167 1174 def change(self, *filenodes):
1168 1175 """
1169 1176 Marks given ``FileNode`` objects to be *changed* in next commit.
1170 1177
1171 1178 :raises ``EmptyRepositoryError``: if there are no commits yet
1172 1179 :raises ``NodeAlreadyExistsError``: if node with same path is already
1173 1180 marked to be *changed*
1174 1181 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1175 1182 marked to be *removed*
1176 1183 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1177 1184 commit
1178 1185 :raises ``NodeNotChangedError``: if node hasn't really be changed
1179 1186 """
1180 1187 for node in filenodes:
1181 1188 if node.path in (n.path for n in self.removed):
1182 1189 raise NodeAlreadyRemovedError(
1183 1190 "Node at %s is already marked as removed" % node.path)
1184 1191 try:
1185 1192 self.repository.get_commit()
1186 1193 except EmptyRepositoryError:
1187 1194 raise EmptyRepositoryError(
1188 1195 "Nothing to change - try to *add* new nodes rather than "
1189 1196 "changing them")
1190 1197 for node in filenodes:
1191 1198 if node.path in (n.path for n in self.changed):
1192 1199 raise NodeAlreadyChangedError(
1193 1200 "Node at '%s' is already marked as changed" % node.path)
1194 1201 self.changed.append(node)
1195 1202
1196 1203 def remove(self, *filenodes):
1197 1204 """
1198 1205 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1199 1206 *removed* in next commit.
1200 1207
1201 1208 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1202 1209 be *removed*
1203 1210 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1204 1211 be *changed*
1205 1212 """
1206 1213 for node in filenodes:
1207 1214 if node.path in (n.path for n in self.removed):
1208 1215 raise NodeAlreadyRemovedError(
1209 1216 "Node is already marked to for removal at %s" % node.path)
1210 1217 if node.path in (n.path for n in self.changed):
1211 1218 raise NodeAlreadyChangedError(
1212 1219 "Node is already marked to be changed at %s" % node.path)
1213 1220 # We only mark node as *removed* - real removal is done by
1214 1221 # commit method
1215 1222 self.removed.append(node)
1216 1223
1217 1224 def reset(self):
1218 1225 """
1219 1226 Resets this instance to initial state (cleans ``added``, ``changed``
1220 1227 and ``removed`` lists).
1221 1228 """
1222 1229 self.added = []
1223 1230 self.changed = []
1224 1231 self.removed = []
1225 1232 self.parents = []
1226 1233
1227 1234 def get_ipaths(self):
1228 1235 """
1229 1236 Returns generator of paths from nodes marked as added, changed or
1230 1237 removed.
1231 1238 """
1232 1239 for node in itertools.chain(self.added, self.changed, self.removed):
1233 1240 yield node.path
1234 1241
1235 1242 def get_paths(self):
1236 1243 """
1237 1244 Returns list of paths from nodes marked as added, changed or removed.
1238 1245 """
1239 1246 return list(self.get_ipaths())
1240 1247
1241 1248 def check_integrity(self, parents=None):
1242 1249 """
1243 1250 Checks in-memory commit's integrity. Also, sets parents if not
1244 1251 already set.
1245 1252
1246 1253 :raises CommitError: if any error occurs (i.e.
1247 1254 ``NodeDoesNotExistError``).
1248 1255 """
1249 1256 if not self.parents:
1250 1257 parents = parents or []
1251 1258 if len(parents) == 0:
1252 1259 try:
1253 1260 parents = [self.repository.get_commit(), None]
1254 1261 except EmptyRepositoryError:
1255 1262 parents = [None, None]
1256 1263 elif len(parents) == 1:
1257 1264 parents += [None]
1258 1265 self.parents = parents
1259 1266
1260 1267 # Local parents, only if not None
1261 1268 parents = [p for p in self.parents if p]
1262 1269
1263 1270 # Check nodes marked as added
1264 1271 for p in parents:
1265 1272 for node in self.added:
1266 1273 try:
1267 1274 p.get_node(node.path)
1268 1275 except NodeDoesNotExistError:
1269 1276 pass
1270 1277 else:
1271 1278 raise NodeAlreadyExistsError(
1272 1279 "Node `%s` already exists at %s" % (node.path, p))
1273 1280
1274 1281 # Check nodes marked as changed
1275 1282 missing = set(self.changed)
1276 1283 not_changed = set(self.changed)
1277 1284 if self.changed and not parents:
1278 1285 raise NodeDoesNotExistError(str(self.changed[0].path))
1279 1286 for p in parents:
1280 1287 for node in self.changed:
1281 1288 try:
1282 1289 old = p.get_node(node.path)
1283 1290 missing.remove(node)
1284 1291 # if content actually changed, remove node from not_changed
1285 1292 if old.content != node.content:
1286 1293 not_changed.remove(node)
1287 1294 except NodeDoesNotExistError:
1288 1295 pass
1289 1296 if self.changed and missing:
1290 1297 raise NodeDoesNotExistError(
1291 1298 "Node `%s` marked as modified but missing in parents: %s"
1292 1299 % (node.path, parents))
1293 1300
1294 1301 if self.changed and not_changed:
1295 1302 raise NodeNotChangedError(
1296 1303 "Node `%s` wasn't actually changed (parents: %s)"
1297 1304 % (not_changed.pop().path, parents))
1298 1305
1299 1306 # Check nodes marked as removed
1300 1307 if self.removed and not parents:
1301 1308 raise NodeDoesNotExistError(
1302 1309 "Cannot remove node at %s as there "
1303 1310 "were no parents specified" % self.removed[0].path)
1304 1311 really_removed = set()
1305 1312 for p in parents:
1306 1313 for node in self.removed:
1307 1314 try:
1308 1315 p.get_node(node.path)
1309 1316 really_removed.add(node)
1310 1317 except CommitError:
1311 1318 pass
1312 1319 not_removed = set(self.removed) - really_removed
1313 1320 if not_removed:
1314 1321 # TODO: johbo: This code branch does not seem to be covered
1315 1322 raise NodeDoesNotExistError(
1316 1323 "Cannot remove node at %s from "
1317 1324 "following parents: %s" % (not_removed, parents))
1318 1325
1319 1326 def commit(
1320 1327 self, message, author, parents=None, branch=None, date=None,
1321 1328 **kwargs):
1322 1329 """
1323 1330 Performs in-memory commit (doesn't check workdir in any way) and
1324 1331 returns newly created :class:`BaseCommit`. Updates repository's
1325 1332 attribute `commits`.
1326 1333
1327 1334 .. note::
1328 1335
1329 1336 While overriding this method each backend's should call
1330 1337 ``self.check_integrity(parents)`` in the first place.
1331 1338
1332 1339 :param message: message of the commit
1333 1340 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1334 1341 :param parents: single parent or sequence of parents from which commit
1335 1342 would be derived
1336 1343 :param date: ``datetime.datetime`` instance. Defaults to
1337 1344 ``datetime.datetime.now()``.
1338 1345 :param branch: branch name, as string. If none given, default backend's
1339 1346 branch would be used.
1340 1347
1341 1348 :raises ``CommitError``: if any error occurs while committing
1342 1349 """
1343 1350 raise NotImplementedError
1344 1351
1345 1352
1346 1353 class BaseInMemoryChangesetClass(type):
1347 1354
1348 1355 def __instancecheck__(self, instance):
1349 1356 return isinstance(instance, BaseInMemoryCommit)
1350 1357
1351 1358
1352 1359 class BaseInMemoryChangeset(BaseInMemoryCommit):
1353 1360
1354 1361 __metaclass__ = BaseInMemoryChangesetClass
1355 1362
1356 1363 def __new__(cls, *args, **kwargs):
1357 1364 warnings.warn(
1358 1365 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1359 1366 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1360 1367
1361 1368
1362 1369 class EmptyCommit(BaseCommit):
1363 1370 """
1364 1371 An dummy empty commit. It's possible to pass hash when creating
1365 1372 an EmptyCommit
1366 1373 """
1367 1374
1368 1375 def __init__(
1369 1376 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1370 1377 message='', author='', date=None):
1371 1378 self._empty_commit_id = commit_id
1372 1379 # TODO: johbo: Solve idx parameter, default value does not make
1373 1380 # too much sense
1374 1381 self.idx = idx
1375 1382 self.message = message
1376 1383 self.author = author
1377 1384 self.date = date or datetime.datetime.fromtimestamp(0)
1378 1385 self.repository = repo
1379 1386 self.alias = alias
1380 1387
1381 1388 @LazyProperty
1382 1389 def raw_id(self):
1383 1390 """
1384 1391 Returns raw string identifying this commit, useful for web
1385 1392 representation.
1386 1393 """
1387 1394
1388 1395 return self._empty_commit_id
1389 1396
1390 1397 @LazyProperty
1391 1398 def branch(self):
1392 1399 if self.alias:
1393 1400 from rhodecode.lib.vcs.backends import get_backend
1394 1401 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1395 1402
1396 1403 @LazyProperty
1397 1404 def short_id(self):
1398 1405 return self.raw_id[:12]
1399 1406
1400 1407 @LazyProperty
1401 1408 def id(self):
1402 1409 return self.raw_id
1403 1410
1404 1411 def get_file_commit(self, path):
1405 1412 return self
1406 1413
1407 1414 def get_file_content(self, path):
1408 1415 return u''
1409 1416
1410 1417 def get_file_size(self, path):
1411 1418 return 0
1412 1419
1413 1420
1414 1421 class EmptyChangesetClass(type):
1415 1422
1416 1423 def __instancecheck__(self, instance):
1417 1424 return isinstance(instance, EmptyCommit)
1418 1425
1419 1426
1420 1427 class EmptyChangeset(EmptyCommit):
1421 1428
1422 1429 __metaclass__ = EmptyChangesetClass
1423 1430
1424 1431 def __new__(cls, *args, **kwargs):
1425 1432 warnings.warn(
1426 1433 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1427 1434 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1428 1435
1429 1436 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1430 1437 alias=None, revision=-1, message='', author='', date=None):
1431 1438 if requested_revision is not None:
1432 1439 warnings.warn(
1433 1440 "Parameter requested_revision not supported anymore",
1434 1441 DeprecationWarning)
1435 1442 super(EmptyChangeset, self).__init__(
1436 1443 commit_id=cs, repo=repo, alias=alias, idx=revision,
1437 1444 message=message, author=author, date=date)
1438 1445
1439 1446 @property
1440 1447 def revision(self):
1441 1448 warnings.warn("Use idx instead", DeprecationWarning)
1442 1449 return self.idx
1443 1450
1444 1451 @revision.setter
1445 1452 def revision(self, value):
1446 1453 warnings.warn("Use idx instead", DeprecationWarning)
1447 1454 self.idx = value
1448 1455
1449 1456
1450 1457 class EmptyRepository(BaseRepository):
1451 1458 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1452 1459 pass
1453 1460
1454 1461 def get_diff(self, *args, **kwargs):
1455 1462 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1456 1463 return GitDiff('')
1457 1464
1458 1465
1459 1466 class CollectionGenerator(object):
1460 1467
1461 1468 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1462 1469 self.repo = repo
1463 1470 self.commit_ids = commit_ids
1464 1471 # TODO: (oliver) this isn't currently hooked up
1465 1472 self.collection_size = None
1466 1473 self.pre_load = pre_load
1467 1474
1468 1475 def __len__(self):
1469 1476 if self.collection_size is not None:
1470 1477 return self.collection_size
1471 1478 return self.commit_ids.__len__()
1472 1479
1473 1480 def __iter__(self):
1474 1481 for commit_id in self.commit_ids:
1475 1482 # TODO: johbo: Mercurial passes in commit indices or commit ids
1476 1483 yield self._commit_factory(commit_id)
1477 1484
1478 1485 def _commit_factory(self, commit_id):
1479 1486 """
1480 1487 Allows backends to override the way commits are generated.
1481 1488 """
1482 1489 return self.repo.get_commit(commit_id=commit_id,
1483 1490 pre_load=self.pre_load)
1484 1491
1485 1492 def __getslice__(self, i, j):
1486 1493 """
1487 1494 Returns an iterator of sliced repository
1488 1495 """
1489 1496 commit_ids = self.commit_ids[i:j]
1490 1497 return self.__class__(
1491 1498 self.repo, commit_ids, pre_load=self.pre_load)
1492 1499
1493 1500 def __repr__(self):
1494 1501 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1495 1502
1496 1503
1497 1504 class Config(object):
1498 1505 """
1499 1506 Represents the configuration for a repository.
1500 1507
1501 1508 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1502 1509 standard library. It implements only the needed subset.
1503 1510 """
1504 1511
1505 1512 def __init__(self):
1506 1513 self._values = {}
1507 1514
1508 1515 def copy(self):
1509 1516 clone = Config()
1510 1517 for section, values in self._values.items():
1511 1518 clone._values[section] = values.copy()
1512 1519 return clone
1513 1520
1514 1521 def __repr__(self):
1515 1522 return '<Config(%s sections) at %s>' % (
1516 1523 len(self._values), hex(id(self)))
1517 1524
1518 1525 def items(self, section):
1519 1526 return self._values.get(section, {}).iteritems()
1520 1527
1521 1528 def get(self, section, option):
1522 1529 return self._values.get(section, {}).get(option)
1523 1530
1524 1531 def set(self, section, option, value):
1525 1532 section_values = self._values.setdefault(section, {})
1526 1533 section_values[option] = value
1527 1534
1528 1535 def clear_section(self, section):
1529 1536 self._values[section] = {}
1530 1537
1531 1538 def serialize(self):
1532 1539 """
1533 1540 Creates a list of three tuples (section, key, value) representing
1534 1541 this config object.
1535 1542 """
1536 1543 items = []
1537 1544 for section in self._values:
1538 1545 for option, value in self._values[section].items():
1539 1546 items.append(
1540 1547 (safe_str(section), safe_str(option), safe_str(value)))
1541 1548 return items
1542 1549
1543 1550
1544 1551 class Diff(object):
1545 1552 """
1546 1553 Represents a diff result from a repository backend.
1547 1554
1548 1555 Subclasses have to provide a backend specific value for
1549 1556 :attr:`_header_re` and :attr:`_meta_re`.
1550 1557 """
1551 1558 _meta_re = None
1552 1559 _header_re = None
1553 1560
1554 1561 def __init__(self, raw_diff):
1555 1562 self.raw = raw_diff
1556 1563
1557 1564 def chunks(self):
1558 1565 """
1559 1566 split the diff in chunks of separate --git a/file b/file chunks
1560 1567 to make diffs consistent we must prepend with \n, and make sure
1561 1568 we can detect last chunk as this was also has special rule
1562 1569 """
1563 1570
1564 1571 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1565 1572 header = diff_parts[0]
1566 1573
1567 1574 if self._meta_re:
1568 1575 match = self._meta_re.match(header)
1569 1576
1570 1577 chunks = diff_parts[1:]
1571 1578 total_chunks = len(chunks)
1572 1579
1573 1580 return (
1574 1581 DiffChunk(chunk, self, cur_chunk == total_chunks)
1575 1582 for cur_chunk, chunk in enumerate(chunks, start=1))
1576 1583
1577 1584
1578 1585 class DiffChunk(object):
1579 1586
1580 1587 def __init__(self, chunk, diff, last_chunk):
1581 1588 self._diff = diff
1582 1589
1583 1590 # since we split by \ndiff --git that part is lost from original diff
1584 1591 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1585 1592 if not last_chunk:
1586 1593 chunk += '\n'
1587 1594
1588 1595 match = self._diff._header_re.match(chunk)
1589 1596 self.header = match.groupdict()
1590 1597 self.diff = chunk[match.end():]
1591 1598 self.raw = chunk
General Comments 0
You need to be logged in to leave comments. Login now