##// END OF EJS Templates
path-filter: enable for quick search menu.
marcink -
r3871:95b389c1 default
parent child Browse files
Show More
@@ -1,2315 +1,2313 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.lib.vcs import RepositoryError
40 40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 45 ChangesetComment)
46 46 from rhodecode.model.repo import RepoModel
47 47 from rhodecode.model.scm import ScmModel, RepoList
48 48 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
49 49 from rhodecode.model import validation_schema
50 50 from rhodecode.model.validation_schema.schemas import repo_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 @jsonrpc_method()
56 56 def get_repo(request, apiuser, repoid, cache=Optional(True)):
57 57 """
58 58 Gets an existing repository by its name or repository_id.
59 59
60 60 The members section so the output returns users groups or users
61 61 associated with that repository.
62 62
63 63 This command can only be run using an |authtoken| with admin rights,
64 64 or users with at least read rights to the |repo|.
65 65
66 66 :param apiuser: This is filled automatically from the |authtoken|.
67 67 :type apiuser: AuthUser
68 68 :param repoid: The repository name or repository id.
69 69 :type repoid: str or int
70 70 :param cache: use the cached value for last changeset
71 71 :type: cache: Optional(bool)
72 72
73 73 Example output:
74 74
75 75 .. code-block:: bash
76 76
77 77 {
78 78 "error": null,
79 79 "id": <repo_id>,
80 80 "result": {
81 81 "clone_uri": null,
82 82 "created_on": "timestamp",
83 83 "description": "repo description",
84 84 "enable_downloads": false,
85 85 "enable_locking": false,
86 86 "enable_statistics": false,
87 87 "followers": [
88 88 {
89 89 "active": true,
90 90 "admin": false,
91 91 "api_key": "****************************************",
92 92 "api_keys": [
93 93 "****************************************"
94 94 ],
95 95 "email": "user@example.com",
96 96 "emails": [
97 97 "user@example.com"
98 98 ],
99 99 "extern_name": "rhodecode",
100 100 "extern_type": "rhodecode",
101 101 "firstname": "username",
102 102 "ip_addresses": [],
103 103 "language": null,
104 104 "last_login": "2015-09-16T17:16:35.854",
105 105 "lastname": "surname",
106 106 "user_id": <user_id>,
107 107 "username": "name"
108 108 }
109 109 ],
110 110 "fork_of": "parent-repo",
111 111 "landing_rev": [
112 112 "rev",
113 113 "tip"
114 114 ],
115 115 "last_changeset": {
116 116 "author": "User <user@example.com>",
117 117 "branch": "default",
118 118 "date": "timestamp",
119 119 "message": "last commit message",
120 120 "parents": [
121 121 {
122 122 "raw_id": "commit-id"
123 123 }
124 124 ],
125 125 "raw_id": "commit-id",
126 126 "revision": <revision number>,
127 127 "short_id": "short id"
128 128 },
129 129 "lock_reason": null,
130 130 "locked_by": null,
131 131 "locked_date": null,
132 132 "owner": "owner-name",
133 133 "permissions": [
134 134 {
135 135 "name": "super-admin-name",
136 136 "origin": "super-admin",
137 137 "permission": "repository.admin",
138 138 "type": "user"
139 139 },
140 140 {
141 141 "name": "owner-name",
142 142 "origin": "owner",
143 143 "permission": "repository.admin",
144 144 "type": "user"
145 145 },
146 146 {
147 147 "name": "user-group-name",
148 148 "origin": "permission",
149 149 "permission": "repository.write",
150 150 "type": "user_group"
151 151 }
152 152 ],
153 153 "private": true,
154 154 "repo_id": 676,
155 155 "repo_name": "user-group/repo-name",
156 156 "repo_type": "hg"
157 157 }
158 158 }
159 159 """
160 160
161 161 repo = get_repo_or_error(repoid)
162 162 cache = Optional.extract(cache)
163 163
164 164 include_secrets = False
165 165 if has_superadmin_permission(apiuser):
166 166 include_secrets = True
167 167 else:
168 168 # check if we have at least read permission for this repo !
169 169 _perms = (
170 170 'repository.admin', 'repository.write', 'repository.read',)
171 171 validate_repo_permissions(apiuser, repoid, repo, _perms)
172 172
173 173 permissions = []
174 174 for _user in repo.permissions():
175 175 user_data = {
176 176 'name': _user.username,
177 177 'permission': _user.permission,
178 178 'origin': get_origin(_user),
179 179 'type': "user",
180 180 }
181 181 permissions.append(user_data)
182 182
183 183 for _user_group in repo.permission_user_groups():
184 184 user_group_data = {
185 185 'name': _user_group.users_group_name,
186 186 'permission': _user_group.permission,
187 187 'origin': get_origin(_user_group),
188 188 'type': "user_group",
189 189 }
190 190 permissions.append(user_group_data)
191 191
192 192 following_users = [
193 193 user.user.get_api_data(include_secrets=include_secrets)
194 194 for user in repo.followers]
195 195
196 196 if not cache:
197 197 repo.update_commit_cache()
198 198 data = repo.get_api_data(include_secrets=include_secrets)
199 199 data['permissions'] = permissions
200 200 data['followers'] = following_users
201 201 return data
202 202
203 203
204 204 @jsonrpc_method()
205 205 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
206 206 """
207 207 Lists all existing repositories.
208 208
209 209 This command can only be run using an |authtoken| with admin rights,
210 210 or users with at least read rights to |repos|.
211 211
212 212 :param apiuser: This is filled automatically from the |authtoken|.
213 213 :type apiuser: AuthUser
214 214 :param root: specify root repository group to fetch repositories.
215 215 filters the returned repositories to be members of given root group.
216 216 :type root: Optional(None)
217 217 :param traverse: traverse given root into subrepositories. With this flag
218 218 set to False, it will only return top-level repositories from `root`.
219 219 if root is empty it will return just top-level repositories.
220 220 :type traverse: Optional(True)
221 221
222 222
223 223 Example output:
224 224
225 225 .. code-block:: bash
226 226
227 227 id : <id_given_in_input>
228 228 result: [
229 229 {
230 230 "repo_id" : "<repo_id>",
231 231 "repo_name" : "<reponame>"
232 232 "repo_type" : "<repo_type>",
233 233 "clone_uri" : "<clone_uri>",
234 234 "private": : "<bool>",
235 235 "created_on" : "<datetimecreated>",
236 236 "description" : "<description>",
237 237 "landing_rev": "<landing_rev>",
238 238 "owner": "<repo_owner>",
239 239 "fork_of": "<name_of_fork_parent>",
240 240 "enable_downloads": "<bool>",
241 241 "enable_locking": "<bool>",
242 242 "enable_statistics": "<bool>",
243 243 },
244 244 ...
245 245 ]
246 246 error: null
247 247 """
248 248
249 249 include_secrets = has_superadmin_permission(apiuser)
250 250 _perms = ('repository.read', 'repository.write', 'repository.admin',)
251 251 extras = {'user': apiuser}
252 252
253 253 root = Optional.extract(root)
254 254 traverse = Optional.extract(traverse, binary=True)
255 255
256 256 if root:
257 257 # verify parent existance, if it's empty return an error
258 258 parent = RepoGroup.get_by_group_name(root)
259 259 if not parent:
260 260 raise JSONRPCError(
261 261 'Root repository group `{}` does not exist'.format(root))
262 262
263 263 if traverse:
264 264 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
265 265 else:
266 266 repos = RepoModel().get_repos_for_root(root=parent)
267 267 else:
268 268 if traverse:
269 269 repos = RepoModel().get_all()
270 270 else:
271 271 # return just top-level
272 272 repos = RepoModel().get_repos_for_root(root=None)
273 273
274 274 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
275 275 return [repo.get_api_data(include_secrets=include_secrets)
276 276 for repo in repo_list]
277 277
278 278
279 279 @jsonrpc_method()
280 280 def get_repo_changeset(request, apiuser, repoid, revision,
281 281 details=Optional('basic')):
282 282 """
283 283 Returns information about a changeset.
284 284
285 285 Additionally parameters define the amount of details returned by
286 286 this function.
287 287
288 288 This command can only be run using an |authtoken| with admin rights,
289 289 or users with at least read rights to the |repo|.
290 290
291 291 :param apiuser: This is filled automatically from the |authtoken|.
292 292 :type apiuser: AuthUser
293 293 :param repoid: The repository name or repository id
294 294 :type repoid: str or int
295 295 :param revision: revision for which listing should be done
296 296 :type revision: str
297 297 :param details: details can be 'basic|extended|full' full gives diff
298 298 info details like the diff itself, and number of changed files etc.
299 299 :type details: Optional(str)
300 300
301 301 """
302 302 repo = get_repo_or_error(repoid)
303 303 if not has_superadmin_permission(apiuser):
304 304 _perms = (
305 305 'repository.admin', 'repository.write', 'repository.read',)
306 306 validate_repo_permissions(apiuser, repoid, repo, _perms)
307 307
308 308 changes_details = Optional.extract(details)
309 309 _changes_details_types = ['basic', 'extended', 'full']
310 310 if changes_details not in _changes_details_types:
311 311 raise JSONRPCError(
312 312 'ret_type must be one of %s' % (
313 313 ','.join(_changes_details_types)))
314 314
315 315 pre_load = ['author', 'branch', 'date', 'message', 'parents',
316 316 'status', '_commit', '_file_paths']
317 317
318 318 try:
319 319 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
320 320 except TypeError as e:
321 321 raise JSONRPCError(safe_str(e))
322 322 _cs_json = cs.__json__()
323 323 _cs_json['diff'] = build_commit_data(cs, changes_details)
324 324 if changes_details == 'full':
325 325 _cs_json['refs'] = cs._get_refs()
326 326 return _cs_json
327 327
328 328
329 329 @jsonrpc_method()
330 330 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
331 331 details=Optional('basic')):
332 332 """
333 333 Returns a set of commits limited by the number starting
334 334 from the `start_rev` option.
335 335
336 336 Additional parameters define the amount of details returned by this
337 337 function.
338 338
339 339 This command can only be run using an |authtoken| with admin rights,
340 340 or users with at least read rights to |repos|.
341 341
342 342 :param apiuser: This is filled automatically from the |authtoken|.
343 343 :type apiuser: AuthUser
344 344 :param repoid: The repository name or repository ID.
345 345 :type repoid: str or int
346 346 :param start_rev: The starting revision from where to get changesets.
347 347 :type start_rev: str
348 348 :param limit: Limit the number of commits to this amount
349 349 :type limit: str or int
350 350 :param details: Set the level of detail returned. Valid option are:
351 351 ``basic``, ``extended`` and ``full``.
352 352 :type details: Optional(str)
353 353
354 354 .. note::
355 355
356 356 Setting the parameter `details` to the value ``full`` is extensive
357 357 and returns details like the diff itself, and the number
358 358 of changed files.
359 359
360 360 """
361 361 repo = get_repo_or_error(repoid)
362 362 if not has_superadmin_permission(apiuser):
363 363 _perms = (
364 364 'repository.admin', 'repository.write', 'repository.read',)
365 365 validate_repo_permissions(apiuser, repoid, repo, _perms)
366 366
367 367 changes_details = Optional.extract(details)
368 368 _changes_details_types = ['basic', 'extended', 'full']
369 369 if changes_details not in _changes_details_types:
370 370 raise JSONRPCError(
371 371 'ret_type must be one of %s' % (
372 372 ','.join(_changes_details_types)))
373 373
374 374 limit = int(limit)
375 375 pre_load = ['author', 'branch', 'date', 'message', 'parents',
376 376 'status', '_commit', '_file_paths']
377 377
378 378 vcs_repo = repo.scm_instance()
379 379 # SVN needs a special case to distinguish its index and commit id
380 380 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
381 381 start_rev = vcs_repo.commit_ids[0]
382 382
383 383 try:
384 384 commits = vcs_repo.get_commits(
385 385 start_id=start_rev, pre_load=pre_load, translate_tags=False)
386 386 except TypeError as e:
387 387 raise JSONRPCError(safe_str(e))
388 388 except Exception:
389 389 log.exception('Fetching of commits failed')
390 390 raise JSONRPCError('Error occurred during commit fetching')
391 391
392 392 ret = []
393 393 for cnt, commit in enumerate(commits):
394 394 if cnt >= limit != -1:
395 395 break
396 396 _cs_json = commit.__json__()
397 397 _cs_json['diff'] = build_commit_data(commit, changes_details)
398 398 if changes_details == 'full':
399 399 _cs_json['refs'] = {
400 400 'branches': [commit.branch],
401 401 'bookmarks': getattr(commit, 'bookmarks', []),
402 402 'tags': commit.tags
403 403 }
404 404 ret.append(_cs_json)
405 405 return ret
406 406
407 407
408 408 @jsonrpc_method()
409 409 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
410 410 ret_type=Optional('all'), details=Optional('basic'),
411 411 max_file_bytes=Optional(None)):
412 412 """
413 413 Returns a list of nodes and children in a flat list for a given
414 414 path at given revision.
415 415
416 416 It's possible to specify ret_type to show only `files` or `dirs`.
417 417
418 418 This command can only be run using an |authtoken| with admin rights,
419 419 or users with at least read rights to |repos|.
420 420
421 421 :param apiuser: This is filled automatically from the |authtoken|.
422 422 :type apiuser: AuthUser
423 423 :param repoid: The repository name or repository ID.
424 424 :type repoid: str or int
425 425 :param revision: The revision for which listing should be done.
426 426 :type revision: str
427 427 :param root_path: The path from which to start displaying.
428 428 :type root_path: str
429 429 :param ret_type: Set the return type. Valid options are
430 430 ``all`` (default), ``files`` and ``dirs``.
431 431 :type ret_type: Optional(str)
432 432 :param details: Returns extended information about nodes, such as
433 433 md5, binary, and or content.
434 434 The valid options are ``basic`` and ``full``.
435 435 :type details: Optional(str)
436 436 :param max_file_bytes: Only return file content under this file size bytes
437 437 :type details: Optional(int)
438 438
439 439 Example output:
440 440
441 441 .. code-block:: bash
442 442
443 443 id : <id_given_in_input>
444 444 result: [
445 445 {
446 446 "binary": false,
447 447 "content": "File line",
448 448 "extension": "md",
449 449 "lines": 2,
450 450 "md5": "059fa5d29b19c0657e384749480f6422",
451 451 "mimetype": "text/x-minidsrc",
452 452 "name": "file.md",
453 453 "size": 580,
454 454 "type": "file"
455 455 },
456 456 ...
457 457 ]
458 458 error: null
459 459 """
460 460
461 461 repo = get_repo_or_error(repoid)
462 462 if not has_superadmin_permission(apiuser):
463 463 _perms = ('repository.admin', 'repository.write', 'repository.read',)
464 464 validate_repo_permissions(apiuser, repoid, repo, _perms)
465 465
466 466 ret_type = Optional.extract(ret_type)
467 467 details = Optional.extract(details)
468 468 _extended_types = ['basic', 'full']
469 469 if details not in _extended_types:
470 470 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
471 471 extended_info = False
472 472 content = False
473 473 if details == 'basic':
474 474 extended_info = True
475 475
476 476 if details == 'full':
477 477 extended_info = content = True
478 478
479 479 _map = {}
480 480 try:
481 481 # check if repo is not empty by any chance, skip quicker if it is.
482 482 _scm = repo.scm_instance()
483 483 if _scm.is_empty():
484 484 return []
485 485
486 486 _d, _f = ScmModel().get_nodes(
487 487 repo, revision, root_path, flat=False,
488 488 extended_info=extended_info, content=content,
489 489 max_file_bytes=max_file_bytes)
490 490 _map = {
491 491 'all': _d + _f,
492 492 'files': _f,
493 493 'dirs': _d,
494 494 }
495 495 return _map[ret_type]
496 496 except KeyError:
497 497 raise JSONRPCError(
498 498 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
499 499 except Exception:
500 500 log.exception("Exception occurred while trying to get repo nodes")
501 501 raise JSONRPCError(
502 502 'failed to get repo: `%s` nodes' % repo.repo_name
503 503 )
504 504
505 505
506 506 @jsonrpc_method()
507 507 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
508 508 max_file_bytes=Optional(None), details=Optional('basic'),
509 509 cache=Optional(True)):
510 510 """
511 511 Returns a single file from repository at given revision.
512 512
513 513 This command can only be run using an |authtoken| with admin rights,
514 514 or users with at least read rights to |repos|.
515 515
516 516 :param apiuser: This is filled automatically from the |authtoken|.
517 517 :type apiuser: AuthUser
518 518 :param repoid: The repository name or repository ID.
519 519 :type repoid: str or int
520 520 :param commit_id: The revision for which listing should be done.
521 521 :type commit_id: str
522 522 :param file_path: The path from which to start displaying.
523 523 :type file_path: str
524 524 :param details: Returns different set of information about nodes.
525 525 The valid options are ``minimal`` ``basic`` and ``full``.
526 526 :type details: Optional(str)
527 527 :param max_file_bytes: Only return file content under this file size bytes
528 528 :type max_file_bytes: Optional(int)
529 529 :param cache: Use internal caches for fetching files. If disabled fetching
530 530 files is slower but more memory efficient
531 531 :type cache: Optional(bool)
532 532
533 533 Example output:
534 534
535 535 .. code-block:: bash
536 536
537 537 id : <id_given_in_input>
538 538 result: {
539 539 "binary": false,
540 540 "extension": "py",
541 541 "lines": 35,
542 542 "content": "....",
543 543 "md5": "76318336366b0f17ee249e11b0c99c41",
544 544 "mimetype": "text/x-python",
545 545 "name": "python.py",
546 546 "size": 817,
547 547 "type": "file",
548 548 }
549 549 error: null
550 550 """
551 551
552 552 repo = get_repo_or_error(repoid)
553 553 if not has_superadmin_permission(apiuser):
554 554 _perms = ('repository.admin', 'repository.write', 'repository.read',)
555 555 validate_repo_permissions(apiuser, repoid, repo, _perms)
556 556
557 557 cache = Optional.extract(cache, binary=True)
558 558 details = Optional.extract(details)
559 559 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
560 560 if details not in _extended_types:
561 561 raise JSONRPCError(
562 562 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
563 563 extended_info = False
564 564 content = False
565 565
566 566 if details == 'minimal':
567 567 extended_info = False
568 568
569 569 elif details == 'basic':
570 570 extended_info = True
571 571
572 572 elif details == 'full':
573 573 extended_info = content = True
574 574
575 575 try:
576 576 # check if repo is not empty by any chance, skip quicker if it is.
577 577 _scm = repo.scm_instance()
578 578 if _scm.is_empty():
579 579 return None
580 580
581 581 node = ScmModel().get_node(
582 582 repo, commit_id, file_path, extended_info=extended_info,
583 583 content=content, max_file_bytes=max_file_bytes, cache=cache)
584 584 except NodeDoesNotExistError:
585 585 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
586 586 repo.repo_name, file_path, commit_id))
587 587 except Exception:
588 588 log.exception("Exception occurred while trying to get repo %s file",
589 589 repo.repo_name)
590 590 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
591 591 repo.repo_name, file_path))
592 592
593 593 return node
594 594
595 595
596 596 @jsonrpc_method()
597 597 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
598 598 """
599 599 Returns a list of tree nodes for path at given revision. This api is built
600 600 strictly for usage in full text search building, and shouldn't be consumed
601 601
602 602 This command can only be run using an |authtoken| with admin rights,
603 603 or users with at least read rights to |repos|.
604 604
605 605 """
606 606
607 607 repo = get_repo_or_error(repoid)
608 608 if not has_superadmin_permission(apiuser):
609 609 _perms = ('repository.admin', 'repository.write', 'repository.read',)
610 610 validate_repo_permissions(apiuser, repoid, repo, _perms)
611 611
612 612 repo_id = repo.repo_id
613 613 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
614 614 cache_on = cache_seconds > 0
615 615
616 616 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
617 617 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
618 618
619 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
620 condition=cache_on)
621 619 def compute_fts_tree(repo_id, commit_id, root_path, cache_ver):
622 620 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
623 621
624 622 try:
625 623 # check if repo is not empty by any chance, skip quicker if it is.
626 624 _scm = repo.scm_instance()
627 625 if _scm.is_empty():
628 626 return []
629 627 except RepositoryError:
630 628 log.exception("Exception occurred while trying to get repo nodes")
631 629 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
632 630
633 631 try:
634 632 # we need to resolve commit_id to a FULL sha for cache to work correctly.
635 633 # sending 'master' is a pointer that needs to be translated to current commit.
636 634 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
637 635 log.debug(
638 636 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
639 637 'with caching: %s[TTL: %ss]' % (
640 638 repo_id, commit_id, cache_on, cache_seconds or 0))
641 639
642 640 tree_files = compute_fts_tree(repo_id, commit_id, root_path, 'v1')
643 641 return tree_files
644 642
645 643 except Exception:
646 644 log.exception("Exception occurred while trying to get repo nodes")
647 645 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
648 646
649 647
650 648 @jsonrpc_method()
651 649 def get_repo_refs(request, apiuser, repoid):
652 650 """
653 651 Returns a dictionary of current references. It returns
654 652 bookmarks, branches, closed_branches, and tags for given repository
655 653
656 654 It's possible to specify ret_type to show only `files` or `dirs`.
657 655
658 656 This command can only be run using an |authtoken| with admin rights,
659 657 or users with at least read rights to |repos|.
660 658
661 659 :param apiuser: This is filled automatically from the |authtoken|.
662 660 :type apiuser: AuthUser
663 661 :param repoid: The repository name or repository ID.
664 662 :type repoid: str or int
665 663
666 664 Example output:
667 665
668 666 .. code-block:: bash
669 667
670 668 id : <id_given_in_input>
671 669 "result": {
672 670 "bookmarks": {
673 671 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
674 672 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
675 673 },
676 674 "branches": {
677 675 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
678 676 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
679 677 },
680 678 "branches_closed": {},
681 679 "tags": {
682 680 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
683 681 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
684 682 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
685 683 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
686 684 }
687 685 }
688 686 error: null
689 687 """
690 688
691 689 repo = get_repo_or_error(repoid)
692 690 if not has_superadmin_permission(apiuser):
693 691 _perms = ('repository.admin', 'repository.write', 'repository.read',)
694 692 validate_repo_permissions(apiuser, repoid, repo, _perms)
695 693
696 694 try:
697 695 # check if repo is not empty by any chance, skip quicker if it is.
698 696 vcs_instance = repo.scm_instance()
699 697 refs = vcs_instance.refs()
700 698 return refs
701 699 except Exception:
702 700 log.exception("Exception occurred while trying to get repo refs")
703 701 raise JSONRPCError(
704 702 'failed to get repo: `%s` references' % repo.repo_name
705 703 )
706 704
707 705
708 706 @jsonrpc_method()
709 707 def create_repo(
710 708 request, apiuser, repo_name, repo_type,
711 709 owner=Optional(OAttr('apiuser')),
712 710 description=Optional(''),
713 711 private=Optional(False),
714 712 clone_uri=Optional(None),
715 713 push_uri=Optional(None),
716 714 landing_rev=Optional('rev:tip'),
717 715 enable_statistics=Optional(False),
718 716 enable_locking=Optional(False),
719 717 enable_downloads=Optional(False),
720 718 copy_permissions=Optional(False)):
721 719 """
722 720 Creates a repository.
723 721
724 722 * If the repository name contains "/", repository will be created inside
725 723 a repository group or nested repository groups
726 724
727 725 For example "foo/bar/repo1" will create |repo| called "repo1" inside
728 726 group "foo/bar". You have to have permissions to access and write to
729 727 the last repository group ("bar" in this example)
730 728
731 729 This command can only be run using an |authtoken| with at least
732 730 permissions to create repositories, or write permissions to
733 731 parent repository groups.
734 732
735 733 :param apiuser: This is filled automatically from the |authtoken|.
736 734 :type apiuser: AuthUser
737 735 :param repo_name: Set the repository name.
738 736 :type repo_name: str
739 737 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
740 738 :type repo_type: str
741 739 :param owner: user_id or username
742 740 :type owner: Optional(str)
743 741 :param description: Set the repository description.
744 742 :type description: Optional(str)
745 743 :param private: set repository as private
746 744 :type private: bool
747 745 :param clone_uri: set clone_uri
748 746 :type clone_uri: str
749 747 :param push_uri: set push_uri
750 748 :type push_uri: str
751 749 :param landing_rev: <rev_type>:<rev>
752 750 :type landing_rev: str
753 751 :param enable_locking:
754 752 :type enable_locking: bool
755 753 :param enable_downloads:
756 754 :type enable_downloads: bool
757 755 :param enable_statistics:
758 756 :type enable_statistics: bool
759 757 :param copy_permissions: Copy permission from group in which the
760 758 repository is being created.
761 759 :type copy_permissions: bool
762 760
763 761
764 762 Example output:
765 763
766 764 .. code-block:: bash
767 765
768 766 id : <id_given_in_input>
769 767 result: {
770 768 "msg": "Created new repository `<reponame>`",
771 769 "success": true,
772 770 "task": "<celery task id or None if done sync>"
773 771 }
774 772 error: null
775 773
776 774
777 775 Example error output:
778 776
779 777 .. code-block:: bash
780 778
781 779 id : <id_given_in_input>
782 780 result : null
783 781 error : {
784 782 'failed to create repository `<repo_name>`'
785 783 }
786 784
787 785 """
788 786
789 787 owner = validate_set_owner_permissions(apiuser, owner)
790 788
791 789 description = Optional.extract(description)
792 790 copy_permissions = Optional.extract(copy_permissions)
793 791 clone_uri = Optional.extract(clone_uri)
794 792 push_uri = Optional.extract(push_uri)
795 793 landing_commit_ref = Optional.extract(landing_rev)
796 794
797 795 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
798 796 if isinstance(private, Optional):
799 797 private = defs.get('repo_private') or Optional.extract(private)
800 798 if isinstance(repo_type, Optional):
801 799 repo_type = defs.get('repo_type')
802 800 if isinstance(enable_statistics, Optional):
803 801 enable_statistics = defs.get('repo_enable_statistics')
804 802 if isinstance(enable_locking, Optional):
805 803 enable_locking = defs.get('repo_enable_locking')
806 804 if isinstance(enable_downloads, Optional):
807 805 enable_downloads = defs.get('repo_enable_downloads')
808 806
809 807 schema = repo_schema.RepoSchema().bind(
810 808 repo_type_options=rhodecode.BACKENDS.keys(),
811 809 repo_type=repo_type,
812 810 # user caller
813 811 user=apiuser)
814 812
815 813 try:
816 814 schema_data = schema.deserialize(dict(
817 815 repo_name=repo_name,
818 816 repo_type=repo_type,
819 817 repo_owner=owner.username,
820 818 repo_description=description,
821 819 repo_landing_commit_ref=landing_commit_ref,
822 820 repo_clone_uri=clone_uri,
823 821 repo_push_uri=push_uri,
824 822 repo_private=private,
825 823 repo_copy_permissions=copy_permissions,
826 824 repo_enable_statistics=enable_statistics,
827 825 repo_enable_downloads=enable_downloads,
828 826 repo_enable_locking=enable_locking))
829 827 except validation_schema.Invalid as err:
830 828 raise JSONRPCValidationError(colander_exc=err)
831 829
832 830 try:
833 831 data = {
834 832 'owner': owner,
835 833 'repo_name': schema_data['repo_group']['repo_name_without_group'],
836 834 'repo_name_full': schema_data['repo_name'],
837 835 'repo_group': schema_data['repo_group']['repo_group_id'],
838 836 'repo_type': schema_data['repo_type'],
839 837 'repo_description': schema_data['repo_description'],
840 838 'repo_private': schema_data['repo_private'],
841 839 'clone_uri': schema_data['repo_clone_uri'],
842 840 'push_uri': schema_data['repo_push_uri'],
843 841 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
844 842 'enable_statistics': schema_data['repo_enable_statistics'],
845 843 'enable_locking': schema_data['repo_enable_locking'],
846 844 'enable_downloads': schema_data['repo_enable_downloads'],
847 845 'repo_copy_permissions': schema_data['repo_copy_permissions'],
848 846 }
849 847
850 848 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
851 849 task_id = get_task_id(task)
852 850 # no commit, it's done in RepoModel, or async via celery
853 851 return {
854 852 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
855 853 'success': True, # cannot return the repo data here since fork
856 854 # can be done async
857 855 'task': task_id
858 856 }
859 857 except Exception:
860 858 log.exception(
861 859 u"Exception while trying to create the repository %s",
862 860 schema_data['repo_name'])
863 861 raise JSONRPCError(
864 862 'failed to create repository `%s`' % (schema_data['repo_name'],))
865 863
866 864
867 865 @jsonrpc_method()
868 866 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
869 867 description=Optional('')):
870 868 """
871 869 Adds an extra field to a repository.
872 870
873 871 This command can only be run using an |authtoken| with at least
874 872 write permissions to the |repo|.
875 873
876 874 :param apiuser: This is filled automatically from the |authtoken|.
877 875 :type apiuser: AuthUser
878 876 :param repoid: Set the repository name or repository id.
879 877 :type repoid: str or int
880 878 :param key: Create a unique field key for this repository.
881 879 :type key: str
882 880 :param label:
883 881 :type label: Optional(str)
884 882 :param description:
885 883 :type description: Optional(str)
886 884 """
887 885 repo = get_repo_or_error(repoid)
888 886 if not has_superadmin_permission(apiuser):
889 887 _perms = ('repository.admin',)
890 888 validate_repo_permissions(apiuser, repoid, repo, _perms)
891 889
892 890 label = Optional.extract(label) or key
893 891 description = Optional.extract(description)
894 892
895 893 field = RepositoryField.get_by_key_name(key, repo)
896 894 if field:
897 895 raise JSONRPCError('Field with key '
898 896 '`%s` exists for repo `%s`' % (key, repoid))
899 897
900 898 try:
901 899 RepoModel().add_repo_field(repo, key, field_label=label,
902 900 field_desc=description)
903 901 Session().commit()
904 902 return {
905 903 'msg': "Added new repository field `%s`" % (key,),
906 904 'success': True,
907 905 }
908 906 except Exception:
909 907 log.exception("Exception occurred while trying to add field to repo")
910 908 raise JSONRPCError(
911 909 'failed to create new field for repository `%s`' % (repoid,))
912 910
913 911
914 912 @jsonrpc_method()
915 913 def remove_field_from_repo(request, apiuser, repoid, key):
916 914 """
917 915 Removes an extra field from a repository.
918 916
919 917 This command can only be run using an |authtoken| with at least
920 918 write permissions to the |repo|.
921 919
922 920 :param apiuser: This is filled automatically from the |authtoken|.
923 921 :type apiuser: AuthUser
924 922 :param repoid: Set the repository name or repository ID.
925 923 :type repoid: str or int
926 924 :param key: Set the unique field key for this repository.
927 925 :type key: str
928 926 """
929 927
930 928 repo = get_repo_or_error(repoid)
931 929 if not has_superadmin_permission(apiuser):
932 930 _perms = ('repository.admin',)
933 931 validate_repo_permissions(apiuser, repoid, repo, _perms)
934 932
935 933 field = RepositoryField.get_by_key_name(key, repo)
936 934 if not field:
937 935 raise JSONRPCError('Field with key `%s` does not '
938 936 'exists for repo `%s`' % (key, repoid))
939 937
940 938 try:
941 939 RepoModel().delete_repo_field(repo, field_key=key)
942 940 Session().commit()
943 941 return {
944 942 'msg': "Deleted repository field `%s`" % (key,),
945 943 'success': True,
946 944 }
947 945 except Exception:
948 946 log.exception(
949 947 "Exception occurred while trying to delete field from repo")
950 948 raise JSONRPCError(
951 949 'failed to delete field for repository `%s`' % (repoid,))
952 950
953 951
954 952 @jsonrpc_method()
955 953 def update_repo(
956 954 request, apiuser, repoid, repo_name=Optional(None),
957 955 owner=Optional(OAttr('apiuser')), description=Optional(''),
958 956 private=Optional(False),
959 957 clone_uri=Optional(None), push_uri=Optional(None),
960 958 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
961 959 enable_statistics=Optional(False),
962 960 enable_locking=Optional(False),
963 961 enable_downloads=Optional(False), fields=Optional('')):
964 962 """
965 963 Updates a repository with the given information.
966 964
967 965 This command can only be run using an |authtoken| with at least
968 966 admin permissions to the |repo|.
969 967
970 968 * If the repository name contains "/", repository will be updated
971 969 accordingly with a repository group or nested repository groups
972 970
973 971 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
974 972 called "repo-test" and place it inside group "foo/bar".
975 973 You have to have permissions to access and write to the last repository
976 974 group ("bar" in this example)
977 975
978 976 :param apiuser: This is filled automatically from the |authtoken|.
979 977 :type apiuser: AuthUser
980 978 :param repoid: repository name or repository ID.
981 979 :type repoid: str or int
982 980 :param repo_name: Update the |repo| name, including the
983 981 repository group it's in.
984 982 :type repo_name: str
985 983 :param owner: Set the |repo| owner.
986 984 :type owner: str
987 985 :param fork_of: Set the |repo| as fork of another |repo|.
988 986 :type fork_of: str
989 987 :param description: Update the |repo| description.
990 988 :type description: str
991 989 :param private: Set the |repo| as private. (True | False)
992 990 :type private: bool
993 991 :param clone_uri: Update the |repo| clone URI.
994 992 :type clone_uri: str
995 993 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
996 994 :type landing_rev: str
997 995 :param enable_statistics: Enable statistics on the |repo|, (True | False).
998 996 :type enable_statistics: bool
999 997 :param enable_locking: Enable |repo| locking.
1000 998 :type enable_locking: bool
1001 999 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1002 1000 :type enable_downloads: bool
1003 1001 :param fields: Add extra fields to the |repo|. Use the following
1004 1002 example format: ``field_key=field_val,field_key2=fieldval2``.
1005 1003 Escape ', ' with \,
1006 1004 :type fields: str
1007 1005 """
1008 1006
1009 1007 repo = get_repo_or_error(repoid)
1010 1008
1011 1009 include_secrets = False
1012 1010 if not has_superadmin_permission(apiuser):
1013 1011 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1014 1012 else:
1015 1013 include_secrets = True
1016 1014
1017 1015 updates = dict(
1018 1016 repo_name=repo_name
1019 1017 if not isinstance(repo_name, Optional) else repo.repo_name,
1020 1018
1021 1019 fork_id=fork_of
1022 1020 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1023 1021
1024 1022 user=owner
1025 1023 if not isinstance(owner, Optional) else repo.user.username,
1026 1024
1027 1025 repo_description=description
1028 1026 if not isinstance(description, Optional) else repo.description,
1029 1027
1030 1028 repo_private=private
1031 1029 if not isinstance(private, Optional) else repo.private,
1032 1030
1033 1031 clone_uri=clone_uri
1034 1032 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1035 1033
1036 1034 push_uri=push_uri
1037 1035 if not isinstance(push_uri, Optional) else repo.push_uri,
1038 1036
1039 1037 repo_landing_rev=landing_rev
1040 1038 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1041 1039
1042 1040 repo_enable_statistics=enable_statistics
1043 1041 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1044 1042
1045 1043 repo_enable_locking=enable_locking
1046 1044 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1047 1045
1048 1046 repo_enable_downloads=enable_downloads
1049 1047 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1050 1048
1051 1049 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1052 1050 request.translate, repo=repo)
1053 1051
1054 1052 old_values = repo.get_api_data()
1055 1053 repo_type = repo.repo_type
1056 1054 schema = repo_schema.RepoSchema().bind(
1057 1055 repo_type_options=rhodecode.BACKENDS.keys(),
1058 1056 repo_ref_options=ref_choices,
1059 1057 repo_type=repo_type,
1060 1058 # user caller
1061 1059 user=apiuser,
1062 1060 old_values=old_values)
1063 1061 try:
1064 1062 schema_data = schema.deserialize(dict(
1065 1063 # we save old value, users cannot change type
1066 1064 repo_type=repo_type,
1067 1065
1068 1066 repo_name=updates['repo_name'],
1069 1067 repo_owner=updates['user'],
1070 1068 repo_description=updates['repo_description'],
1071 1069 repo_clone_uri=updates['clone_uri'],
1072 1070 repo_push_uri=updates['push_uri'],
1073 1071 repo_fork_of=updates['fork_id'],
1074 1072 repo_private=updates['repo_private'],
1075 1073 repo_landing_commit_ref=updates['repo_landing_rev'],
1076 1074 repo_enable_statistics=updates['repo_enable_statistics'],
1077 1075 repo_enable_downloads=updates['repo_enable_downloads'],
1078 1076 repo_enable_locking=updates['repo_enable_locking']))
1079 1077 except validation_schema.Invalid as err:
1080 1078 raise JSONRPCValidationError(colander_exc=err)
1081 1079
1082 1080 # save validated data back into the updates dict
1083 1081 validated_updates = dict(
1084 1082 repo_name=schema_data['repo_group']['repo_name_without_group'],
1085 1083 repo_group=schema_data['repo_group']['repo_group_id'],
1086 1084
1087 1085 user=schema_data['repo_owner'],
1088 1086 repo_description=schema_data['repo_description'],
1089 1087 repo_private=schema_data['repo_private'],
1090 1088 clone_uri=schema_data['repo_clone_uri'],
1091 1089 push_uri=schema_data['repo_push_uri'],
1092 1090 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1093 1091 repo_enable_statistics=schema_data['repo_enable_statistics'],
1094 1092 repo_enable_locking=schema_data['repo_enable_locking'],
1095 1093 repo_enable_downloads=schema_data['repo_enable_downloads'],
1096 1094 )
1097 1095
1098 1096 if schema_data['repo_fork_of']:
1099 1097 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1100 1098 validated_updates['fork_id'] = fork_repo.repo_id
1101 1099
1102 1100 # extra fields
1103 1101 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1104 1102 if fields:
1105 1103 validated_updates.update(fields)
1106 1104
1107 1105 try:
1108 1106 RepoModel().update(repo, **validated_updates)
1109 1107 audit_logger.store_api(
1110 1108 'repo.edit', action_data={'old_data': old_values},
1111 1109 user=apiuser, repo=repo)
1112 1110 Session().commit()
1113 1111 return {
1114 1112 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1115 1113 'repository': repo.get_api_data(include_secrets=include_secrets)
1116 1114 }
1117 1115 except Exception:
1118 1116 log.exception(
1119 1117 u"Exception while trying to update the repository %s",
1120 1118 repoid)
1121 1119 raise JSONRPCError('failed to update repo `%s`' % repoid)
1122 1120
1123 1121
1124 1122 @jsonrpc_method()
1125 1123 def fork_repo(request, apiuser, repoid, fork_name,
1126 1124 owner=Optional(OAttr('apiuser')),
1127 1125 description=Optional(''),
1128 1126 private=Optional(False),
1129 1127 clone_uri=Optional(None),
1130 1128 landing_rev=Optional('rev:tip'),
1131 1129 copy_permissions=Optional(False)):
1132 1130 """
1133 1131 Creates a fork of the specified |repo|.
1134 1132
1135 1133 * If the fork_name contains "/", fork will be created inside
1136 1134 a repository group or nested repository groups
1137 1135
1138 1136 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1139 1137 inside group "foo/bar". You have to have permissions to access and
1140 1138 write to the last repository group ("bar" in this example)
1141 1139
1142 1140 This command can only be run using an |authtoken| with minimum
1143 1141 read permissions of the forked repo, create fork permissions for an user.
1144 1142
1145 1143 :param apiuser: This is filled automatically from the |authtoken|.
1146 1144 :type apiuser: AuthUser
1147 1145 :param repoid: Set repository name or repository ID.
1148 1146 :type repoid: str or int
1149 1147 :param fork_name: Set the fork name, including it's repository group membership.
1150 1148 :type fork_name: str
1151 1149 :param owner: Set the fork owner.
1152 1150 :type owner: str
1153 1151 :param description: Set the fork description.
1154 1152 :type description: str
1155 1153 :param copy_permissions: Copy permissions from parent |repo|. The
1156 1154 default is False.
1157 1155 :type copy_permissions: bool
1158 1156 :param private: Make the fork private. The default is False.
1159 1157 :type private: bool
1160 1158 :param landing_rev: Set the landing revision. The default is tip.
1161 1159
1162 1160 Example output:
1163 1161
1164 1162 .. code-block:: bash
1165 1163
1166 1164 id : <id_for_response>
1167 1165 api_key : "<api_key>"
1168 1166 args: {
1169 1167 "repoid" : "<reponame or repo_id>",
1170 1168 "fork_name": "<forkname>",
1171 1169 "owner": "<username or user_id = Optional(=apiuser)>",
1172 1170 "description": "<description>",
1173 1171 "copy_permissions": "<bool>",
1174 1172 "private": "<bool>",
1175 1173 "landing_rev": "<landing_rev>"
1176 1174 }
1177 1175
1178 1176 Example error output:
1179 1177
1180 1178 .. code-block:: bash
1181 1179
1182 1180 id : <id_given_in_input>
1183 1181 result: {
1184 1182 "msg": "Created fork of `<reponame>` as `<forkname>`",
1185 1183 "success": true,
1186 1184 "task": "<celery task id or None if done sync>"
1187 1185 }
1188 1186 error: null
1189 1187
1190 1188 """
1191 1189
1192 1190 repo = get_repo_or_error(repoid)
1193 1191 repo_name = repo.repo_name
1194 1192
1195 1193 if not has_superadmin_permission(apiuser):
1196 1194 # check if we have at least read permission for
1197 1195 # this repo that we fork !
1198 1196 _perms = (
1199 1197 'repository.admin', 'repository.write', 'repository.read')
1200 1198 validate_repo_permissions(apiuser, repoid, repo, _perms)
1201 1199
1202 1200 # check if the regular user has at least fork permissions as well
1203 1201 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1204 1202 raise JSONRPCForbidden()
1205 1203
1206 1204 # check if user can set owner parameter
1207 1205 owner = validate_set_owner_permissions(apiuser, owner)
1208 1206
1209 1207 description = Optional.extract(description)
1210 1208 copy_permissions = Optional.extract(copy_permissions)
1211 1209 clone_uri = Optional.extract(clone_uri)
1212 1210 landing_commit_ref = Optional.extract(landing_rev)
1213 1211 private = Optional.extract(private)
1214 1212
1215 1213 schema = repo_schema.RepoSchema().bind(
1216 1214 repo_type_options=rhodecode.BACKENDS.keys(),
1217 1215 repo_type=repo.repo_type,
1218 1216 # user caller
1219 1217 user=apiuser)
1220 1218
1221 1219 try:
1222 1220 schema_data = schema.deserialize(dict(
1223 1221 repo_name=fork_name,
1224 1222 repo_type=repo.repo_type,
1225 1223 repo_owner=owner.username,
1226 1224 repo_description=description,
1227 1225 repo_landing_commit_ref=landing_commit_ref,
1228 1226 repo_clone_uri=clone_uri,
1229 1227 repo_private=private,
1230 1228 repo_copy_permissions=copy_permissions))
1231 1229 except validation_schema.Invalid as err:
1232 1230 raise JSONRPCValidationError(colander_exc=err)
1233 1231
1234 1232 try:
1235 1233 data = {
1236 1234 'fork_parent_id': repo.repo_id,
1237 1235
1238 1236 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1239 1237 'repo_name_full': schema_data['repo_name'],
1240 1238 'repo_group': schema_data['repo_group']['repo_group_id'],
1241 1239 'repo_type': schema_data['repo_type'],
1242 1240 'description': schema_data['repo_description'],
1243 1241 'private': schema_data['repo_private'],
1244 1242 'copy_permissions': schema_data['repo_copy_permissions'],
1245 1243 'landing_rev': schema_data['repo_landing_commit_ref'],
1246 1244 }
1247 1245
1248 1246 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1249 1247 # no commit, it's done in RepoModel, or async via celery
1250 1248 task_id = get_task_id(task)
1251 1249
1252 1250 return {
1253 1251 'msg': 'Created fork of `%s` as `%s`' % (
1254 1252 repo.repo_name, schema_data['repo_name']),
1255 1253 'success': True, # cannot return the repo data here since fork
1256 1254 # can be done async
1257 1255 'task': task_id
1258 1256 }
1259 1257 except Exception:
1260 1258 log.exception(
1261 1259 u"Exception while trying to create fork %s",
1262 1260 schema_data['repo_name'])
1263 1261 raise JSONRPCError(
1264 1262 'failed to fork repository `%s` as `%s`' % (
1265 1263 repo_name, schema_data['repo_name']))
1266 1264
1267 1265
1268 1266 @jsonrpc_method()
1269 1267 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1270 1268 """
1271 1269 Deletes a repository.
1272 1270
1273 1271 * When the `forks` parameter is set it's possible to detach or delete
1274 1272 forks of deleted repository.
1275 1273
1276 1274 This command can only be run using an |authtoken| with admin
1277 1275 permissions on the |repo|.
1278 1276
1279 1277 :param apiuser: This is filled automatically from the |authtoken|.
1280 1278 :type apiuser: AuthUser
1281 1279 :param repoid: Set the repository name or repository ID.
1282 1280 :type repoid: str or int
1283 1281 :param forks: Set to `detach` or `delete` forks from the |repo|.
1284 1282 :type forks: Optional(str)
1285 1283
1286 1284 Example error output:
1287 1285
1288 1286 .. code-block:: bash
1289 1287
1290 1288 id : <id_given_in_input>
1291 1289 result: {
1292 1290 "msg": "Deleted repository `<reponame>`",
1293 1291 "success": true
1294 1292 }
1295 1293 error: null
1296 1294 """
1297 1295
1298 1296 repo = get_repo_or_error(repoid)
1299 1297 repo_name = repo.repo_name
1300 1298 if not has_superadmin_permission(apiuser):
1301 1299 _perms = ('repository.admin',)
1302 1300 validate_repo_permissions(apiuser, repoid, repo, _perms)
1303 1301
1304 1302 try:
1305 1303 handle_forks = Optional.extract(forks)
1306 1304 _forks_msg = ''
1307 1305 _forks = [f for f in repo.forks]
1308 1306 if handle_forks == 'detach':
1309 1307 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1310 1308 elif handle_forks == 'delete':
1311 1309 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1312 1310 elif _forks:
1313 1311 raise JSONRPCError(
1314 1312 'Cannot delete `%s` it still contains attached forks' %
1315 1313 (repo.repo_name,)
1316 1314 )
1317 1315 old_data = repo.get_api_data()
1318 1316 RepoModel().delete(repo, forks=forks)
1319 1317
1320 1318 repo = audit_logger.RepoWrap(repo_id=None,
1321 1319 repo_name=repo.repo_name)
1322 1320
1323 1321 audit_logger.store_api(
1324 1322 'repo.delete', action_data={'old_data': old_data},
1325 1323 user=apiuser, repo=repo)
1326 1324
1327 1325 ScmModel().mark_for_invalidation(repo_name, delete=True)
1328 1326 Session().commit()
1329 1327 return {
1330 1328 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1331 1329 'success': True
1332 1330 }
1333 1331 except Exception:
1334 1332 log.exception("Exception occurred while trying to delete repo")
1335 1333 raise JSONRPCError(
1336 1334 'failed to delete repository `%s`' % (repo_name,)
1337 1335 )
1338 1336
1339 1337
1340 1338 #TODO: marcink, change name ?
1341 1339 @jsonrpc_method()
1342 1340 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1343 1341 """
1344 1342 Invalidates the cache for the specified repository.
1345 1343
1346 1344 This command can only be run using an |authtoken| with admin rights to
1347 1345 the specified repository.
1348 1346
1349 1347 This command takes the following options:
1350 1348
1351 1349 :param apiuser: This is filled automatically from |authtoken|.
1352 1350 :type apiuser: AuthUser
1353 1351 :param repoid: Sets the repository name or repository ID.
1354 1352 :type repoid: str or int
1355 1353 :param delete_keys: This deletes the invalidated keys instead of
1356 1354 just flagging them.
1357 1355 :type delete_keys: Optional(``True`` | ``False``)
1358 1356
1359 1357 Example output:
1360 1358
1361 1359 .. code-block:: bash
1362 1360
1363 1361 id : <id_given_in_input>
1364 1362 result : {
1365 1363 'msg': Cache for repository `<repository name>` was invalidated,
1366 1364 'repository': <repository name>
1367 1365 }
1368 1366 error : null
1369 1367
1370 1368 Example error output:
1371 1369
1372 1370 .. code-block:: bash
1373 1371
1374 1372 id : <id_given_in_input>
1375 1373 result : null
1376 1374 error : {
1377 1375 'Error occurred during cache invalidation action'
1378 1376 }
1379 1377
1380 1378 """
1381 1379
1382 1380 repo = get_repo_or_error(repoid)
1383 1381 if not has_superadmin_permission(apiuser):
1384 1382 _perms = ('repository.admin', 'repository.write',)
1385 1383 validate_repo_permissions(apiuser, repoid, repo, _perms)
1386 1384
1387 1385 delete = Optional.extract(delete_keys)
1388 1386 try:
1389 1387 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1390 1388 return {
1391 1389 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1392 1390 'repository': repo.repo_name
1393 1391 }
1394 1392 except Exception:
1395 1393 log.exception(
1396 1394 "Exception occurred while trying to invalidate repo cache")
1397 1395 raise JSONRPCError(
1398 1396 'Error occurred during cache invalidation action'
1399 1397 )
1400 1398
1401 1399
1402 1400 #TODO: marcink, change name ?
1403 1401 @jsonrpc_method()
1404 1402 def lock(request, apiuser, repoid, locked=Optional(None),
1405 1403 userid=Optional(OAttr('apiuser'))):
1406 1404 """
1407 1405 Sets the lock state of the specified |repo| by the given user.
1408 1406 From more information, see :ref:`repo-locking`.
1409 1407
1410 1408 * If the ``userid`` option is not set, the repository is locked to the
1411 1409 user who called the method.
1412 1410 * If the ``locked`` parameter is not set, the current lock state of the
1413 1411 repository is displayed.
1414 1412
1415 1413 This command can only be run using an |authtoken| with admin rights to
1416 1414 the specified repository.
1417 1415
1418 1416 This command takes the following options:
1419 1417
1420 1418 :param apiuser: This is filled automatically from the |authtoken|.
1421 1419 :type apiuser: AuthUser
1422 1420 :param repoid: Sets the repository name or repository ID.
1423 1421 :type repoid: str or int
1424 1422 :param locked: Sets the lock state.
1425 1423 :type locked: Optional(``True`` | ``False``)
1426 1424 :param userid: Set the repository lock to this user.
1427 1425 :type userid: Optional(str or int)
1428 1426
1429 1427 Example error output:
1430 1428
1431 1429 .. code-block:: bash
1432 1430
1433 1431 id : <id_given_in_input>
1434 1432 result : {
1435 1433 'repo': '<reponame>',
1436 1434 'locked': <bool: lock state>,
1437 1435 'locked_since': <int: lock timestamp>,
1438 1436 'locked_by': <username of person who made the lock>,
1439 1437 'lock_reason': <str: reason for locking>,
1440 1438 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1441 1439 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1442 1440 or
1443 1441 'msg': 'Repo `<repository name>` not locked.'
1444 1442 or
1445 1443 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1446 1444 }
1447 1445 error : null
1448 1446
1449 1447 Example error output:
1450 1448
1451 1449 .. code-block:: bash
1452 1450
1453 1451 id : <id_given_in_input>
1454 1452 result : null
1455 1453 error : {
1456 1454 'Error occurred locking repository `<reponame>`'
1457 1455 }
1458 1456 """
1459 1457
1460 1458 repo = get_repo_or_error(repoid)
1461 1459 if not has_superadmin_permission(apiuser):
1462 1460 # check if we have at least write permission for this repo !
1463 1461 _perms = ('repository.admin', 'repository.write',)
1464 1462 validate_repo_permissions(apiuser, repoid, repo, _perms)
1465 1463
1466 1464 # make sure normal user does not pass someone else userid,
1467 1465 # he is not allowed to do that
1468 1466 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1469 1467 raise JSONRPCError('userid is not the same as your user')
1470 1468
1471 1469 if isinstance(userid, Optional):
1472 1470 userid = apiuser.user_id
1473 1471
1474 1472 user = get_user_or_error(userid)
1475 1473
1476 1474 if isinstance(locked, Optional):
1477 1475 lockobj = repo.locked
1478 1476
1479 1477 if lockobj[0] is None:
1480 1478 _d = {
1481 1479 'repo': repo.repo_name,
1482 1480 'locked': False,
1483 1481 'locked_since': None,
1484 1482 'locked_by': None,
1485 1483 'lock_reason': None,
1486 1484 'lock_state_changed': False,
1487 1485 'msg': 'Repo `%s` not locked.' % repo.repo_name
1488 1486 }
1489 1487 return _d
1490 1488 else:
1491 1489 _user_id, _time, _reason = lockobj
1492 1490 lock_user = get_user_or_error(userid)
1493 1491 _d = {
1494 1492 'repo': repo.repo_name,
1495 1493 'locked': True,
1496 1494 'locked_since': _time,
1497 1495 'locked_by': lock_user.username,
1498 1496 'lock_reason': _reason,
1499 1497 'lock_state_changed': False,
1500 1498 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1501 1499 % (repo.repo_name, lock_user.username,
1502 1500 json.dumps(time_to_datetime(_time))))
1503 1501 }
1504 1502 return _d
1505 1503
1506 1504 # force locked state through a flag
1507 1505 else:
1508 1506 locked = str2bool(locked)
1509 1507 lock_reason = Repository.LOCK_API
1510 1508 try:
1511 1509 if locked:
1512 1510 lock_time = time.time()
1513 1511 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1514 1512 else:
1515 1513 lock_time = None
1516 1514 Repository.unlock(repo)
1517 1515 _d = {
1518 1516 'repo': repo.repo_name,
1519 1517 'locked': locked,
1520 1518 'locked_since': lock_time,
1521 1519 'locked_by': user.username,
1522 1520 'lock_reason': lock_reason,
1523 1521 'lock_state_changed': True,
1524 1522 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1525 1523 % (user.username, repo.repo_name, locked))
1526 1524 }
1527 1525 return _d
1528 1526 except Exception:
1529 1527 log.exception(
1530 1528 "Exception occurred while trying to lock repository")
1531 1529 raise JSONRPCError(
1532 1530 'Error occurred locking repository `%s`' % repo.repo_name
1533 1531 )
1534 1532
1535 1533
1536 1534 @jsonrpc_method()
1537 1535 def comment_commit(
1538 1536 request, apiuser, repoid, commit_id, message, status=Optional(None),
1539 1537 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1540 1538 resolves_comment_id=Optional(None),
1541 1539 userid=Optional(OAttr('apiuser'))):
1542 1540 """
1543 1541 Set a commit comment, and optionally change the status of the commit.
1544 1542
1545 1543 :param apiuser: This is filled automatically from the |authtoken|.
1546 1544 :type apiuser: AuthUser
1547 1545 :param repoid: Set the repository name or repository ID.
1548 1546 :type repoid: str or int
1549 1547 :param commit_id: Specify the commit_id for which to set a comment.
1550 1548 :type commit_id: str
1551 1549 :param message: The comment text.
1552 1550 :type message: str
1553 1551 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1554 1552 'approved', 'rejected', 'under_review'
1555 1553 :type status: str
1556 1554 :param comment_type: Comment type, one of: 'note', 'todo'
1557 1555 :type comment_type: Optional(str), default: 'note'
1558 1556 :param userid: Set the user name of the comment creator.
1559 1557 :type userid: Optional(str or int)
1560 1558
1561 1559 Example error output:
1562 1560
1563 1561 .. code-block:: bash
1564 1562
1565 1563 {
1566 1564 "id" : <id_given_in_input>,
1567 1565 "result" : {
1568 1566 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1569 1567 "status_change": null or <status>,
1570 1568 "success": true
1571 1569 },
1572 1570 "error" : null
1573 1571 }
1574 1572
1575 1573 """
1576 1574 repo = get_repo_or_error(repoid)
1577 1575 if not has_superadmin_permission(apiuser):
1578 1576 _perms = ('repository.read', 'repository.write', 'repository.admin')
1579 1577 validate_repo_permissions(apiuser, repoid, repo, _perms)
1580 1578
1581 1579 try:
1582 1580 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1583 1581 except Exception as e:
1584 1582 log.exception('Failed to fetch commit')
1585 1583 raise JSONRPCError(safe_str(e))
1586 1584
1587 1585 if isinstance(userid, Optional):
1588 1586 userid = apiuser.user_id
1589 1587
1590 1588 user = get_user_or_error(userid)
1591 1589 status = Optional.extract(status)
1592 1590 comment_type = Optional.extract(comment_type)
1593 1591 resolves_comment_id = Optional.extract(resolves_comment_id)
1594 1592
1595 1593 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1596 1594 if status and status not in allowed_statuses:
1597 1595 raise JSONRPCError('Bad status, must be on '
1598 1596 'of %s got %s' % (allowed_statuses, status,))
1599 1597
1600 1598 if resolves_comment_id:
1601 1599 comment = ChangesetComment.get(resolves_comment_id)
1602 1600 if not comment:
1603 1601 raise JSONRPCError(
1604 1602 'Invalid resolves_comment_id `%s` for this commit.'
1605 1603 % resolves_comment_id)
1606 1604 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1607 1605 raise JSONRPCError(
1608 1606 'Comment `%s` is wrong type for setting status to resolved.'
1609 1607 % resolves_comment_id)
1610 1608
1611 1609 try:
1612 1610 rc_config = SettingsModel().get_all_settings()
1613 1611 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1614 1612 status_change_label = ChangesetStatus.get_status_lbl(status)
1615 1613 comment = CommentsModel().create(
1616 1614 message, repo, user, commit_id=commit_id,
1617 1615 status_change=status_change_label,
1618 1616 status_change_type=status,
1619 1617 renderer=renderer,
1620 1618 comment_type=comment_type,
1621 1619 resolves_comment_id=resolves_comment_id,
1622 1620 auth_user=apiuser
1623 1621 )
1624 1622 if status:
1625 1623 # also do a status change
1626 1624 try:
1627 1625 ChangesetStatusModel().set_status(
1628 1626 repo, status, user, comment, revision=commit_id,
1629 1627 dont_allow_on_closed_pull_request=True
1630 1628 )
1631 1629 except StatusChangeOnClosedPullRequestError:
1632 1630 log.exception(
1633 1631 "Exception occurred while trying to change repo commit status")
1634 1632 msg = ('Changing status on a changeset associated with '
1635 1633 'a closed pull request is not allowed')
1636 1634 raise JSONRPCError(msg)
1637 1635
1638 1636 Session().commit()
1639 1637 return {
1640 1638 'msg': (
1641 1639 'Commented on commit `%s` for repository `%s`' % (
1642 1640 comment.revision, repo.repo_name)),
1643 1641 'status_change': status,
1644 1642 'success': True,
1645 1643 }
1646 1644 except JSONRPCError:
1647 1645 # catch any inside errors, and re-raise them to prevent from
1648 1646 # below global catch to silence them
1649 1647 raise
1650 1648 except Exception:
1651 1649 log.exception("Exception occurred while trying to comment on commit")
1652 1650 raise JSONRPCError(
1653 1651 'failed to set comment on repository `%s`' % (repo.repo_name,)
1654 1652 )
1655 1653
1656 1654
1657 1655 @jsonrpc_method()
1658 1656 def get_repo_comments(request, apiuser, repoid,
1659 1657 commit_id=Optional(None), comment_type=Optional(None),
1660 1658 userid=Optional(None)):
1661 1659 """
1662 1660 Get all comments for a repository
1663 1661
1664 1662 :param apiuser: This is filled automatically from the |authtoken|.
1665 1663 :type apiuser: AuthUser
1666 1664 :param repoid: Set the repository name or repository ID.
1667 1665 :type repoid: str or int
1668 1666 :param commit_id: Optionally filter the comments by the commit_id
1669 1667 :type commit_id: Optional(str), default: None
1670 1668 :param comment_type: Optionally filter the comments by the comment_type
1671 1669 one of: 'note', 'todo'
1672 1670 :type comment_type: Optional(str), default: None
1673 1671 :param userid: Optionally filter the comments by the author of comment
1674 1672 :type userid: Optional(str or int), Default: None
1675 1673
1676 1674 Example error output:
1677 1675
1678 1676 .. code-block:: bash
1679 1677
1680 1678 {
1681 1679 "id" : <id_given_in_input>,
1682 1680 "result" : [
1683 1681 {
1684 1682 "comment_author": <USER_DETAILS>,
1685 1683 "comment_created_on": "2017-02-01T14:38:16.309",
1686 1684 "comment_f_path": "file.txt",
1687 1685 "comment_id": 282,
1688 1686 "comment_lineno": "n1",
1689 1687 "comment_resolved_by": null,
1690 1688 "comment_status": [],
1691 1689 "comment_text": "This file needs a header",
1692 1690 "comment_type": "todo"
1693 1691 }
1694 1692 ],
1695 1693 "error" : null
1696 1694 }
1697 1695
1698 1696 """
1699 1697 repo = get_repo_or_error(repoid)
1700 1698 if not has_superadmin_permission(apiuser):
1701 1699 _perms = ('repository.read', 'repository.write', 'repository.admin')
1702 1700 validate_repo_permissions(apiuser, repoid, repo, _perms)
1703 1701
1704 1702 commit_id = Optional.extract(commit_id)
1705 1703
1706 1704 userid = Optional.extract(userid)
1707 1705 if userid:
1708 1706 user = get_user_or_error(userid)
1709 1707 else:
1710 1708 user = None
1711 1709
1712 1710 comment_type = Optional.extract(comment_type)
1713 1711 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1714 1712 raise JSONRPCError(
1715 1713 'comment_type must be one of `{}` got {}'.format(
1716 1714 ChangesetComment.COMMENT_TYPES, comment_type)
1717 1715 )
1718 1716
1719 1717 comments = CommentsModel().get_repository_comments(
1720 1718 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1721 1719 return comments
1722 1720
1723 1721
1724 1722 @jsonrpc_method()
1725 1723 def grant_user_permission(request, apiuser, repoid, userid, perm):
1726 1724 """
1727 1725 Grant permissions for the specified user on the given repository,
1728 1726 or update existing permissions if found.
1729 1727
1730 1728 This command can only be run using an |authtoken| with admin
1731 1729 permissions on the |repo|.
1732 1730
1733 1731 :param apiuser: This is filled automatically from the |authtoken|.
1734 1732 :type apiuser: AuthUser
1735 1733 :param repoid: Set the repository name or repository ID.
1736 1734 :type repoid: str or int
1737 1735 :param userid: Set the user name.
1738 1736 :type userid: str
1739 1737 :param perm: Set the user permissions, using the following format
1740 1738 ``(repository.(none|read|write|admin))``
1741 1739 :type perm: str
1742 1740
1743 1741 Example output:
1744 1742
1745 1743 .. code-block:: bash
1746 1744
1747 1745 id : <id_given_in_input>
1748 1746 result: {
1749 1747 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1750 1748 "success": true
1751 1749 }
1752 1750 error: null
1753 1751 """
1754 1752
1755 1753 repo = get_repo_or_error(repoid)
1756 1754 user = get_user_or_error(userid)
1757 1755 perm = get_perm_or_error(perm)
1758 1756 if not has_superadmin_permission(apiuser):
1759 1757 _perms = ('repository.admin',)
1760 1758 validate_repo_permissions(apiuser, repoid, repo, _perms)
1761 1759
1762 1760 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1763 1761 try:
1764 1762 changes = RepoModel().update_permissions(
1765 1763 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1766 1764
1767 1765 action_data = {
1768 1766 'added': changes['added'],
1769 1767 'updated': changes['updated'],
1770 1768 'deleted': changes['deleted'],
1771 1769 }
1772 1770 audit_logger.store_api(
1773 1771 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1774 1772
1775 1773 Session().commit()
1776 1774 return {
1777 1775 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1778 1776 perm.permission_name, user.username, repo.repo_name
1779 1777 ),
1780 1778 'success': True
1781 1779 }
1782 1780 except Exception:
1783 1781 log.exception("Exception occurred while trying edit permissions for repo")
1784 1782 raise JSONRPCError(
1785 1783 'failed to edit permission for user: `%s` in repo: `%s`' % (
1786 1784 userid, repoid
1787 1785 )
1788 1786 )
1789 1787
1790 1788
1791 1789 @jsonrpc_method()
1792 1790 def revoke_user_permission(request, apiuser, repoid, userid):
1793 1791 """
1794 1792 Revoke permission for a user on the specified repository.
1795 1793
1796 1794 This command can only be run using an |authtoken| with admin
1797 1795 permissions on the |repo|.
1798 1796
1799 1797 :param apiuser: This is filled automatically from the |authtoken|.
1800 1798 :type apiuser: AuthUser
1801 1799 :param repoid: Set the repository name or repository ID.
1802 1800 :type repoid: str or int
1803 1801 :param userid: Set the user name of revoked user.
1804 1802 :type userid: str or int
1805 1803
1806 1804 Example error output:
1807 1805
1808 1806 .. code-block:: bash
1809 1807
1810 1808 id : <id_given_in_input>
1811 1809 result: {
1812 1810 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1813 1811 "success": true
1814 1812 }
1815 1813 error: null
1816 1814 """
1817 1815
1818 1816 repo = get_repo_or_error(repoid)
1819 1817 user = get_user_or_error(userid)
1820 1818 if not has_superadmin_permission(apiuser):
1821 1819 _perms = ('repository.admin',)
1822 1820 validate_repo_permissions(apiuser, repoid, repo, _perms)
1823 1821
1824 1822 perm_deletions = [[user.user_id, None, "user"]]
1825 1823 try:
1826 1824 changes = RepoModel().update_permissions(
1827 1825 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1828 1826
1829 1827 action_data = {
1830 1828 'added': changes['added'],
1831 1829 'updated': changes['updated'],
1832 1830 'deleted': changes['deleted'],
1833 1831 }
1834 1832 audit_logger.store_api(
1835 1833 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1836 1834
1837 1835 Session().commit()
1838 1836 return {
1839 1837 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1840 1838 user.username, repo.repo_name
1841 1839 ),
1842 1840 'success': True
1843 1841 }
1844 1842 except Exception:
1845 1843 log.exception("Exception occurred while trying revoke permissions to repo")
1846 1844 raise JSONRPCError(
1847 1845 'failed to edit permission for user: `%s` in repo: `%s`' % (
1848 1846 userid, repoid
1849 1847 )
1850 1848 )
1851 1849
1852 1850
1853 1851 @jsonrpc_method()
1854 1852 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1855 1853 """
1856 1854 Grant permission for a user group on the specified repository,
1857 1855 or update existing permissions.
1858 1856
1859 1857 This command can only be run using an |authtoken| with admin
1860 1858 permissions on the |repo|.
1861 1859
1862 1860 :param apiuser: This is filled automatically from the |authtoken|.
1863 1861 :type apiuser: AuthUser
1864 1862 :param repoid: Set the repository name or repository ID.
1865 1863 :type repoid: str or int
1866 1864 :param usergroupid: Specify the ID of the user group.
1867 1865 :type usergroupid: str or int
1868 1866 :param perm: Set the user group permissions using the following
1869 1867 format: (repository.(none|read|write|admin))
1870 1868 :type perm: str
1871 1869
1872 1870 Example output:
1873 1871
1874 1872 .. code-block:: bash
1875 1873
1876 1874 id : <id_given_in_input>
1877 1875 result : {
1878 1876 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1879 1877 "success": true
1880 1878
1881 1879 }
1882 1880 error : null
1883 1881
1884 1882 Example error output:
1885 1883
1886 1884 .. code-block:: bash
1887 1885
1888 1886 id : <id_given_in_input>
1889 1887 result : null
1890 1888 error : {
1891 1889 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1892 1890 }
1893 1891
1894 1892 """
1895 1893
1896 1894 repo = get_repo_or_error(repoid)
1897 1895 perm = get_perm_or_error(perm)
1898 1896 if not has_superadmin_permission(apiuser):
1899 1897 _perms = ('repository.admin',)
1900 1898 validate_repo_permissions(apiuser, repoid, repo, _perms)
1901 1899
1902 1900 user_group = get_user_group_or_error(usergroupid)
1903 1901 if not has_superadmin_permission(apiuser):
1904 1902 # check if we have at least read permission for this user group !
1905 1903 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1906 1904 if not HasUserGroupPermissionAnyApi(*_perms)(
1907 1905 user=apiuser, user_group_name=user_group.users_group_name):
1908 1906 raise JSONRPCError(
1909 1907 'user group `%s` does not exist' % (usergroupid,))
1910 1908
1911 1909 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1912 1910 try:
1913 1911 changes = RepoModel().update_permissions(
1914 1912 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1915 1913 action_data = {
1916 1914 'added': changes['added'],
1917 1915 'updated': changes['updated'],
1918 1916 'deleted': changes['deleted'],
1919 1917 }
1920 1918 audit_logger.store_api(
1921 1919 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1922 1920
1923 1921 Session().commit()
1924 1922 return {
1925 1923 'msg': 'Granted perm: `%s` for user group: `%s` in '
1926 1924 'repo: `%s`' % (
1927 1925 perm.permission_name, user_group.users_group_name,
1928 1926 repo.repo_name
1929 1927 ),
1930 1928 'success': True
1931 1929 }
1932 1930 except Exception:
1933 1931 log.exception(
1934 1932 "Exception occurred while trying change permission on repo")
1935 1933 raise JSONRPCError(
1936 1934 'failed to edit permission for user group: `%s` in '
1937 1935 'repo: `%s`' % (
1938 1936 usergroupid, repo.repo_name
1939 1937 )
1940 1938 )
1941 1939
1942 1940
1943 1941 @jsonrpc_method()
1944 1942 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1945 1943 """
1946 1944 Revoke the permissions of a user group on a given repository.
1947 1945
1948 1946 This command can only be run using an |authtoken| with admin
1949 1947 permissions on the |repo|.
1950 1948
1951 1949 :param apiuser: This is filled automatically from the |authtoken|.
1952 1950 :type apiuser: AuthUser
1953 1951 :param repoid: Set the repository name or repository ID.
1954 1952 :type repoid: str or int
1955 1953 :param usergroupid: Specify the user group ID.
1956 1954 :type usergroupid: str or int
1957 1955
1958 1956 Example output:
1959 1957
1960 1958 .. code-block:: bash
1961 1959
1962 1960 id : <id_given_in_input>
1963 1961 result: {
1964 1962 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1965 1963 "success": true
1966 1964 }
1967 1965 error: null
1968 1966 """
1969 1967
1970 1968 repo = get_repo_or_error(repoid)
1971 1969 if not has_superadmin_permission(apiuser):
1972 1970 _perms = ('repository.admin',)
1973 1971 validate_repo_permissions(apiuser, repoid, repo, _perms)
1974 1972
1975 1973 user_group = get_user_group_or_error(usergroupid)
1976 1974 if not has_superadmin_permission(apiuser):
1977 1975 # check if we have at least read permission for this user group !
1978 1976 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1979 1977 if not HasUserGroupPermissionAnyApi(*_perms)(
1980 1978 user=apiuser, user_group_name=user_group.users_group_name):
1981 1979 raise JSONRPCError(
1982 1980 'user group `%s` does not exist' % (usergroupid,))
1983 1981
1984 1982 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1985 1983 try:
1986 1984 changes = RepoModel().update_permissions(
1987 1985 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1988 1986 action_data = {
1989 1987 'added': changes['added'],
1990 1988 'updated': changes['updated'],
1991 1989 'deleted': changes['deleted'],
1992 1990 }
1993 1991 audit_logger.store_api(
1994 1992 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1995 1993
1996 1994 Session().commit()
1997 1995 return {
1998 1996 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1999 1997 user_group.users_group_name, repo.repo_name
2000 1998 ),
2001 1999 'success': True
2002 2000 }
2003 2001 except Exception:
2004 2002 log.exception("Exception occurred while trying revoke "
2005 2003 "user group permission on repo")
2006 2004 raise JSONRPCError(
2007 2005 'failed to edit permission for user group: `%s` in '
2008 2006 'repo: `%s`' % (
2009 2007 user_group.users_group_name, repo.repo_name
2010 2008 )
2011 2009 )
2012 2010
2013 2011
2014 2012 @jsonrpc_method()
2015 2013 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2016 2014 """
2017 2015 Triggers a pull on the given repository from a remote location. You
2018 2016 can use this to keep remote repositories up-to-date.
2019 2017
2020 2018 This command can only be run using an |authtoken| with admin
2021 2019 rights to the specified repository. For more information,
2022 2020 see :ref:`config-token-ref`.
2023 2021
2024 2022 This command takes the following options:
2025 2023
2026 2024 :param apiuser: This is filled automatically from the |authtoken|.
2027 2025 :type apiuser: AuthUser
2028 2026 :param repoid: The repository name or repository ID.
2029 2027 :type repoid: str or int
2030 2028 :param remote_uri: Optional remote URI to pass in for pull
2031 2029 :type remote_uri: str
2032 2030
2033 2031 Example output:
2034 2032
2035 2033 .. code-block:: bash
2036 2034
2037 2035 id : <id_given_in_input>
2038 2036 result : {
2039 2037 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2040 2038 "repository": "<repository name>"
2041 2039 }
2042 2040 error : null
2043 2041
2044 2042 Example error output:
2045 2043
2046 2044 .. code-block:: bash
2047 2045
2048 2046 id : <id_given_in_input>
2049 2047 result : null
2050 2048 error : {
2051 2049 "Unable to push changes from `<remote_url>`"
2052 2050 }
2053 2051
2054 2052 """
2055 2053
2056 2054 repo = get_repo_or_error(repoid)
2057 2055 remote_uri = Optional.extract(remote_uri)
2058 2056 remote_uri_display = remote_uri or repo.clone_uri_hidden
2059 2057 if not has_superadmin_permission(apiuser):
2060 2058 _perms = ('repository.admin',)
2061 2059 validate_repo_permissions(apiuser, repoid, repo, _perms)
2062 2060
2063 2061 try:
2064 2062 ScmModel().pull_changes(
2065 2063 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2066 2064 return {
2067 2065 'msg': 'Pulled from url `%s` on repo `%s`' % (
2068 2066 remote_uri_display, repo.repo_name),
2069 2067 'repository': repo.repo_name
2070 2068 }
2071 2069 except Exception:
2072 2070 log.exception("Exception occurred while trying to "
2073 2071 "pull changes from remote location")
2074 2072 raise JSONRPCError(
2075 2073 'Unable to pull changes from `%s`' % remote_uri_display
2076 2074 )
2077 2075
2078 2076
2079 2077 @jsonrpc_method()
2080 2078 def strip(request, apiuser, repoid, revision, branch):
2081 2079 """
2082 2080 Strips the given revision from the specified repository.
2083 2081
2084 2082 * This will remove the revision and all of its decendants.
2085 2083
2086 2084 This command can only be run using an |authtoken| with admin rights to
2087 2085 the specified repository.
2088 2086
2089 2087 This command takes the following options:
2090 2088
2091 2089 :param apiuser: This is filled automatically from the |authtoken|.
2092 2090 :type apiuser: AuthUser
2093 2091 :param repoid: The repository name or repository ID.
2094 2092 :type repoid: str or int
2095 2093 :param revision: The revision you wish to strip.
2096 2094 :type revision: str
2097 2095 :param branch: The branch from which to strip the revision.
2098 2096 :type branch: str
2099 2097
2100 2098 Example output:
2101 2099
2102 2100 .. code-block:: bash
2103 2101
2104 2102 id : <id_given_in_input>
2105 2103 result : {
2106 2104 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2107 2105 "repository": "<repository name>"
2108 2106 }
2109 2107 error : null
2110 2108
2111 2109 Example error output:
2112 2110
2113 2111 .. code-block:: bash
2114 2112
2115 2113 id : <id_given_in_input>
2116 2114 result : null
2117 2115 error : {
2118 2116 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2119 2117 }
2120 2118
2121 2119 """
2122 2120
2123 2121 repo = get_repo_or_error(repoid)
2124 2122 if not has_superadmin_permission(apiuser):
2125 2123 _perms = ('repository.admin',)
2126 2124 validate_repo_permissions(apiuser, repoid, repo, _perms)
2127 2125
2128 2126 try:
2129 2127 ScmModel().strip(repo, revision, branch)
2130 2128 audit_logger.store_api(
2131 2129 'repo.commit.strip', action_data={'commit_id': revision},
2132 2130 repo=repo,
2133 2131 user=apiuser, commit=True)
2134 2132
2135 2133 return {
2136 2134 'msg': 'Stripped commit %s from repo `%s`' % (
2137 2135 revision, repo.repo_name),
2138 2136 'repository': repo.repo_name
2139 2137 }
2140 2138 except Exception:
2141 2139 log.exception("Exception while trying to strip")
2142 2140 raise JSONRPCError(
2143 2141 'Unable to strip commit %s from repo `%s`' % (
2144 2142 revision, repo.repo_name)
2145 2143 )
2146 2144
2147 2145
2148 2146 @jsonrpc_method()
2149 2147 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2150 2148 """
2151 2149 Returns all settings for a repository. If key is given it only returns the
2152 2150 setting identified by the key or null.
2153 2151
2154 2152 :param apiuser: This is filled automatically from the |authtoken|.
2155 2153 :type apiuser: AuthUser
2156 2154 :param repoid: The repository name or repository id.
2157 2155 :type repoid: str or int
2158 2156 :param key: Key of the setting to return.
2159 2157 :type: key: Optional(str)
2160 2158
2161 2159 Example output:
2162 2160
2163 2161 .. code-block:: bash
2164 2162
2165 2163 {
2166 2164 "error": null,
2167 2165 "id": 237,
2168 2166 "result": {
2169 2167 "extensions_largefiles": true,
2170 2168 "extensions_evolve": true,
2171 2169 "hooks_changegroup_push_logger": true,
2172 2170 "hooks_changegroup_repo_size": false,
2173 2171 "hooks_outgoing_pull_logger": true,
2174 2172 "phases_publish": "True",
2175 2173 "rhodecode_hg_use_rebase_for_merging": true,
2176 2174 "rhodecode_pr_merge_enabled": true,
2177 2175 "rhodecode_use_outdated_comments": true
2178 2176 }
2179 2177 }
2180 2178 """
2181 2179
2182 2180 # Restrict access to this api method to admins only.
2183 2181 if not has_superadmin_permission(apiuser):
2184 2182 raise JSONRPCForbidden()
2185 2183
2186 2184 try:
2187 2185 repo = get_repo_or_error(repoid)
2188 2186 settings_model = VcsSettingsModel(repo=repo)
2189 2187 settings = settings_model.get_global_settings()
2190 2188 settings.update(settings_model.get_repo_settings())
2191 2189
2192 2190 # If only a single setting is requested fetch it from all settings.
2193 2191 key = Optional.extract(key)
2194 2192 if key is not None:
2195 2193 settings = settings.get(key, None)
2196 2194 except Exception:
2197 2195 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2198 2196 log.exception(msg)
2199 2197 raise JSONRPCError(msg)
2200 2198
2201 2199 return settings
2202 2200
2203 2201
2204 2202 @jsonrpc_method()
2205 2203 def set_repo_settings(request, apiuser, repoid, settings):
2206 2204 """
2207 2205 Update repository settings. Returns true on success.
2208 2206
2209 2207 :param apiuser: This is filled automatically from the |authtoken|.
2210 2208 :type apiuser: AuthUser
2211 2209 :param repoid: The repository name or repository id.
2212 2210 :type repoid: str or int
2213 2211 :param settings: The new settings for the repository.
2214 2212 :type: settings: dict
2215 2213
2216 2214 Example output:
2217 2215
2218 2216 .. code-block:: bash
2219 2217
2220 2218 {
2221 2219 "error": null,
2222 2220 "id": 237,
2223 2221 "result": true
2224 2222 }
2225 2223 """
2226 2224 # Restrict access to this api method to admins only.
2227 2225 if not has_superadmin_permission(apiuser):
2228 2226 raise JSONRPCForbidden()
2229 2227
2230 2228 if type(settings) is not dict:
2231 2229 raise JSONRPCError('Settings have to be a JSON Object.')
2232 2230
2233 2231 try:
2234 2232 settings_model = VcsSettingsModel(repo=repoid)
2235 2233
2236 2234 # Merge global, repo and incoming settings.
2237 2235 new_settings = settings_model.get_global_settings()
2238 2236 new_settings.update(settings_model.get_repo_settings())
2239 2237 new_settings.update(settings)
2240 2238
2241 2239 # Update the settings.
2242 2240 inherit_global_settings = new_settings.get(
2243 2241 'inherit_global_settings', False)
2244 2242 settings_model.create_or_update_repo_settings(
2245 2243 new_settings, inherit_global_settings=inherit_global_settings)
2246 2244 Session().commit()
2247 2245 except Exception:
2248 2246 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2249 2247 log.exception(msg)
2250 2248 raise JSONRPCError(msg)
2251 2249
2252 2250 # Indicate success.
2253 2251 return True
2254 2252
2255 2253
2256 2254 @jsonrpc_method()
2257 2255 def maintenance(request, apiuser, repoid):
2258 2256 """
2259 2257 Triggers a maintenance on the given repository.
2260 2258
2261 2259 This command can only be run using an |authtoken| with admin
2262 2260 rights to the specified repository. For more information,
2263 2261 see :ref:`config-token-ref`.
2264 2262
2265 2263 This command takes the following options:
2266 2264
2267 2265 :param apiuser: This is filled automatically from the |authtoken|.
2268 2266 :type apiuser: AuthUser
2269 2267 :param repoid: The repository name or repository ID.
2270 2268 :type repoid: str or int
2271 2269
2272 2270 Example output:
2273 2271
2274 2272 .. code-block:: bash
2275 2273
2276 2274 id : <id_given_in_input>
2277 2275 result : {
2278 2276 "msg": "executed maintenance command",
2279 2277 "executed_actions": [
2280 2278 <action_message>, <action_message2>...
2281 2279 ],
2282 2280 "repository": "<repository name>"
2283 2281 }
2284 2282 error : null
2285 2283
2286 2284 Example error output:
2287 2285
2288 2286 .. code-block:: bash
2289 2287
2290 2288 id : <id_given_in_input>
2291 2289 result : null
2292 2290 error : {
2293 2291 "Unable to execute maintenance on `<reponame>`"
2294 2292 }
2295 2293
2296 2294 """
2297 2295
2298 2296 repo = get_repo_or_error(repoid)
2299 2297 if not has_superadmin_permission(apiuser):
2300 2298 _perms = ('repository.admin',)
2301 2299 validate_repo_permissions(apiuser, repoid, repo, _perms)
2302 2300
2303 2301 try:
2304 2302 maintenance = repo_maintenance.RepoMaintenance()
2305 2303 executed_actions = maintenance.execute(repo)
2306 2304
2307 2305 return {
2308 2306 'msg': 'executed maintenance command',
2309 2307 'executed_actions': executed_actions,
2310 2308 'repository': repo.repo_name
2311 2309 }
2312 2310 except Exception:
2313 2311 log.exception("Exception occurred while trying to run maintenance")
2314 2312 raise JSONRPCError(
2315 2313 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,728 +1,734 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23 import operator
24 24
25 25 from pyramid import compat
26 26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 27
28 28 from rhodecode.lib import helpers as h, diffs
29 29 from rhodecode.lib.utils2 import (
30 30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 31 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
32 32 from rhodecode.model import repo
33 33 from rhodecode.model import repo_group
34 34 from rhodecode.model import user_group
35 35 from rhodecode.model import user
36 36 from rhodecode.model.db import User
37 37 from rhodecode.model.scm import ScmModel
38 38 from rhodecode.model.settings import VcsSettingsModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 ADMIN_PREFIX = '/_admin'
44 44 STATIC_FILE_PREFIX = '/_static'
45 45
46 46 URL_NAME_REQUIREMENTS = {
47 47 # group name can have a slash in them, but they must not end with a slash
48 48 'group_name': r'.*?[^/]',
49 49 'repo_group_name': r'.*?[^/]',
50 50 # repo names can have a slash in them, but they must not end with a slash
51 51 'repo_name': r'.*?[^/]',
52 52 # file path eats up everything at the end
53 53 'f_path': r'.*',
54 54 # reference types
55 55 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
56 56 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
57 57 }
58 58
59 59
60 60 def add_route_with_slash(config,name, pattern, **kw):
61 61 config.add_route(name, pattern, **kw)
62 62 if not pattern.endswith('/'):
63 63 config.add_route(name + '_slash', pattern + '/', **kw)
64 64
65 65
66 66 def add_route_requirements(route_path, requirements=None):
67 67 """
68 68 Adds regex requirements to pyramid routes using a mapping dict
69 69 e.g::
70 70 add_route_requirements('{repo_name}/settings')
71 71 """
72 72 requirements = requirements or URL_NAME_REQUIREMENTS
73 73 for key, regex in requirements.items():
74 74 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
75 75 return route_path
76 76
77 77
78 78 def get_format_ref_id(repo):
79 79 """Returns a `repo` specific reference formatter function"""
80 80 if h.is_svn(repo):
81 81 return _format_ref_id_svn
82 82 else:
83 83 return _format_ref_id
84 84
85 85
86 86 def _format_ref_id(name, raw_id):
87 87 """Default formatting of a given reference `name`"""
88 88 return name
89 89
90 90
91 91 def _format_ref_id_svn(name, raw_id):
92 92 """Special way of formatting a reference for Subversion including path"""
93 93 return '%s@%s' % (name, raw_id)
94 94
95 95
96 96 class TemplateArgs(StrictAttributeDict):
97 97 pass
98 98
99 99
100 100 class BaseAppView(object):
101 101
102 102 def __init__(self, context, request):
103 103 self.request = request
104 104 self.context = context
105 105 self.session = request.session
106 106 if not hasattr(request, 'user'):
107 107 # NOTE(marcink): edge case, we ended up in matched route
108 108 # but probably of web-app context, e.g API CALL/VCS CALL
109 109 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
110 110 log.warning('Unable to process request `%s` in this scope', request)
111 111 raise HTTPBadRequest()
112 112
113 113 self._rhodecode_user = request.user # auth user
114 114 self._rhodecode_db_user = self._rhodecode_user.get_instance()
115 115 self._maybe_needs_password_change(
116 116 request.matched_route.name, self._rhodecode_db_user)
117 117
118 118 def _maybe_needs_password_change(self, view_name, user_obj):
119 119 log.debug('Checking if user %s needs password change on view %s',
120 120 user_obj, view_name)
121 121 skip_user_views = [
122 122 'logout', 'login',
123 123 'my_account_password', 'my_account_password_update'
124 124 ]
125 125
126 126 if not user_obj:
127 127 return
128 128
129 129 if user_obj.username == User.DEFAULT_USER:
130 130 return
131 131
132 132 now = time.time()
133 133 should_change = user_obj.user_data.get('force_password_change')
134 134 change_after = safe_int(should_change) or 0
135 135 if should_change and now > change_after:
136 136 log.debug('User %s requires password change', user_obj)
137 137 h.flash('You are required to change your password', 'warning',
138 138 ignore_duplicate=True)
139 139
140 140 if view_name not in skip_user_views:
141 141 raise HTTPFound(
142 142 self.request.route_path('my_account_password'))
143 143
144 144 def _log_creation_exception(self, e, repo_name):
145 145 _ = self.request.translate
146 146 reason = None
147 147 if len(e.args) == 2:
148 148 reason = e.args[1]
149 149
150 150 if reason == 'INVALID_CERTIFICATE':
151 151 log.exception(
152 152 'Exception creating a repository: invalid certificate')
153 153 msg = (_('Error creating repository %s: invalid certificate')
154 154 % repo_name)
155 155 else:
156 156 log.exception("Exception creating a repository")
157 157 msg = (_('Error creating repository %s')
158 158 % repo_name)
159 159 return msg
160 160
161 161 def _get_local_tmpl_context(self, include_app_defaults=True):
162 162 c = TemplateArgs()
163 163 c.auth_user = self.request.user
164 164 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
165 165 c.rhodecode_user = self.request.user
166 166
167 167 if include_app_defaults:
168 168 from rhodecode.lib.base import attach_context_attributes
169 169 attach_context_attributes(c, self.request, self.request.user.user_id)
170 170
171 171 c.is_super_admin = c.auth_user.is_admin
172 172
173 173 c.can_create_repo = c.is_super_admin
174 174 c.can_create_repo_group = c.is_super_admin
175 175 c.can_create_user_group = c.is_super_admin
176 176
177 177 c.is_delegated_admin = False
178 178
179 179 if not c.auth_user.is_default and not c.is_super_admin:
180 180 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
181 181 user=self.request.user)
182 182 repositories = c.auth_user.repositories_admin or c.can_create_repo
183 183
184 184 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
185 185 user=self.request.user)
186 186 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
187 187
188 188 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
189 189 user=self.request.user)
190 190 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
191 191 # delegated admin can create, or manage some objects
192 192 c.is_delegated_admin = repositories or repository_groups or user_groups
193 193 return c
194 194
195 195 def _get_template_context(self, tmpl_args, **kwargs):
196 196
197 197 local_tmpl_args = {
198 198 'defaults': {},
199 199 'errors': {},
200 200 'c': tmpl_args
201 201 }
202 202 local_tmpl_args.update(kwargs)
203 203 return local_tmpl_args
204 204
205 205 def load_default_context(self):
206 206 """
207 207 example:
208 208
209 209 def load_default_context(self):
210 210 c = self._get_local_tmpl_context()
211 211 c.custom_var = 'foobar'
212 212
213 213 return c
214 214 """
215 215 raise NotImplementedError('Needs implementation in view class')
216 216
217 217
218 218 class RepoAppView(BaseAppView):
219 219
220 220 def __init__(self, context, request):
221 221 super(RepoAppView, self).__init__(context, request)
222 222 self.db_repo = request.db_repo
223 223 self.db_repo_name = self.db_repo.repo_name
224 224 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
225 225
226 226 def _handle_missing_requirements(self, error):
227 227 log.error(
228 228 'Requirements are missing for repository %s: %s',
229 229 self.db_repo_name, safe_unicode(error))
230 230
231 231 def _get_local_tmpl_context(self, include_app_defaults=True):
232 232 _ = self.request.translate
233 233 c = super(RepoAppView, self)._get_local_tmpl_context(
234 234 include_app_defaults=include_app_defaults)
235 235
236 236 # register common vars for this type of view
237 237 c.rhodecode_db_repo = self.db_repo
238 238 c.repo_name = self.db_repo_name
239 239 c.repository_pull_requests = self.db_repo_pull_requests
240 240 c.repository_is_user_following = ScmModel().is_following_repo(
241 241 self.db_repo_name, self._rhodecode_user.user_id)
242 242 self.path_filter = PathFilter(None)
243 243
244 244 c.repository_requirements_missing = {}
245 245 try:
246 246 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
247 247 # NOTE(marcink):
248 248 # comparison to None since if it's an object __bool__ is expensive to
249 249 # calculate
250 250 if self.rhodecode_vcs_repo is not None:
251 251 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
252 252 c.auth_user.username)
253 253 self.path_filter = PathFilter(path_perms)
254 254 except RepositoryRequirementError as e:
255 255 c.repository_requirements_missing = {'error': str(e)}
256 256 self._handle_missing_requirements(e)
257 257 self.rhodecode_vcs_repo = None
258 258
259 259 c.path_filter = self.path_filter # used by atom_feed_entry.mako
260 260
261 261 if self.rhodecode_vcs_repo is None:
262 262 # unable to fetch this repo as vcs instance, report back to user
263 263 h.flash(_(
264 264 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
265 265 "Please check if it exist, or is not damaged.") %
266 266 {'repo_name': c.repo_name},
267 267 category='error', ignore_duplicate=True)
268 268 if c.repository_requirements_missing:
269 269 route = self.request.matched_route.name
270 270 if route.startswith(('edit_repo', 'repo_summary')):
271 271 # allow summary and edit repo on missing requirements
272 272 return c
273 273
274 274 raise HTTPFound(
275 275 h.route_path('repo_summary', repo_name=self.db_repo_name))
276 276
277 277 else: # redirect if we don't show missing requirements
278 278 raise HTTPFound(h.route_path('home'))
279 279
280 280 c.has_origin_repo_read_perm = False
281 281 if self.db_repo.fork:
282 282 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
283 283 'repository.write', 'repository.read', 'repository.admin')(
284 284 self.db_repo.fork.repo_name, 'summary fork link')
285 285
286 286 return c
287 287
288 288 def _get_f_path_unchecked(self, matchdict, default=None):
289 289 """
290 290 Should only be used by redirects, everything else should call _get_f_path
291 291 """
292 292 f_path = matchdict.get('f_path')
293 293 if f_path:
294 294 # fix for multiple initial slashes that causes errors for GIT
295 295 return f_path.lstrip('/')
296 296
297 297 return default
298 298
299 299 def _get_f_path(self, matchdict, default=None):
300 300 f_path_match = self._get_f_path_unchecked(matchdict, default)
301 301 return self.path_filter.assert_path_permissions(f_path_match)
302 302
303 303 def _get_general_setting(self, target_repo, settings_key, default=False):
304 304 settings_model = VcsSettingsModel(repo=target_repo)
305 305 settings = settings_model.get_general_settings()
306 306 return settings.get(settings_key, default)
307 307
308 308 def get_recache_flag(self):
309 309 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
310 310 flag_val = self.request.GET.get(flag_name)
311 311 if str2bool(flag_val):
312 312 return True
313 313 return False
314 314
315 315
316 316 class PathFilter(object):
317 317
318 318 # Expects and instance of BasePathPermissionChecker or None
319 319 def __init__(self, permission_checker):
320 320 self.permission_checker = permission_checker
321 321
322 322 def assert_path_permissions(self, path):
323 if path and self.permission_checker and not self.permission_checker.has_access(path):
324 raise HTTPForbidden()
325 return path
323 if self.path_access_allowed(path):
324 return path
325 raise HTTPForbidden()
326
327 def path_access_allowed(self, path):
328 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
329 if self.permission_checker:
330 return path and self.permission_checker.has_access(path)
331 return True
326 332
327 333 def filter_patchset(self, patchset):
328 334 if not self.permission_checker or not patchset:
329 335 return patchset, False
330 336 had_filtered = False
331 337 filtered_patchset = []
332 338 for patch in patchset:
333 339 filename = patch.get('filename', None)
334 340 if not filename or self.permission_checker.has_access(filename):
335 341 filtered_patchset.append(patch)
336 342 else:
337 343 had_filtered = True
338 344 if had_filtered:
339 345 if isinstance(patchset, diffs.LimitedDiffContainer):
340 346 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
341 347 return filtered_patchset, True
342 348 else:
343 349 return patchset, False
344 350
345 351 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
346 352 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
347 353 result = diffset.render_patchset(
348 354 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
349 355 result.has_hidden_changes = has_hidden_changes
350 356 return result
351 357
352 358 def get_raw_patch(self, diff_processor):
353 359 if self.permission_checker is None:
354 360 return diff_processor.as_raw()
355 361 elif self.permission_checker.has_full_access:
356 362 return diff_processor.as_raw()
357 363 else:
358 364 return '# Repository has user-specific filters, raw patch generation is disabled.'
359 365
360 366 @property
361 367 def is_enabled(self):
362 368 return self.permission_checker is not None
363 369
364 370
365 371 class RepoGroupAppView(BaseAppView):
366 372 def __init__(self, context, request):
367 373 super(RepoGroupAppView, self).__init__(context, request)
368 374 self.db_repo_group = request.db_repo_group
369 375 self.db_repo_group_name = self.db_repo_group.group_name
370 376
371 377 def _get_local_tmpl_context(self, include_app_defaults=True):
372 378 _ = self.request.translate
373 379 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
374 380 include_app_defaults=include_app_defaults)
375 381 c.repo_group = self.db_repo_group
376 382 return c
377 383
378 384 def _revoke_perms_on_yourself(self, form_result):
379 385 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
380 386 form_result['perm_updates'])
381 387 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
382 388 form_result['perm_additions'])
383 389 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
384 390 form_result['perm_deletions'])
385 391 admin_perm = 'group.admin'
386 392 if _updates and _updates[0][1] != admin_perm or \
387 393 _additions and _additions[0][1] != admin_perm or \
388 394 _deletions and _deletions[0][1] != admin_perm:
389 395 return True
390 396 return False
391 397
392 398
393 399 class UserGroupAppView(BaseAppView):
394 400 def __init__(self, context, request):
395 401 super(UserGroupAppView, self).__init__(context, request)
396 402 self.db_user_group = request.db_user_group
397 403 self.db_user_group_name = self.db_user_group.users_group_name
398 404
399 405
400 406 class UserAppView(BaseAppView):
401 407 def __init__(self, context, request):
402 408 super(UserAppView, self).__init__(context, request)
403 409 self.db_user = request.db_user
404 410 self.db_user_id = self.db_user.user_id
405 411
406 412 _ = self.request.translate
407 413 if not request.db_user_supports_default:
408 414 if self.db_user.username == User.DEFAULT_USER:
409 415 h.flash(_("Editing user `{}` is disabled.".format(
410 416 User.DEFAULT_USER)), category='warning')
411 417 raise HTTPFound(h.route_path('users'))
412 418
413 419
414 420 class DataGridAppView(object):
415 421 """
416 422 Common class to have re-usable grid rendering components
417 423 """
418 424
419 425 def _extract_ordering(self, request, column_map=None):
420 426 column_map = column_map or {}
421 427 column_index = safe_int(request.GET.get('order[0][column]'))
422 428 order_dir = request.GET.get(
423 429 'order[0][dir]', 'desc')
424 430 order_by = request.GET.get(
425 431 'columns[%s][data][sort]' % column_index, 'name_raw')
426 432
427 433 # translate datatable to DB columns
428 434 order_by = column_map.get(order_by) or order_by
429 435
430 436 search_q = request.GET.get('search[value]')
431 437 return search_q, order_by, order_dir
432 438
433 439 def _extract_chunk(self, request):
434 440 start = safe_int(request.GET.get('start'), 0)
435 441 length = safe_int(request.GET.get('length'), 25)
436 442 draw = safe_int(request.GET.get('draw'))
437 443 return draw, start, length
438 444
439 445 def _get_order_col(self, order_by, model):
440 446 if isinstance(order_by, compat.string_types):
441 447 try:
442 448 return operator.attrgetter(order_by)(model)
443 449 except AttributeError:
444 450 return None
445 451 else:
446 452 return order_by
447 453
448 454
449 455 class BaseReferencesView(RepoAppView):
450 456 """
451 457 Base for reference view for branches, tags and bookmarks.
452 458 """
453 459 def load_default_context(self):
454 460 c = self._get_local_tmpl_context()
455 461
456 462
457 463 return c
458 464
459 465 def load_refs_context(self, ref_items, partials_template):
460 466 _render = self.request.get_partial_renderer(partials_template)
461 467 pre_load = ["author", "date", "message", "parents"]
462 468
463 469 is_svn = h.is_svn(self.rhodecode_vcs_repo)
464 470 is_hg = h.is_hg(self.rhodecode_vcs_repo)
465 471
466 472 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
467 473
468 474 closed_refs = {}
469 475 if is_hg:
470 476 closed_refs = self.rhodecode_vcs_repo.branches_closed
471 477
472 478 data = []
473 479 for ref_name, commit_id in ref_items:
474 480 commit = self.rhodecode_vcs_repo.get_commit(
475 481 commit_id=commit_id, pre_load=pre_load)
476 482 closed = ref_name in closed_refs
477 483
478 484 # TODO: johbo: Unify generation of reference links
479 485 use_commit_id = '/' in ref_name or is_svn
480 486
481 487 if use_commit_id:
482 488 files_url = h.route_path(
483 489 'repo_files',
484 490 repo_name=self.db_repo_name,
485 491 f_path=ref_name if is_svn else '',
486 492 commit_id=commit_id)
487 493
488 494 else:
489 495 files_url = h.route_path(
490 496 'repo_files',
491 497 repo_name=self.db_repo_name,
492 498 f_path=ref_name if is_svn else '',
493 499 commit_id=ref_name,
494 500 _query=dict(at=ref_name))
495 501
496 502 data.append({
497 503 "name": _render('name', ref_name, files_url, closed),
498 504 "name_raw": ref_name,
499 505 "date": _render('date', commit.date),
500 506 "date_raw": datetime_to_time(commit.date),
501 507 "author": _render('author', commit.author),
502 508 "commit": _render(
503 509 'commit', commit.message, commit.raw_id, commit.idx),
504 510 "commit_raw": commit.idx,
505 511 "compare": _render(
506 512 'compare', format_ref_id(ref_name, commit.raw_id)),
507 513 })
508 514
509 515 return data
510 516
511 517
512 518 class RepoRoutePredicate(object):
513 519 def __init__(self, val, config):
514 520 self.val = val
515 521
516 522 def text(self):
517 523 return 'repo_route = %s' % self.val
518 524
519 525 phash = text
520 526
521 527 def __call__(self, info, request):
522 528 if hasattr(request, 'vcs_call'):
523 529 # skip vcs calls
524 530 return
525 531
526 532 repo_name = info['match']['repo_name']
527 533 repo_model = repo.RepoModel()
528 534
529 535 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
530 536
531 537 def redirect_if_creating(route_info, db_repo):
532 538 skip_views = ['edit_repo_advanced_delete']
533 539 route = route_info['route']
534 540 # we should skip delete view so we can actually "remove" repositories
535 541 # if they get stuck in creating state.
536 542 if route.name in skip_views:
537 543 return
538 544
539 545 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
540 546 repo_creating_url = request.route_path(
541 547 'repo_creating', repo_name=db_repo.repo_name)
542 548 raise HTTPFound(repo_creating_url)
543 549
544 550 if by_name_match:
545 551 # register this as request object we can re-use later
546 552 request.db_repo = by_name_match
547 553 redirect_if_creating(info, by_name_match)
548 554 return True
549 555
550 556 by_id_match = repo_model.get_repo_by_id(repo_name)
551 557 if by_id_match:
552 558 request.db_repo = by_id_match
553 559 redirect_if_creating(info, by_id_match)
554 560 return True
555 561
556 562 return False
557 563
558 564
559 565 class RepoForbidArchivedRoutePredicate(object):
560 566 def __init__(self, val, config):
561 567 self.val = val
562 568
563 569 def text(self):
564 570 return 'repo_forbid_archived = %s' % self.val
565 571
566 572 phash = text
567 573
568 574 def __call__(self, info, request):
569 575 _ = request.translate
570 576 rhodecode_db_repo = request.db_repo
571 577
572 578 log.debug(
573 579 '%s checking if archived flag for repo for %s',
574 580 self.__class__.__name__, rhodecode_db_repo.repo_name)
575 581
576 582 if rhodecode_db_repo.archived:
577 583 log.warning('Current view is not supported for archived repo:%s',
578 584 rhodecode_db_repo.repo_name)
579 585
580 586 h.flash(
581 587 h.literal(_('Action not supported for archived repository.')),
582 588 category='warning')
583 589 summary_url = request.route_path(
584 590 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
585 591 raise HTTPFound(summary_url)
586 592 return True
587 593
588 594
589 595 class RepoTypeRoutePredicate(object):
590 596 def __init__(self, val, config):
591 597 self.val = val or ['hg', 'git', 'svn']
592 598
593 599 def text(self):
594 600 return 'repo_accepted_type = %s' % self.val
595 601
596 602 phash = text
597 603
598 604 def __call__(self, info, request):
599 605 if hasattr(request, 'vcs_call'):
600 606 # skip vcs calls
601 607 return
602 608
603 609 rhodecode_db_repo = request.db_repo
604 610
605 611 log.debug(
606 612 '%s checking repo type for %s in %s',
607 613 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
608 614
609 615 if rhodecode_db_repo.repo_type in self.val:
610 616 return True
611 617 else:
612 618 log.warning('Current view is not supported for repo type:%s',
613 619 rhodecode_db_repo.repo_type)
614 620 return False
615 621
616 622
617 623 class RepoGroupRoutePredicate(object):
618 624 def __init__(self, val, config):
619 625 self.val = val
620 626
621 627 def text(self):
622 628 return 'repo_group_route = %s' % self.val
623 629
624 630 phash = text
625 631
626 632 def __call__(self, info, request):
627 633 if hasattr(request, 'vcs_call'):
628 634 # skip vcs calls
629 635 return
630 636
631 637 repo_group_name = info['match']['repo_group_name']
632 638 repo_group_model = repo_group.RepoGroupModel()
633 639 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
634 640
635 641 if by_name_match:
636 642 # register this as request object we can re-use later
637 643 request.db_repo_group = by_name_match
638 644 return True
639 645
640 646 return False
641 647
642 648
643 649 class UserGroupRoutePredicate(object):
644 650 def __init__(self, val, config):
645 651 self.val = val
646 652
647 653 def text(self):
648 654 return 'user_group_route = %s' % self.val
649 655
650 656 phash = text
651 657
652 658 def __call__(self, info, request):
653 659 if hasattr(request, 'vcs_call'):
654 660 # skip vcs calls
655 661 return
656 662
657 663 user_group_id = info['match']['user_group_id']
658 664 user_group_model = user_group.UserGroup()
659 665 by_id_match = user_group_model.get(user_group_id, cache=False)
660 666
661 667 if by_id_match:
662 668 # register this as request object we can re-use later
663 669 request.db_user_group = by_id_match
664 670 return True
665 671
666 672 return False
667 673
668 674
669 675 class UserRoutePredicateBase(object):
670 676 supports_default = None
671 677
672 678 def __init__(self, val, config):
673 679 self.val = val
674 680
675 681 def text(self):
676 682 raise NotImplementedError()
677 683
678 684 def __call__(self, info, request):
679 685 if hasattr(request, 'vcs_call'):
680 686 # skip vcs calls
681 687 return
682 688
683 689 user_id = info['match']['user_id']
684 690 user_model = user.User()
685 691 by_id_match = user_model.get(user_id, cache=False)
686 692
687 693 if by_id_match:
688 694 # register this as request object we can re-use later
689 695 request.db_user = by_id_match
690 696 request.db_user_supports_default = self.supports_default
691 697 return True
692 698
693 699 return False
694 700
695 701
696 702 class UserRoutePredicate(UserRoutePredicateBase):
697 703 supports_default = False
698 704
699 705 def text(self):
700 706 return 'user_route = %s' % self.val
701 707
702 708 phash = text
703 709
704 710
705 711 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
706 712 supports_default = True
707 713
708 714 def text(self):
709 715 return 'user_with_default_route = %s' % self.val
710 716
711 717 phash = text
712 718
713 719
714 720 def includeme(config):
715 721 config.add_route_predicate(
716 722 'repo_route', RepoRoutePredicate)
717 723 config.add_route_predicate(
718 724 'repo_accepted_types', RepoTypeRoutePredicate)
719 725 config.add_route_predicate(
720 726 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
721 727 config.add_route_predicate(
722 728 'repo_group_route', RepoGroupRoutePredicate)
723 729 config.add_route_predicate(
724 730 'user_group_route', UserGroupRoutePredicate)
725 731 config.add_route_predicate(
726 732 'user_route_with_default', UserRouteWithDefaultPredicate)
727 733 config.add_route_predicate(
728 734 'user_route', UserRoutePredicate)
@@ -1,1526 +1,1528 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_rev[1]
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError):
185 185 msg = _('No such commit exists for this repository')
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if not repo.is_empty():
241 241 commit = repo.get_commit(commit_id=commit_id)
242 242 if commit:
243 243 branch_name = commit.branch
244 244 sha_commit_id = commit.raw_id
245 245
246 246 return branch_name, sha_commit_id, is_head
247 247
248 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
249 249
250 250 repo_id = self.db_repo.repo_id
251 251 force_recache = self.get_recache_flag()
252 252
253 253 cache_seconds = safe_int(
254 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 255 cache_on = not force_recache and cache_seconds > 0
256 256 log.debug(
257 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 258 'with caching: %s[TTL: %ss]' % (
259 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 260
261 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 263
264 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
265 265 condition=cache_on)
266 266 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
267 267 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
268 268 ver, repo_id, commit_id, f_path)
269 269
270 270 c.full_load = full_load
271 271 return render(
272 272 'rhodecode:templates/files/files_browser_tree.mako',
273 273 self._get_template_context(c), self.request)
274 274
275 275 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
276 276
277 277 def _get_archive_spec(self, fname):
278 278 log.debug('Detecting archive spec for: `%s`', fname)
279 279
280 280 fileformat = None
281 281 ext = None
282 282 content_type = None
283 283 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
284 284
285 285 if fname.endswith(extension):
286 286 fileformat = a_type
287 287 log.debug('archive is of type: %s', fileformat)
288 288 ext = extension
289 289 break
290 290
291 291 if not fileformat:
292 292 raise ValueError()
293 293
294 294 # left over part of whole fname is the commit
295 295 commit_id = fname[:-len(ext)]
296 296
297 297 return commit_id, ext, fileformat, content_type
298 298
299 299 def create_pure_path(self, *parts):
300 300 # Split paths and sanitize them, removing any ../ etc
301 301 sanitized_path = [
302 302 x for x in pathlib2.PurePath(*parts).parts
303 303 if x not in ['.', '..']]
304 304
305 305 pure_path = pathlib2.PurePath(*sanitized_path)
306 306 return pure_path
307 307
308 308 @LoginRequired()
309 309 @HasRepoPermissionAnyDecorator(
310 310 'repository.read', 'repository.write', 'repository.admin')
311 311 @view_config(
312 312 route_name='repo_archivefile', request_method='GET',
313 313 renderer=None)
314 314 def repo_archivefile(self):
315 315 # archive cache config
316 316 from rhodecode import CONFIG
317 317 _ = self.request.translate
318 318 self.load_default_context()
319 319 default_at_path = '/'
320 320 fname = self.request.matchdict['fname']
321 321 subrepos = self.request.GET.get('subrepos') == 'true'
322 322 at_path = self.request.GET.get('at_path') or default_at_path
323 323
324 324 if not self.db_repo.enable_downloads:
325 325 return Response(_('Downloads disabled'))
326 326
327 327 try:
328 328 commit_id, ext, fileformat, content_type = \
329 329 self._get_archive_spec(fname)
330 330 except ValueError:
331 331 return Response(_('Unknown archive type for: `{}`').format(
332 332 h.escape(fname)))
333 333
334 334 try:
335 335 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
336 336 except CommitDoesNotExistError:
337 337 return Response(_('Unknown commit_id {}').format(
338 338 h.escape(commit_id)))
339 339 except EmptyRepositoryError:
340 340 return Response(_('Empty repository'))
341 341
342 342 try:
343 343 at_path = commit.get_node(at_path).path or default_at_path
344 344 except Exception:
345 345 return Response(_('No node at path {} for this repository').format(at_path))
346 346
347 347 path_sha = sha1(at_path)[:8]
348 348
349 349 # original backward compat name of archive
350 350 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
351 351 short_sha = safe_str(commit.short_id)
352 352
353 353 if at_path == default_at_path:
354 354 archive_name = '{}-{}{}{}'.format(
355 355 clean_name,
356 356 '-sub' if subrepos else '',
357 357 short_sha,
358 358 ext)
359 359 # custom path and new name
360 360 else:
361 361 archive_name = '{}-{}{}-{}{}'.format(
362 362 clean_name,
363 363 '-sub' if subrepos else '',
364 364 short_sha,
365 365 path_sha,
366 366 ext)
367 367
368 368 use_cached_archive = False
369 369 archive_cache_enabled = CONFIG.get(
370 370 'archive_cache_dir') and not self.request.GET.get('no_cache')
371 371 cached_archive_path = None
372 372
373 373 if archive_cache_enabled:
374 374 # check if we it's ok to write
375 375 if not os.path.isdir(CONFIG['archive_cache_dir']):
376 376 os.makedirs(CONFIG['archive_cache_dir'])
377 377 cached_archive_path = os.path.join(
378 378 CONFIG['archive_cache_dir'], archive_name)
379 379 if os.path.isfile(cached_archive_path):
380 380 log.debug('Found cached archive in %s', cached_archive_path)
381 381 fd, archive = None, cached_archive_path
382 382 use_cached_archive = True
383 383 else:
384 384 log.debug('Archive %s is not yet cached', archive_name)
385 385
386 386 if not use_cached_archive:
387 387 # generate new archive
388 388 fd, archive = tempfile.mkstemp()
389 389 log.debug('Creating new temp archive in %s', archive)
390 390 try:
391 391 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
392 392 archive_at_path=at_path)
393 393 except ImproperArchiveTypeError:
394 394 return _('Unknown archive type')
395 395 if archive_cache_enabled:
396 396 # if we generated the archive and we have cache enabled
397 397 # let's use this for future
398 398 log.debug('Storing new archive in %s', cached_archive_path)
399 399 shutil.move(archive, cached_archive_path)
400 400 archive = cached_archive_path
401 401
402 402 # store download action
403 403 audit_logger.store_web(
404 404 'repo.archive.download', action_data={
405 405 'user_agent': self.request.user_agent,
406 406 'archive_name': archive_name,
407 407 'archive_spec': fname,
408 408 'archive_cached': use_cached_archive},
409 409 user=self._rhodecode_user,
410 410 repo=self.db_repo,
411 411 commit=True
412 412 )
413 413
414 414 def get_chunked_archive(archive_path):
415 415 with open(archive_path, 'rb') as stream:
416 416 while True:
417 417 data = stream.read(16 * 1024)
418 418 if not data:
419 419 if fd: # fd means we used temporary file
420 420 os.close(fd)
421 421 if not archive_cache_enabled:
422 422 log.debug('Destroying temp archive %s', archive_path)
423 423 os.remove(archive_path)
424 424 break
425 425 yield data
426 426
427 427 response = Response(app_iter=get_chunked_archive(archive))
428 428 response.content_disposition = str(
429 429 'attachment; filename=%s' % archive_name)
430 430 response.content_type = str(content_type)
431 431
432 432 return response
433 433
434 434 def _get_file_node(self, commit_id, f_path):
435 435 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
436 436 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
437 437 try:
438 438 node = commit.get_node(f_path)
439 439 if node.is_dir():
440 440 raise NodeError('%s path is a %s not a file'
441 441 % (node, type(node)))
442 442 except NodeDoesNotExistError:
443 443 commit = EmptyCommit(
444 444 commit_id=commit_id,
445 445 idx=commit.idx,
446 446 repo=commit.repository,
447 447 alias=commit.repository.alias,
448 448 message=commit.message,
449 449 author=commit.author,
450 450 date=commit.date)
451 451 node = FileNode(f_path, '', commit=commit)
452 452 else:
453 453 commit = EmptyCommit(
454 454 repo=self.rhodecode_vcs_repo,
455 455 alias=self.rhodecode_vcs_repo.alias)
456 456 node = FileNode(f_path, '', commit=commit)
457 457 return node
458 458
459 459 @LoginRequired()
460 460 @HasRepoPermissionAnyDecorator(
461 461 'repository.read', 'repository.write', 'repository.admin')
462 462 @view_config(
463 463 route_name='repo_files_diff', request_method='GET',
464 464 renderer=None)
465 465 def repo_files_diff(self):
466 466 c = self.load_default_context()
467 467 f_path = self._get_f_path(self.request.matchdict)
468 468 diff1 = self.request.GET.get('diff1', '')
469 469 diff2 = self.request.GET.get('diff2', '')
470 470
471 471 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
472 472
473 473 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
474 474 line_context = self.request.GET.get('context', 3)
475 475
476 476 if not any((diff1, diff2)):
477 477 h.flash(
478 478 'Need query parameter "diff1" or "diff2" to generate a diff.',
479 479 category='error')
480 480 raise HTTPBadRequest()
481 481
482 482 c.action = self.request.GET.get('diff')
483 483 if c.action not in ['download', 'raw']:
484 484 compare_url = h.route_path(
485 485 'repo_compare',
486 486 repo_name=self.db_repo_name,
487 487 source_ref_type='rev',
488 488 source_ref=diff1,
489 489 target_repo=self.db_repo_name,
490 490 target_ref_type='rev',
491 491 target_ref=diff2,
492 492 _query=dict(f_path=f_path))
493 493 # redirect to new view if we render diff
494 494 raise HTTPFound(compare_url)
495 495
496 496 try:
497 497 node1 = self._get_file_node(diff1, path1)
498 498 node2 = self._get_file_node(diff2, f_path)
499 499 except (RepositoryError, NodeError):
500 500 log.exception("Exception while trying to get node from repository")
501 501 raise HTTPFound(
502 502 h.route_path('repo_files', repo_name=self.db_repo_name,
503 503 commit_id='tip', f_path=f_path))
504 504
505 505 if all(isinstance(node.commit, EmptyCommit)
506 506 for node in (node1, node2)):
507 507 raise HTTPNotFound()
508 508
509 509 c.commit_1 = node1.commit
510 510 c.commit_2 = node2.commit
511 511
512 512 if c.action == 'download':
513 513 _diff = diffs.get_gitdiff(node1, node2,
514 514 ignore_whitespace=ignore_whitespace,
515 515 context=line_context)
516 516 diff = diffs.DiffProcessor(_diff, format='gitdiff')
517 517
518 518 response = Response(self.path_filter.get_raw_patch(diff))
519 519 response.content_type = 'text/plain'
520 520 response.content_disposition = (
521 521 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
522 522 )
523 523 charset = self._get_default_encoding(c)
524 524 if charset:
525 525 response.charset = charset
526 526 return response
527 527
528 528 elif c.action == 'raw':
529 529 _diff = diffs.get_gitdiff(node1, node2,
530 530 ignore_whitespace=ignore_whitespace,
531 531 context=line_context)
532 532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 533
534 534 response = Response(self.path_filter.get_raw_patch(diff))
535 535 response.content_type = 'text/plain'
536 536 charset = self._get_default_encoding(c)
537 537 if charset:
538 538 response.charset = charset
539 539 return response
540 540
541 541 # in case we ever end up here
542 542 raise HTTPNotFound()
543 543
544 544 @LoginRequired()
545 545 @HasRepoPermissionAnyDecorator(
546 546 'repository.read', 'repository.write', 'repository.admin')
547 547 @view_config(
548 548 route_name='repo_files_diff_2way_redirect', request_method='GET',
549 549 renderer=None)
550 550 def repo_files_diff_2way_redirect(self):
551 551 """
552 552 Kept only to make OLD links work
553 553 """
554 554 f_path = self._get_f_path_unchecked(self.request.matchdict)
555 555 diff1 = self.request.GET.get('diff1', '')
556 556 diff2 = self.request.GET.get('diff2', '')
557 557
558 558 if not any((diff1, diff2)):
559 559 h.flash(
560 560 'Need query parameter "diff1" or "diff2" to generate a diff.',
561 561 category='error')
562 562 raise HTTPBadRequest()
563 563
564 564 compare_url = h.route_path(
565 565 'repo_compare',
566 566 repo_name=self.db_repo_name,
567 567 source_ref_type='rev',
568 568 source_ref=diff1,
569 569 target_ref_type='rev',
570 570 target_ref=diff2,
571 571 _query=dict(f_path=f_path, diffmode='sideside',
572 572 target_repo=self.db_repo_name,))
573 573 raise HTTPFound(compare_url)
574 574
575 575 @LoginRequired()
576 576 @HasRepoPermissionAnyDecorator(
577 577 'repository.read', 'repository.write', 'repository.admin')
578 578 @view_config(
579 579 route_name='repo_files', request_method='GET',
580 580 renderer=None)
581 581 @view_config(
582 582 route_name='repo_files:default_path', request_method='GET',
583 583 renderer=None)
584 584 @view_config(
585 585 route_name='repo_files:default_commit', request_method='GET',
586 586 renderer=None)
587 587 @view_config(
588 588 route_name='repo_files:rendered', request_method='GET',
589 589 renderer=None)
590 590 @view_config(
591 591 route_name='repo_files:annotated', request_method='GET',
592 592 renderer=None)
593 593 def repo_files(self):
594 594 c = self.load_default_context()
595 595
596 596 view_name = getattr(self.request.matched_route, 'name', None)
597 597
598 598 c.annotate = view_name == 'repo_files:annotated'
599 599 # default is false, but .rst/.md files later are auto rendered, we can
600 600 # overwrite auto rendering by setting this GET flag
601 601 c.renderer = view_name == 'repo_files:rendered' or \
602 602 not self.request.GET.get('no-render', False)
603 603
604 604 # redirect to given commit_id from form if given
605 605 get_commit_id = self.request.GET.get('at_rev', None)
606 606 if get_commit_id:
607 607 self._get_commit_or_redirect(get_commit_id)
608 608
609 609 commit_id, f_path = self._get_commit_and_path()
610 610 c.commit = self._get_commit_or_redirect(commit_id)
611 611 c.branch = self.request.GET.get('branch', None)
612 612 c.f_path = f_path
613 613
614 614 # prev link
615 615 try:
616 616 prev_commit = c.commit.prev(c.branch)
617 617 c.prev_commit = prev_commit
618 618 c.url_prev = h.route_path(
619 619 'repo_files', repo_name=self.db_repo_name,
620 620 commit_id=prev_commit.raw_id, f_path=f_path)
621 621 if c.branch:
622 622 c.url_prev += '?branch=%s' % c.branch
623 623 except (CommitDoesNotExistError, VCSError):
624 624 c.url_prev = '#'
625 625 c.prev_commit = EmptyCommit()
626 626
627 627 # next link
628 628 try:
629 629 next_commit = c.commit.next(c.branch)
630 630 c.next_commit = next_commit
631 631 c.url_next = h.route_path(
632 632 'repo_files', repo_name=self.db_repo_name,
633 633 commit_id=next_commit.raw_id, f_path=f_path)
634 634 if c.branch:
635 635 c.url_next += '?branch=%s' % c.branch
636 636 except (CommitDoesNotExistError, VCSError):
637 637 c.url_next = '#'
638 638 c.next_commit = EmptyCommit()
639 639
640 640 # files or dirs
641 641 try:
642 642 c.file = c.commit.get_node(f_path)
643 643 c.file_author = True
644 644 c.file_tree = ''
645 645
646 646 # load file content
647 647 if c.file.is_file():
648 648 c.lf_node = c.file.get_largefile_node()
649 649
650 650 c.file_source_page = 'true'
651 651 c.file_last_commit = c.file.last_commit
652 652 if c.file.size < c.visual.cut_off_limit_diff:
653 653 if c.annotate: # annotation has precedence over renderer
654 654 c.annotated_lines = filenode_as_annotated_lines_tokens(
655 655 c.file
656 656 )
657 657 else:
658 658 c.renderer = (
659 659 c.renderer and h.renderer_from_filename(c.file.path)
660 660 )
661 661 if not c.renderer:
662 662 c.lines = filenode_as_lines_tokens(c.file)
663 663
664 664 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
665 665 commit_id, self.rhodecode_vcs_repo)
666 666 c.on_branch_head = is_head
667 667
668 668 branch = c.commit.branch if (
669 669 c.commit.branch and '/' not in c.commit.branch) else None
670 670 c.branch_or_raw_id = branch or c.commit.raw_id
671 671 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
672 672
673 673 author = c.file_last_commit.author
674 674 c.authors = [[
675 675 h.email(author),
676 676 h.person(author, 'username_or_name_or_email'),
677 677 1
678 678 ]]
679 679
680 680 else: # load tree content at path
681 681 c.file_source_page = 'false'
682 682 c.authors = []
683 683 # this loads a simple tree without metadata to speed things up
684 684 # later via ajax we call repo_nodetree_full and fetch whole
685 685 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
686 686
687 687 except RepositoryError as e:
688 688 h.flash(safe_str(h.escape(e)), category='error')
689 689 raise HTTPNotFound()
690 690
691 691 if self.request.environ.get('HTTP_X_PJAX'):
692 692 html = render('rhodecode:templates/files/files_pjax.mako',
693 693 self._get_template_context(c), self.request)
694 694 else:
695 695 html = render('rhodecode:templates/files/files.mako',
696 696 self._get_template_context(c), self.request)
697 697 return Response(html)
698 698
699 699 @HasRepoPermissionAnyDecorator(
700 700 'repository.read', 'repository.write', 'repository.admin')
701 701 @view_config(
702 702 route_name='repo_files:annotated_previous', request_method='GET',
703 703 renderer=None)
704 704 def repo_files_annotated_previous(self):
705 705 self.load_default_context()
706 706
707 707 commit_id, f_path = self._get_commit_and_path()
708 708 commit = self._get_commit_or_redirect(commit_id)
709 709 prev_commit_id = commit.raw_id
710 710 line_anchor = self.request.GET.get('line_anchor')
711 711 is_file = False
712 712 try:
713 713 _file = commit.get_node(f_path)
714 714 is_file = _file.is_file()
715 715 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
716 716 pass
717 717
718 718 if is_file:
719 719 history = commit.get_path_history(f_path)
720 720 prev_commit_id = history[1].raw_id \
721 721 if len(history) > 1 else prev_commit_id
722 722 prev_url = h.route_path(
723 723 'repo_files:annotated', repo_name=self.db_repo_name,
724 724 commit_id=prev_commit_id, f_path=f_path,
725 725 _anchor='L{}'.format(line_anchor))
726 726
727 727 raise HTTPFound(prev_url)
728 728
729 729 @LoginRequired()
730 730 @HasRepoPermissionAnyDecorator(
731 731 'repository.read', 'repository.write', 'repository.admin')
732 732 @view_config(
733 733 route_name='repo_nodetree_full', request_method='GET',
734 734 renderer=None, xhr=True)
735 735 @view_config(
736 736 route_name='repo_nodetree_full:default_path', request_method='GET',
737 737 renderer=None, xhr=True)
738 738 def repo_nodetree_full(self):
739 739 """
740 740 Returns rendered html of file tree that contains commit date,
741 741 author, commit_id for the specified combination of
742 742 repo, commit_id and file path
743 743 """
744 744 c = self.load_default_context()
745 745
746 746 commit_id, f_path = self._get_commit_and_path()
747 747 commit = self._get_commit_or_redirect(commit_id)
748 748 try:
749 749 dir_node = commit.get_node(f_path)
750 750 except RepositoryError as e:
751 751 return Response('error: {}'.format(h.escape(safe_str(e))))
752 752
753 753 if dir_node.is_file():
754 754 return Response('')
755 755
756 756 c.file = dir_node
757 757 c.commit = commit
758 758
759 759 html = self._get_tree_at_commit(
760 760 c, commit.raw_id, dir_node.path, full_load=True)
761 761
762 762 return Response(html)
763 763
764 764 def _get_attachement_headers(self, f_path):
765 765 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
766 766 safe_path = f_name.replace('"', '\\"')
767 767 encoded_path = urllib.quote(f_name)
768 768
769 769 return "attachment; " \
770 770 "filename=\"{}\"; " \
771 771 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
772 772
773 773 @LoginRequired()
774 774 @HasRepoPermissionAnyDecorator(
775 775 'repository.read', 'repository.write', 'repository.admin')
776 776 @view_config(
777 777 route_name='repo_file_raw', request_method='GET',
778 778 renderer=None)
779 779 def repo_file_raw(self):
780 780 """
781 781 Action for show as raw, some mimetypes are "rendered",
782 782 those include images, icons.
783 783 """
784 784 c = self.load_default_context()
785 785
786 786 commit_id, f_path = self._get_commit_and_path()
787 787 commit = self._get_commit_or_redirect(commit_id)
788 788 file_node = self._get_filenode_or_redirect(commit, f_path)
789 789
790 790 raw_mimetype_mapping = {
791 791 # map original mimetype to a mimetype used for "show as raw"
792 792 # you can also provide a content-disposition to override the
793 793 # default "attachment" disposition.
794 794 # orig_type: (new_type, new_dispo)
795 795
796 796 # show images inline:
797 797 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
798 798 # for example render an SVG with javascript inside or even render
799 799 # HTML.
800 800 'image/x-icon': ('image/x-icon', 'inline'),
801 801 'image/png': ('image/png', 'inline'),
802 802 'image/gif': ('image/gif', 'inline'),
803 803 'image/jpeg': ('image/jpeg', 'inline'),
804 804 'application/pdf': ('application/pdf', 'inline'),
805 805 }
806 806
807 807 mimetype = file_node.mimetype
808 808 try:
809 809 mimetype, disposition = raw_mimetype_mapping[mimetype]
810 810 except KeyError:
811 811 # we don't know anything special about this, handle it safely
812 812 if file_node.is_binary:
813 813 # do same as download raw for binary files
814 814 mimetype, disposition = 'application/octet-stream', 'attachment'
815 815 else:
816 816 # do not just use the original mimetype, but force text/plain,
817 817 # otherwise it would serve text/html and that might be unsafe.
818 818 # Note: underlying vcs library fakes text/plain mimetype if the
819 819 # mimetype can not be determined and it thinks it is not
820 820 # binary.This might lead to erroneous text display in some
821 821 # cases, but helps in other cases, like with text files
822 822 # without extension.
823 823 mimetype, disposition = 'text/plain', 'inline'
824 824
825 825 if disposition == 'attachment':
826 826 disposition = self._get_attachement_headers(f_path)
827 827
828 828 def stream_node():
829 829 yield file_node.raw_bytes
830 830
831 831 response = Response(app_iter=stream_node())
832 832 response.content_disposition = disposition
833 833 response.content_type = mimetype
834 834
835 835 charset = self._get_default_encoding(c)
836 836 if charset:
837 837 response.charset = charset
838 838
839 839 return response
840 840
841 841 @LoginRequired()
842 842 @HasRepoPermissionAnyDecorator(
843 843 'repository.read', 'repository.write', 'repository.admin')
844 844 @view_config(
845 845 route_name='repo_file_download', request_method='GET',
846 846 renderer=None)
847 847 @view_config(
848 848 route_name='repo_file_download:legacy', request_method='GET',
849 849 renderer=None)
850 850 def repo_file_download(self):
851 851 c = self.load_default_context()
852 852
853 853 commit_id, f_path = self._get_commit_and_path()
854 854 commit = self._get_commit_or_redirect(commit_id)
855 855 file_node = self._get_filenode_or_redirect(commit, f_path)
856 856
857 857 if self.request.GET.get('lf'):
858 858 # only if lf get flag is passed, we download this file
859 859 # as LFS/Largefile
860 860 lf_node = file_node.get_largefile_node()
861 861 if lf_node:
862 862 # overwrite our pointer with the REAL large-file
863 863 file_node = lf_node
864 864
865 865 disposition = self._get_attachement_headers(f_path)
866 866
867 867 def stream_node():
868 868 yield file_node.raw_bytes
869 869
870 870 response = Response(app_iter=stream_node())
871 871 response.content_disposition = disposition
872 872 response.content_type = file_node.mimetype
873 873
874 874 charset = self._get_default_encoding(c)
875 875 if charset:
876 876 response.charset = charset
877 877
878 878 return response
879 879
880 880 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
881 881
882 882 cache_seconds = safe_int(
883 883 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
884 884 cache_on = cache_seconds > 0
885 885 log.debug(
886 886 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
887 887 'with caching: %s[TTL: %ss]' % (
888 888 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
889 889
890 890 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
891 891 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
892 892
893 893 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
894 894 condition=cache_on)
895 895 def compute_file_search(repo_id, commit_id, f_path):
896 896 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
897 897 repo_id, commit_id, f_path)
898 898 try:
899 899 _d, _f = ScmModel().get_nodes(
900 900 repo_name, commit_id, f_path, flat=False)
901 901 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
902 902 log.exception(safe_str(e))
903 903 h.flash(safe_str(h.escape(e)), category='error')
904 904 raise HTTPFound(h.route_path(
905 905 'repo_files', repo_name=self.db_repo_name,
906 906 commit_id='tip', f_path='/'))
907
907 908 return _d + _f
908 909
909 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
910 result = compute_file_search(self.db_repo.repo_id, commit_id, f_path)
911 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
910 912
911 913 @LoginRequired()
912 914 @HasRepoPermissionAnyDecorator(
913 915 'repository.read', 'repository.write', 'repository.admin')
914 916 @view_config(
915 917 route_name='repo_files_nodelist', request_method='GET',
916 918 renderer='json_ext', xhr=True)
917 919 def repo_nodelist(self):
918 920 self.load_default_context()
919 921
920 922 commit_id, f_path = self._get_commit_and_path()
921 923 commit = self._get_commit_or_redirect(commit_id)
922 924
923 925 metadata = self._get_nodelist_at_commit(
924 926 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
925 927 return {'nodes': metadata}
926 928
927 929 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
928 930 items = []
929 931 for name, commit_id in branches_or_tags.items():
930 932 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
931 933 items.append((sym_ref, name, ref_type))
932 934 return items
933 935
934 936 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
935 937 return commit_id
936 938
937 939 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
938 940 new_f_path = vcspath.join(name, f_path)
939 941 return u'%s@%s' % (new_f_path, commit_id)
940 942
941 943 def _get_node_history(self, commit_obj, f_path, commits=None):
942 944 """
943 945 get commit history for given node
944 946
945 947 :param commit_obj: commit to calculate history
946 948 :param f_path: path for node to calculate history for
947 949 :param commits: if passed don't calculate history and take
948 950 commits defined in this list
949 951 """
950 952 _ = self.request.translate
951 953
952 954 # calculate history based on tip
953 955 tip = self.rhodecode_vcs_repo.get_commit()
954 956 if commits is None:
955 957 pre_load = ["author", "branch"]
956 958 try:
957 959 commits = tip.get_path_history(f_path, pre_load=pre_load)
958 960 except (NodeDoesNotExistError, CommitError):
959 961 # this node is not present at tip!
960 962 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
961 963
962 964 history = []
963 965 commits_group = ([], _("Changesets"))
964 966 for commit in commits:
965 967 branch = ' (%s)' % commit.branch if commit.branch else ''
966 968 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
967 969 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
968 970 history.append(commits_group)
969 971
970 972 symbolic_reference = self._symbolic_reference
971 973
972 974 if self.rhodecode_vcs_repo.alias == 'svn':
973 975 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
974 976 f_path, self.rhodecode_vcs_repo)
975 977 if adjusted_f_path != f_path:
976 978 log.debug(
977 979 'Recognized svn tag or branch in file "%s", using svn '
978 980 'specific symbolic references', f_path)
979 981 f_path = adjusted_f_path
980 982 symbolic_reference = self._symbolic_reference_svn
981 983
982 984 branches = self._create_references(
983 985 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
984 986 branches_group = (branches, _("Branches"))
985 987
986 988 tags = self._create_references(
987 989 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
988 990 tags_group = (tags, _("Tags"))
989 991
990 992 history.append(branches_group)
991 993 history.append(tags_group)
992 994
993 995 return history, commits
994 996
995 997 @LoginRequired()
996 998 @HasRepoPermissionAnyDecorator(
997 999 'repository.read', 'repository.write', 'repository.admin')
998 1000 @view_config(
999 1001 route_name='repo_file_history', request_method='GET',
1000 1002 renderer='json_ext')
1001 1003 def repo_file_history(self):
1002 1004 self.load_default_context()
1003 1005
1004 1006 commit_id, f_path = self._get_commit_and_path()
1005 1007 commit = self._get_commit_or_redirect(commit_id)
1006 1008 file_node = self._get_filenode_or_redirect(commit, f_path)
1007 1009
1008 1010 if file_node.is_file():
1009 1011 file_history, _hist = self._get_node_history(commit, f_path)
1010 1012
1011 1013 res = []
1012 1014 for obj in file_history:
1013 1015 res.append({
1014 1016 'text': obj[1],
1015 1017 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
1016 1018 })
1017 1019
1018 1020 data = {
1019 1021 'more': False,
1020 1022 'results': res
1021 1023 }
1022 1024 return data
1023 1025
1024 1026 log.warning('Cannot fetch history for directory')
1025 1027 raise HTTPBadRequest()
1026 1028
1027 1029 @LoginRequired()
1028 1030 @HasRepoPermissionAnyDecorator(
1029 1031 'repository.read', 'repository.write', 'repository.admin')
1030 1032 @view_config(
1031 1033 route_name='repo_file_authors', request_method='GET',
1032 1034 renderer='rhodecode:templates/files/file_authors_box.mako')
1033 1035 def repo_file_authors(self):
1034 1036 c = self.load_default_context()
1035 1037
1036 1038 commit_id, f_path = self._get_commit_and_path()
1037 1039 commit = self._get_commit_or_redirect(commit_id)
1038 1040 file_node = self._get_filenode_or_redirect(commit, f_path)
1039 1041
1040 1042 if not file_node.is_file():
1041 1043 raise HTTPBadRequest()
1042 1044
1043 1045 c.file_last_commit = file_node.last_commit
1044 1046 if self.request.GET.get('annotate') == '1':
1045 1047 # use _hist from annotation if annotation mode is on
1046 1048 commit_ids = set(x[1] for x in file_node.annotate)
1047 1049 _hist = (
1048 1050 self.rhodecode_vcs_repo.get_commit(commit_id)
1049 1051 for commit_id in commit_ids)
1050 1052 else:
1051 1053 _f_history, _hist = self._get_node_history(commit, f_path)
1052 1054 c.file_author = False
1053 1055
1054 1056 unique = collections.OrderedDict()
1055 1057 for commit in _hist:
1056 1058 author = commit.author
1057 1059 if author not in unique:
1058 1060 unique[commit.author] = [
1059 1061 h.email(author),
1060 1062 h.person(author, 'username_or_name_or_email'),
1061 1063 1 # counter
1062 1064 ]
1063 1065
1064 1066 else:
1065 1067 # increase counter
1066 1068 unique[commit.author][2] += 1
1067 1069
1068 1070 c.authors = [val for val in unique.values()]
1069 1071
1070 1072 return self._get_template_context(c)
1071 1073
1072 1074 @LoginRequired()
1073 1075 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1074 1076 @view_config(
1075 1077 route_name='repo_files_remove_file', request_method='GET',
1076 1078 renderer='rhodecode:templates/files/files_delete.mako')
1077 1079 def repo_files_remove_file(self):
1078 1080 _ = self.request.translate
1079 1081 c = self.load_default_context()
1080 1082 commit_id, f_path = self._get_commit_and_path()
1081 1083
1082 1084 self._ensure_not_locked()
1083 1085 _branch_name, _sha_commit_id, is_head = \
1084 1086 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1085 1087
1086 1088 self.forbid_non_head(is_head, f_path)
1087 1089 self.check_branch_permission(_branch_name)
1088 1090
1089 1091 c.commit = self._get_commit_or_redirect(commit_id)
1090 1092 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1091 1093
1092 1094 c.default_message = _(
1093 1095 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1094 1096 c.f_path = f_path
1095 1097
1096 1098 return self._get_template_context(c)
1097 1099
1098 1100 @LoginRequired()
1099 1101 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1100 1102 @CSRFRequired()
1101 1103 @view_config(
1102 1104 route_name='repo_files_delete_file', request_method='POST',
1103 1105 renderer=None)
1104 1106 def repo_files_delete_file(self):
1105 1107 _ = self.request.translate
1106 1108
1107 1109 c = self.load_default_context()
1108 1110 commit_id, f_path = self._get_commit_and_path()
1109 1111
1110 1112 self._ensure_not_locked()
1111 1113 _branch_name, _sha_commit_id, is_head = \
1112 1114 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1113 1115
1114 1116 self.forbid_non_head(is_head, f_path)
1115 1117 self.check_branch_permission(_branch_name)
1116 1118
1117 1119 c.commit = self._get_commit_or_redirect(commit_id)
1118 1120 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1119 1121
1120 1122 c.default_message = _(
1121 1123 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1122 1124 c.f_path = f_path
1123 1125 node_path = f_path
1124 1126 author = self._rhodecode_db_user.full_contact
1125 1127 message = self.request.POST.get('message') or c.default_message
1126 1128 try:
1127 1129 nodes = {
1128 1130 node_path: {
1129 1131 'content': ''
1130 1132 }
1131 1133 }
1132 1134 ScmModel().delete_nodes(
1133 1135 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1134 1136 message=message,
1135 1137 nodes=nodes,
1136 1138 parent_commit=c.commit,
1137 1139 author=author,
1138 1140 )
1139 1141
1140 1142 h.flash(
1141 1143 _('Successfully deleted file `{}`').format(
1142 1144 h.escape(f_path)), category='success')
1143 1145 except Exception:
1144 1146 log.exception('Error during commit operation')
1145 1147 h.flash(_('Error occurred during commit'), category='error')
1146 1148 raise HTTPFound(
1147 1149 h.route_path('repo_commit', repo_name=self.db_repo_name,
1148 1150 commit_id='tip'))
1149 1151
1150 1152 @LoginRequired()
1151 1153 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1152 1154 @view_config(
1153 1155 route_name='repo_files_edit_file', request_method='GET',
1154 1156 renderer='rhodecode:templates/files/files_edit.mako')
1155 1157 def repo_files_edit_file(self):
1156 1158 _ = self.request.translate
1157 1159 c = self.load_default_context()
1158 1160 commit_id, f_path = self._get_commit_and_path()
1159 1161
1160 1162 self._ensure_not_locked()
1161 1163 _branch_name, _sha_commit_id, is_head = \
1162 1164 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1163 1165
1164 1166 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1165 1167 self.check_branch_permission(_branch_name, commit_id=commit_id)
1166 1168
1167 1169 c.commit = self._get_commit_or_redirect(commit_id)
1168 1170 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1169 1171
1170 1172 if c.file.is_binary:
1171 1173 files_url = h.route_path(
1172 1174 'repo_files',
1173 1175 repo_name=self.db_repo_name,
1174 1176 commit_id=c.commit.raw_id, f_path=f_path)
1175 1177 raise HTTPFound(files_url)
1176 1178
1177 1179 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1178 1180 c.f_path = f_path
1179 1181
1180 1182 return self._get_template_context(c)
1181 1183
1182 1184 @LoginRequired()
1183 1185 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1184 1186 @CSRFRequired()
1185 1187 @view_config(
1186 1188 route_name='repo_files_update_file', request_method='POST',
1187 1189 renderer=None)
1188 1190 def repo_files_update_file(self):
1189 1191 _ = self.request.translate
1190 1192 c = self.load_default_context()
1191 1193 commit_id, f_path = self._get_commit_and_path()
1192 1194
1193 1195 self._ensure_not_locked()
1194 1196
1195 1197 c.commit = self._get_commit_or_redirect(commit_id)
1196 1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1197 1199
1198 1200 if c.file.is_binary:
1199 1201 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1200 1202 commit_id=c.commit.raw_id, f_path=f_path))
1201 1203
1202 1204 _branch_name, _sha_commit_id, is_head = \
1203 1205 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1204 1206
1205 1207 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1206 1208 self.check_branch_permission(_branch_name, commit_id=commit_id)
1207 1209
1208 1210 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1209 1211 c.f_path = f_path
1210 1212
1211 1213 old_content = c.file.content
1212 1214 sl = old_content.splitlines(1)
1213 1215 first_line = sl[0] if sl else ''
1214 1216
1215 1217 r_post = self.request.POST
1216 1218 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1217 1219 line_ending_mode = detect_mode(first_line, 0)
1218 1220 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1219 1221
1220 1222 message = r_post.get('message') or c.default_message
1221 1223 org_node_path = c.file.unicode_path
1222 1224 filename = r_post['filename']
1223 1225
1224 1226 root_path = c.file.dir_path
1225 1227 pure_path = self.create_pure_path(root_path, filename)
1226 1228 node_path = safe_unicode(bytes(pure_path))
1227 1229
1228 1230 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1229 1231 commit_id=commit_id)
1230 1232 if content == old_content and node_path == org_node_path:
1231 1233 h.flash(_('No changes detected on {}').format(org_node_path),
1232 1234 category='warning')
1233 1235 raise HTTPFound(default_redirect_url)
1234 1236
1235 1237 try:
1236 1238 mapping = {
1237 1239 org_node_path: {
1238 1240 'org_filename': org_node_path,
1239 1241 'filename': node_path,
1240 1242 'content': content,
1241 1243 'lexer': '',
1242 1244 'op': 'mod',
1243 1245 'mode': c.file.mode
1244 1246 }
1245 1247 }
1246 1248
1247 1249 commit = ScmModel().update_nodes(
1248 1250 user=self._rhodecode_db_user.user_id,
1249 1251 repo=self.db_repo,
1250 1252 message=message,
1251 1253 nodes=mapping,
1252 1254 parent_commit=c.commit,
1253 1255 )
1254 1256
1255 1257 h.flash(_('Successfully committed changes to file `{}`').format(
1256 1258 h.escape(f_path)), category='success')
1257 1259 default_redirect_url = h.route_path(
1258 1260 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1259 1261
1260 1262 except Exception:
1261 1263 log.exception('Error occurred during commit')
1262 1264 h.flash(_('Error occurred during commit'), category='error')
1263 1265
1264 1266 raise HTTPFound(default_redirect_url)
1265 1267
1266 1268 @LoginRequired()
1267 1269 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1268 1270 @view_config(
1269 1271 route_name='repo_files_add_file', request_method='GET',
1270 1272 renderer='rhodecode:templates/files/files_add.mako')
1271 1273 @view_config(
1272 1274 route_name='repo_files_upload_file', request_method='GET',
1273 1275 renderer='rhodecode:templates/files/files_upload.mako')
1274 1276 def repo_files_add_file(self):
1275 1277 _ = self.request.translate
1276 1278 c = self.load_default_context()
1277 1279 commit_id, f_path = self._get_commit_and_path()
1278 1280
1279 1281 self._ensure_not_locked()
1280 1282
1281 1283 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1282 1284 if c.commit is None:
1283 1285 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1284 1286
1285 1287 if self.rhodecode_vcs_repo.is_empty():
1286 1288 # for empty repository we cannot check for current branch, we rely on
1287 1289 # c.commit.branch instead
1288 1290 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1289 1291 else:
1290 1292 _branch_name, _sha_commit_id, is_head = \
1291 1293 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1292 1294
1293 1295 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1294 1296 self.check_branch_permission(_branch_name, commit_id=commit_id)
1295 1297
1296 1298 c.default_message = (_('Added file via RhodeCode Enterprise'))
1297 1299 c.f_path = f_path.lstrip('/') # ensure not relative path
1298 1300
1299 1301 return self._get_template_context(c)
1300 1302
1301 1303 @LoginRequired()
1302 1304 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1303 1305 @CSRFRequired()
1304 1306 @view_config(
1305 1307 route_name='repo_files_create_file', request_method='POST',
1306 1308 renderer=None)
1307 1309 def repo_files_create_file(self):
1308 1310 _ = self.request.translate
1309 1311 c = self.load_default_context()
1310 1312 commit_id, f_path = self._get_commit_and_path()
1311 1313
1312 1314 self._ensure_not_locked()
1313 1315
1314 1316 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1315 1317 if c.commit is None:
1316 1318 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1317 1319
1318 1320 # calculate redirect URL
1319 1321 if self.rhodecode_vcs_repo.is_empty():
1320 1322 default_redirect_url = h.route_path(
1321 1323 'repo_summary', repo_name=self.db_repo_name)
1322 1324 else:
1323 1325 default_redirect_url = h.route_path(
1324 1326 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1325 1327
1326 1328 if self.rhodecode_vcs_repo.is_empty():
1327 1329 # for empty repository we cannot check for current branch, we rely on
1328 1330 # c.commit.branch instead
1329 1331 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1330 1332 else:
1331 1333 _branch_name, _sha_commit_id, is_head = \
1332 1334 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1333 1335
1334 1336 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1335 1337 self.check_branch_permission(_branch_name, commit_id=commit_id)
1336 1338
1337 1339 c.default_message = (_('Added file via RhodeCode Enterprise'))
1338 1340 c.f_path = f_path
1339 1341
1340 1342 r_post = self.request.POST
1341 1343 message = r_post.get('message') or c.default_message
1342 1344 filename = r_post.get('filename')
1343 1345 unix_mode = 0
1344 1346 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1345 1347
1346 1348 if not filename:
1347 1349 # If there's no commit, redirect to repo summary
1348 1350 if type(c.commit) is EmptyCommit:
1349 1351 redirect_url = h.route_path(
1350 1352 'repo_summary', repo_name=self.db_repo_name)
1351 1353 else:
1352 1354 redirect_url = default_redirect_url
1353 1355 h.flash(_('No filename specified'), category='warning')
1354 1356 raise HTTPFound(redirect_url)
1355 1357
1356 1358 root_path = f_path
1357 1359 pure_path = self.create_pure_path(root_path, filename)
1358 1360 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1359 1361
1360 1362 author = self._rhodecode_db_user.full_contact
1361 1363 nodes = {
1362 1364 node_path: {
1363 1365 'content': content
1364 1366 }
1365 1367 }
1366 1368
1367 1369 try:
1368 1370
1369 1371 commit = ScmModel().create_nodes(
1370 1372 user=self._rhodecode_db_user.user_id,
1371 1373 repo=self.db_repo,
1372 1374 message=message,
1373 1375 nodes=nodes,
1374 1376 parent_commit=c.commit,
1375 1377 author=author,
1376 1378 )
1377 1379
1378 1380 h.flash(_('Successfully committed new file `{}`').format(
1379 1381 h.escape(node_path)), category='success')
1380 1382
1381 1383 default_redirect_url = h.route_path(
1382 1384 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1383 1385
1384 1386 except NonRelativePathError:
1385 1387 log.exception('Non Relative path found')
1386 1388 h.flash(_('The location specified must be a relative path and must not '
1387 1389 'contain .. in the path'), category='warning')
1388 1390 raise HTTPFound(default_redirect_url)
1389 1391 except (NodeError, NodeAlreadyExistsError) as e:
1390 1392 h.flash(_(h.escape(e)), category='error')
1391 1393 except Exception:
1392 1394 log.exception('Error occurred during commit')
1393 1395 h.flash(_('Error occurred during commit'), category='error')
1394 1396
1395 1397 raise HTTPFound(default_redirect_url)
1396 1398
1397 1399 @LoginRequired()
1398 1400 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1399 1401 @CSRFRequired()
1400 1402 @view_config(
1401 1403 route_name='repo_files_upload_file', request_method='POST',
1402 1404 renderer='json_ext')
1403 1405 def repo_files_upload_file(self):
1404 1406 _ = self.request.translate
1405 1407 c = self.load_default_context()
1406 1408 commit_id, f_path = self._get_commit_and_path()
1407 1409
1408 1410 self._ensure_not_locked()
1409 1411
1410 1412 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1411 1413 if c.commit is None:
1412 1414 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1413 1415
1414 1416 # calculate redirect URL
1415 1417 if self.rhodecode_vcs_repo.is_empty():
1416 1418 default_redirect_url = h.route_path(
1417 1419 'repo_summary', repo_name=self.db_repo_name)
1418 1420 else:
1419 1421 default_redirect_url = h.route_path(
1420 1422 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1421 1423
1422 1424 if self.rhodecode_vcs_repo.is_empty():
1423 1425 # for empty repository we cannot check for current branch, we rely on
1424 1426 # c.commit.branch instead
1425 1427 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1426 1428 else:
1427 1429 _branch_name, _sha_commit_id, is_head = \
1428 1430 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1429 1431
1430 1432 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1431 1433 if error:
1432 1434 return {
1433 1435 'error': error,
1434 1436 'redirect_url': default_redirect_url
1435 1437 }
1436 1438 error = self.check_branch_permission(_branch_name, json_mode=True)
1437 1439 if error:
1438 1440 return {
1439 1441 'error': error,
1440 1442 'redirect_url': default_redirect_url
1441 1443 }
1442 1444
1443 1445 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1444 1446 c.f_path = f_path
1445 1447
1446 1448 r_post = self.request.POST
1447 1449
1448 1450 message = c.default_message
1449 1451 user_message = r_post.getall('message')
1450 1452 if isinstance(user_message, list) and user_message:
1451 1453 # we take the first from duplicated results if it's not empty
1452 1454 message = user_message[0] if user_message[0] else message
1453 1455
1454 1456 nodes = {}
1455 1457
1456 1458 for file_obj in r_post.getall('files_upload') or []:
1457 1459 content = file_obj.file
1458 1460 filename = file_obj.filename
1459 1461
1460 1462 root_path = f_path
1461 1463 pure_path = self.create_pure_path(root_path, filename)
1462 1464 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1463 1465
1464 1466 nodes[node_path] = {
1465 1467 'content': content
1466 1468 }
1467 1469
1468 1470 if not nodes:
1469 1471 error = 'missing files'
1470 1472 return {
1471 1473 'error': error,
1472 1474 'redirect_url': default_redirect_url
1473 1475 }
1474 1476
1475 1477 author = self._rhodecode_db_user.full_contact
1476 1478
1477 1479 try:
1478 1480 commit = ScmModel().create_nodes(
1479 1481 user=self._rhodecode_db_user.user_id,
1480 1482 repo=self.db_repo,
1481 1483 message=message,
1482 1484 nodes=nodes,
1483 1485 parent_commit=c.commit,
1484 1486 author=author,
1485 1487 )
1486 1488 if len(nodes) == 1:
1487 1489 flash_message = _('Successfully committed {} new files').format(len(nodes))
1488 1490 else:
1489 1491 flash_message = _('Successfully committed 1 new file')
1490 1492
1491 1493 h.flash(flash_message, category='success')
1492 1494
1493 1495 default_redirect_url = h.route_path(
1494 1496 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1495 1497
1496 1498 except NonRelativePathError:
1497 1499 log.exception('Non Relative path found')
1498 1500 error = _('The location specified must be a relative path and must not '
1499 1501 'contain .. in the path')
1500 1502 h.flash(error, category='warning')
1501 1503
1502 1504 return {
1503 1505 'error': error,
1504 1506 'redirect_url': default_redirect_url
1505 1507 }
1506 1508 except (NodeError, NodeAlreadyExistsError) as e:
1507 1509 error = h.escape(e)
1508 1510 h.flash(error, category='error')
1509 1511
1510 1512 return {
1511 1513 'error': error,
1512 1514 'redirect_url': default_redirect_url
1513 1515 }
1514 1516 except Exception:
1515 1517 log.exception('Error occurred during commit')
1516 1518 error = _('Error occurred during commit')
1517 1519 h.flash(error, category='error')
1518 1520 return {
1519 1521 'error': error,
1520 1522 'redirect_url': default_redirect_url
1521 1523 }
1522 1524
1523 1525 return {
1524 1526 'error': None,
1525 1527 'redirect_url': default_redirect_url
1526 1528 }
General Comments 0
You need to be logged in to leave comments. Login now