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