repo_api.py
2534 lines
| 87.0 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2011-2023 RhodeCode GmbH | |||
r1 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
import logging | ||||
import time | ||||
r1153 | import rhodecode | |||
from rhodecode.api import ( | ||||
jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) | ||||
r1 | from rhodecode.api.utils import ( | |||
has_superadmin_permission, Optional, OAttr, get_repo_or_error, | ||||
r1153 | get_user_group_or_error, get_user_or_error, validate_repo_permissions, | |||
get_perm_or_error, parse_args, get_origin, build_commit_data, | ||||
validate_set_owner_permissions) | ||||
r4505 | from rhodecode.lib import audit_logger, rc_cache, channelstream | |||
r1742 | from rhodecode.lib import repo_maintenance | |||
r4440 | from rhodecode.lib.auth import ( | |||
HasPermissionAnyApi, HasUserGroupPermissionAnyApi, | ||||
HasRepoPermissionAnyApi) | ||||
r2359 | from rhodecode.lib.celerylib.utils import get_task_id | |||
r4440 | from rhodecode.lib.utils2 import ( | |||
r5047 | str2bool, time_to_datetime, safe_str, safe_int) | |||
r1153 | from rhodecode.lib.ext_json import json | |||
r4440 | from rhodecode.lib.exceptions import ( | |||
StatusChangeOnClosedPullRequestError, CommentVersionMismatch) | ||||
r3462 | from rhodecode.lib.vcs import RepositoryError | |||
r3490 | from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError | |||
r1 | from rhodecode.model.changeset_status import ChangesetStatusModel | |||
r1323 | from rhodecode.model.comment import CommentsModel | |||
r1 | from rhodecode.model.db import ( | |||
r1337 | Session, ChangesetStatus, RepositoryField, Repository, RepoGroup, | |||
ChangesetComment) | ||||
r3824 | from rhodecode.model.permission import PermissionModel | |||
r4444 | from rhodecode.model.pull_request import PullRequestModel | |||
r1 | from rhodecode.model.repo import RepoModel | |||
from rhodecode.model.scm import ScmModel, RepoList | ||||
Martin Bornhold
|
r387 | from rhodecode.model.settings import SettingsModel, VcsSettingsModel | ||
r1153 | from rhodecode.model import validation_schema | |||
r523 | from rhodecode.model.validation_schema.schemas import repo_schema | |||
r1 | ||||
log = logging.getLogger(__name__) | ||||
@jsonrpc_method() | ||||
def get_repo(request, apiuser, repoid, cache=Optional(True)): | ||||
""" | ||||
Gets an existing repository by its name or repository_id. | ||||
The members section so the output returns users groups or users | ||||
associated with that repository. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository id. | ||||
:type repoid: str or int | ||||
:param cache: use the cached value for last changeset | ||||
:type: cache: Optional(bool) | ||||
Example output: | ||||
.. code-block:: bash | ||||
{ | ||||
"error": null, | ||||
"id": <repo_id>, | ||||
"result": { | ||||
"clone_uri": null, | ||||
"created_on": "timestamp", | ||||
"description": "repo description", | ||||
"enable_downloads": false, | ||||
"enable_locking": false, | ||||
"enable_statistics": false, | ||||
"followers": [ | ||||
{ | ||||
"active": true, | ||||
"admin": false, | ||||
"api_key": "****************************************", | ||||
"api_keys": [ | ||||
"****************************************" | ||||
], | ||||
"email": "user@example.com", | ||||
"emails": [ | ||||
"user@example.com" | ||||
], | ||||
"extern_name": "rhodecode", | ||||
"extern_type": "rhodecode", | ||||
"firstname": "username", | ||||
"ip_addresses": [], | ||||
"language": null, | ||||
"last_login": "2015-09-16T17:16:35.854", | ||||
"lastname": "surname", | ||||
"user_id": <user_id>, | ||||
"username": "name" | ||||
} | ||||
], | ||||
"fork_of": "parent-repo", | ||||
"landing_rev": [ | ||||
"rev", | ||||
"tip" | ||||
], | ||||
"last_changeset": { | ||||
"author": "User <user@example.com>", | ||||
"branch": "default", | ||||
"date": "timestamp", | ||||
"message": "last commit message", | ||||
"parents": [ | ||||
{ | ||||
"raw_id": "commit-id" | ||||
} | ||||
], | ||||
"raw_id": "commit-id", | ||||
"revision": <revision number>, | ||||
"short_id": "short id" | ||||
}, | ||||
"lock_reason": null, | ||||
"locked_by": null, | ||||
"locked_date": null, | ||||
"owner": "owner-name", | ||||
"permissions": [ | ||||
{ | ||||
"name": "super-admin-name", | ||||
"origin": "super-admin", | ||||
"permission": "repository.admin", | ||||
"type": "user" | ||||
}, | ||||
{ | ||||
"name": "owner-name", | ||||
"origin": "owner", | ||||
"permission": "repository.admin", | ||||
"type": "user" | ||||
}, | ||||
{ | ||||
"name": "user-group-name", | ||||
"origin": "permission", | ||||
"permission": "repository.write", | ||||
"type": "user_group" | ||||
} | ||||
], | ||||
"private": true, | ||||
"repo_id": 676, | ||||
"repo_name": "user-group/repo-name", | ||||
"repo_type": "hg" | ||||
} | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
cache = Optional.extract(cache) | ||||
r1153 | ||||
r1 | include_secrets = False | |||
if has_superadmin_permission(apiuser): | ||||
include_secrets = True | ||||
else: | ||||
# check if we have at least read permission for this repo ! | ||||
_perms = ( | ||||
'repository.admin', 'repository.write', 'repository.read',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
permissions = [] | ||||
for _user in repo.permissions(): | ||||
user_data = { | ||||
'name': _user.username, | ||||
'permission': _user.permission, | ||||
'origin': get_origin(_user), | ||||
'type': "user", | ||||
} | ||||
permissions.append(user_data) | ||||
for _user_group in repo.permission_user_groups(): | ||||
user_group_data = { | ||||
'name': _user_group.users_group_name, | ||||
'permission': _user_group.permission, | ||||
'origin': get_origin(_user_group), | ||||
'type': "user_group", | ||||
} | ||||
permissions.append(user_group_data) | ||||
following_users = [ | ||||
user.user.get_api_data(include_secrets=include_secrets) | ||||
for user in repo.followers] | ||||
if not cache: | ||||
repo.update_commit_cache() | ||||
data = repo.get_api_data(include_secrets=include_secrets) | ||||
data['permissions'] = permissions | ||||
data['followers'] = following_users | ||||
r5047 | ||||
r1 | return data | |||
@jsonrpc_method() | ||||
r1267 | def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)): | |||
r1 | """ | |||
Lists all existing repositories. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
r1267 | :param root: specify root repository group to fetch repositories. | |||
filters the returned repositories to be members of given root group. | ||||
:type root: Optional(None) | ||||
:param traverse: traverse given root into subrepositories. With this flag | ||||
set to False, it will only return top-level repositories from `root`. | ||||
if root is empty it will return just top-level repositories. | ||||
:type traverse: Optional(True) | ||||
r1 | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: [ | ||||
{ | ||||
"repo_id" : "<repo_id>", | ||||
"repo_name" : "<reponame>" | ||||
"repo_type" : "<repo_type>", | ||||
"clone_uri" : "<clone_uri>", | ||||
"private": : "<bool>", | ||||
"created_on" : "<datetimecreated>", | ||||
"description" : "<description>", | ||||
"landing_rev": "<landing_rev>", | ||||
"owner": "<repo_owner>", | ||||
"fork_of": "<name_of_fork_parent>", | ||||
"enable_downloads": "<bool>", | ||||
"enable_locking": "<bool>", | ||||
"enable_statistics": "<bool>", | ||||
}, | ||||
... | ||||
] | ||||
error: null | ||||
""" | ||||
include_secrets = has_superadmin_permission(apiuser) | ||||
_perms = ('repository.read', 'repository.write', 'repository.admin',) | ||||
extras = {'user': apiuser} | ||||
r1267 | root = Optional.extract(root) | |||
traverse = Optional.extract(traverse, binary=True) | ||||
if root: | ||||
# verify parent existance, if it's empty return an error | ||||
parent = RepoGroup.get_by_group_name(root) | ||||
if not parent: | ||||
raise JSONRPCError( | ||||
r5092 | f'Root repository group `{root}` does not exist') | |||
r1267 | ||||
if traverse: | ||||
repos = RepoModel().get_repos_for_root(root=root, traverse=traverse) | ||||
else: | ||||
repos = RepoModel().get_repos_for_root(root=parent) | ||||
else: | ||||
if traverse: | ||||
repos = RepoModel().get_all() | ||||
else: | ||||
# return just top-level | ||||
repos = RepoModel().get_repos_for_root(root=None) | ||||
repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras) | ||||
r1 | return [repo.get_api_data(include_secrets=include_secrets) | |||
for repo in repo_list] | ||||
@jsonrpc_method() | ||||
def get_repo_changeset(request, apiuser, repoid, revision, | ||||
details=Optional('basic')): | ||||
""" | ||||
Returns information about a changeset. | ||||
Additionally parameters define the amount of details returned by | ||||
this function. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository id | ||||
:type repoid: str or int | ||||
:param revision: revision for which listing should be done | ||||
:type revision: str | ||||
:param details: details can be 'basic|extended|full' full gives diff | ||||
info details like the diff itself, and number of changed files etc. | ||||
:type details: Optional(str) | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
r4474 | _perms = ('repository.admin', 'repository.write', 'repository.read',) | |||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
changes_details = Optional.extract(details) | ||||
_changes_details_types = ['basic', 'extended', 'full'] | ||||
if changes_details not in _changes_details_types: | ||||
raise JSONRPCError( | ||||
'ret_type must be one of %s' % ( | ||||
','.join(_changes_details_types))) | ||||
r4531 | vcs_repo = repo.scm_instance() | |||
r1 | pre_load = ['author', 'branch', 'date', 'message', 'parents', | |||
'status', '_commit', '_file_paths'] | ||||
try: | ||||
r4531 | commit = repo.get_commit(commit_id=revision, pre_load=pre_load) | |||
r1 | except TypeError as e: | |||
r3104 | raise JSONRPCError(safe_str(e)) | |||
r4531 | _cs_json = commit.__json__() | |||
_cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details) | ||||
r1 | if changes_details == 'full': | |||
r4531 | _cs_json['refs'] = commit._get_refs() | |||
r1 | return _cs_json | |||
@jsonrpc_method() | ||||
def get_repo_changesets(request, apiuser, repoid, start_rev, limit, | ||||
details=Optional('basic')): | ||||
""" | ||||
r61 | Returns a set of commits limited by the number starting | |||
r1 | from the `start_rev` option. | |||
Additional parameters define the amount of details returned by this | ||||
function. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param start_rev: The starting revision from where to get changesets. | ||||
:type start_rev: str | ||||
r61 | :param limit: Limit the number of commits to this amount | |||
r1 | :type limit: str or int | |||
:param details: Set the level of detail returned. Valid option are: | ||||
``basic``, ``extended`` and ``full``. | ||||
:type details: Optional(str) | ||||
.. note:: | ||||
Setting the parameter `details` to the value ``full`` is extensive | ||||
and returns details like the diff itself, and the number | ||||
of changed files. | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
r4474 | _perms = ('repository.admin', 'repository.write', 'repository.read',) | |||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
changes_details = Optional.extract(details) | ||||
_changes_details_types = ['basic', 'extended', 'full'] | ||||
if changes_details not in _changes_details_types: | ||||
raise JSONRPCError( | ||||
'ret_type must be one of %s' % ( | ||||
','.join(_changes_details_types))) | ||||
limit = int(limit) | ||||
pre_load = ['author', 'branch', 'date', 'message', 'parents', | ||||
'status', '_commit', '_file_paths'] | ||||
vcs_repo = repo.scm_instance() | ||||
# SVN needs a special case to distinguish its index and commit id | ||||
r61 | if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'): | |||
r1 | start_rev = vcs_repo.commit_ids[0] | |||
try: | ||||
r61 | commits = vcs_repo.get_commits( | |||
r3468 | start_id=start_rev, pre_load=pre_load, translate_tags=False) | |||
r1 | except TypeError as e: | |||
r3104 | raise JSONRPCError(safe_str(e)) | |||
r61 | except Exception: | |||
log.exception('Fetching of commits failed') | ||||
raise JSONRPCError('Error occurred during commit fetching') | ||||
r1 | ||||
ret = [] | ||||
for cnt, commit in enumerate(commits): | ||||
if cnt >= limit != -1: | ||||
break | ||||
_cs_json = commit.__json__() | ||||
r4531 | _cs_json['diff'] = build_commit_data(vcs_repo, commit, changes_details) | |||
r1 | if changes_details == 'full': | |||
_cs_json['refs'] = { | ||||
'branches': [commit.branch], | ||||
'bookmarks': getattr(commit, 'bookmarks', []), | ||||
'tags': commit.tags | ||||
} | ||||
ret.append(_cs_json) | ||||
return ret | ||||
@jsonrpc_method() | ||||
def get_repo_nodes(request, apiuser, repoid, revision, root_path, | ||||
r502 | ret_type=Optional('all'), details=Optional('basic'), | |||
max_file_bytes=Optional(None)): | ||||
r1 | """ | |||
Returns a list of nodes and children in a flat list for a given | ||||
path at given revision. | ||||
It's possible to specify ret_type to show only `files` or `dirs`. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param revision: The revision for which listing should be done. | ||||
:type revision: str | ||||
:param root_path: The path from which to start displaying. | ||||
:type root_path: str | ||||
:param ret_type: Set the return type. Valid options are | ||||
``all`` (default), ``files`` and ``dirs``. | ||||
:type ret_type: Optional(str) | ||||
:param details: Returns extended information about nodes, such as | ||||
r3460 | md5, binary, and or content. | |||
The valid options are ``basic`` and ``full``. | ||||
r1 | :type details: Optional(str) | |||
r502 | :param max_file_bytes: Only return file content under this file size bytes | |||
:type details: Optional(int) | ||||
r1 | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: [ | ||||
r3460 | { | |||
"binary": false, | ||||
r3693 | "content": "File line", | |||
r3460 | "extension": "md", | |||
"lines": 2, | ||||
"md5": "059fa5d29b19c0657e384749480f6422", | ||||
"mimetype": "text/x-minidsrc", | ||||
"name": "file.md", | ||||
"size": 580, | ||||
"type": "file" | ||||
}, | ||||
r1 | ... | |||
] | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
r3460 | _perms = ('repository.admin', 'repository.write', 'repository.read',) | |||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
ret_type = Optional.extract(ret_type) | ||||
details = Optional.extract(details) | ||||
r5001 | max_file_bytes = Optional.extract(max_file_bytes) | |||
r1 | _extended_types = ['basic', 'full'] | |||
if details not in _extended_types: | ||||
r5047 | ret_types = ','.join(_extended_types) | |||
raise JSONRPCError(f'ret_type must be one of {ret_types}') | ||||
r1 | extended_info = False | |||
content = False | ||||
if details == 'basic': | ||||
extended_info = True | ||||
if details == 'full': | ||||
extended_info = content = True | ||||
_map = {} | ||||
try: | ||||
# check if repo is not empty by any chance, skip quicker if it is. | ||||
_scm = repo.scm_instance() | ||||
if _scm.is_empty(): | ||||
return [] | ||||
_d, _f = ScmModel().get_nodes( | ||||
repo, revision, root_path, flat=False, | ||||
r502 | extended_info=extended_info, content=content, | |||
max_file_bytes=max_file_bytes) | ||||
r5001 | ||||
r1 | _map = { | |||
'all': _d + _f, | ||||
'files': _f, | ||||
'dirs': _d, | ||||
} | ||||
r5047 | ||||
r1 | return _map[ret_type] | |||
except KeyError: | ||||
r5047 | keys = ','.join(sorted(_map.keys())) | |||
raise JSONRPCError(f'ret_type must be one of {keys}') | ||||
r1 | except Exception: | |||
log.exception("Exception occurred while trying to get repo nodes") | ||||
r5047 | raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes') | |||
r1 | ||||
@jsonrpc_method() | ||||
r3460 | def get_repo_file(request, apiuser, repoid, commit_id, file_path, | |||
r5047 | max_file_bytes=Optional(0), details=Optional('basic'), | |||
r3479 | cache=Optional(True)): | |||
r3460 | """ | |||
Returns a single file from repository at given revision. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param commit_id: The revision for which listing should be done. | ||||
:type commit_id: str | ||||
:param file_path: The path from which to start displaying. | ||||
:type file_path: str | ||||
:param details: Returns different set of information about nodes. | ||||
The valid options are ``minimal`` ``basic`` and ``full``. | ||||
:type details: Optional(str) | ||||
:param max_file_bytes: Only return file content under this file size bytes | ||||
r3479 | :type max_file_bytes: Optional(int) | |||
:param cache: Use internal caches for fetching files. If disabled fetching | ||||
files is slower but more memory efficient | ||||
:type cache: Optional(bool) | ||||
r3693 | ||||
r3460 | Example output: | |||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"binary": false, | ||||
"extension": "py", | ||||
"lines": 35, | ||||
"content": "....", | ||||
"md5": "76318336366b0f17ee249e11b0c99c41", | ||||
"mimetype": "text/x-python", | ||||
"name": "python.py", | ||||
"size": 817, | ||||
"type": "file", | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin', 'repository.write', 'repository.read',) | ||||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
r3479 | cache = Optional.extract(cache, binary=True) | |||
r3460 | details = Optional.extract(details) | |||
r5047 | max_file_bytes = Optional.extract(max_file_bytes) | |||
r3460 | _extended_types = ['minimal', 'minimal+search', 'basic', 'full'] | |||
if details not in _extended_types: | ||||
r5047 | ret_types = ','.join(_extended_types) | |||
raise JSONRPCError(f'ret_type must be one of %s, got {ret_types}', details) | ||||
r3460 | extended_info = False | |||
content = False | ||||
if details == 'minimal': | ||||
extended_info = False | ||||
elif details == 'basic': | ||||
extended_info = True | ||||
elif details == 'full': | ||||
extended_info = content = True | ||||
r5047 | file_path = safe_str(file_path) | |||
r3460 | try: | |||
# check if repo is not empty by any chance, skip quicker if it is. | ||||
_scm = repo.scm_instance() | ||||
if _scm.is_empty(): | ||||
return None | ||||
node = ScmModel().get_node( | ||||
repo, commit_id, file_path, extended_info=extended_info, | ||||
r3479 | content=content, max_file_bytes=max_file_bytes, cache=cache) | |||
r5047 | ||||
r3490 | except NodeDoesNotExistError: | |||
r5047 | raise JSONRPCError( | |||
f'There is no file in repo: `{repo.repo_name}` at path `{file_path}` for commit: `{commit_id}`') | ||||
r3460 | except Exception: | |||
r5047 | log.exception("Exception occurred while trying to get repo %s file", | |||
r3480 | repo.repo_name) | |||
r5047 | raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` file at path {file_path}') | |||
r3460 | ||||
return node | ||||
@jsonrpc_method() | ||||
def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path): | ||||
""" | ||||
Returns a list of tree nodes for path at given revision. This api is built | ||||
strictly for usage in full text search building, and shouldn't be consumed | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin', 'repository.write', 'repository.read',) | ||||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
r3462 | repo_id = repo.repo_id | |||
r5047 | cache_seconds = rhodecode.ConfigGet().get_int('rc_cache.cache_repo.expiration_time') | |||
r3462 | cache_on = cache_seconds > 0 | |||
r5092 | cache_namespace_uid = f'repo.{repo_id}' | |||
r5047 | rc_cache.get_or_create_region('cache_repo', cache_namespace_uid) | |||
r3462 | ||||
r4036 | def compute_fts_tree(cache_ver, repo_id, commit_id, root_path): | |||
r3462 | return ScmModel().get_fts_data(repo_id, commit_id, root_path) | |||
r3460 | try: | |||
# check if repo is not empty by any chance, skip quicker if it is. | ||||
_scm = repo.scm_instance() | ||||
r5047 | if not _scm or _scm.is_empty(): | |||
r3460 | return [] | |||
r3462 | except RepositoryError: | |||
log.exception("Exception occurred while trying to get repo nodes") | ||||
r5047 | raise JSONRPCError(f'failed to get repo: `{repo.repo_name}` nodes') | |||
r3460 | ||||
r3462 | try: | |||
# we need to resolve commit_id to a FULL sha for cache to work correctly. | ||||
# sending 'master' is a pointer that needs to be translated to current commit. | ||||
commit_id = _scm.get_commit(commit_id=commit_id).raw_id | ||||
log.debug( | ||||
'Computing FTS REPO TREE for repo_id %s commit_id `%s` ' | ||||
'with caching: %s[TTL: %ss]' % ( | ||||
repo_id, commit_id, cache_on, cache_seconds or 0)) | ||||
r4036 | tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path) | |||
r5047 | ||||
r3460 | return tree_files | |||
except Exception: | ||||
log.exception("Exception occurred while trying to get repo nodes") | ||||
raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name) | ||||
@jsonrpc_method() | ||||
r1 | def get_repo_refs(request, apiuser, repoid): | |||
""" | ||||
Returns a dictionary of current references. It returns | ||||
bookmarks, branches, closed_branches, and tags for given repository | ||||
It's possible to specify ret_type to show only `files` or `dirs`. | ||||
This command can only be run using an |authtoken| with admin rights, | ||||
or users with at least read rights to |repos|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
r1243 | "result": { | |||
"bookmarks": { | ||||
"dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188", | ||||
"master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf" | ||||
}, | ||||
"branches": { | ||||
"default": "5611d30200f4040ba2ab4f3d64e5b06408a02188", | ||||
"stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf" | ||||
}, | ||||
"branches_closed": {}, | ||||
"tags": { | ||||
"tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188", | ||||
"v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022", | ||||
"v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27", | ||||
"v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17", | ||||
} | ||||
} | ||||
r1 | error: null | |||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin', 'repository.write', 'repository.read',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
try: | ||||
# check if repo is not empty by any chance, skip quicker if it is. | ||||
vcs_instance = repo.scm_instance() | ||||
refs = vcs_instance.refs() | ||||
return refs | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to get repo refs") | ||||
raise JSONRPCError( | ||||
'failed to get repo: `%s` references' % repo.repo_name | ||||
) | ||||
@jsonrpc_method() | ||||
r1153 | def create_repo( | |||
request, apiuser, repo_name, repo_type, | ||||
owner=Optional(OAttr('apiuser')), | ||||
description=Optional(''), | ||||
private=Optional(False), | ||||
clone_uri=Optional(None), | ||||
r2562 | push_uri=Optional(None), | |||
r3881 | landing_rev=Optional(None), | |||
r1153 | enable_statistics=Optional(False), | |||
enable_locking=Optional(False), | ||||
enable_downloads=Optional(False), | ||||
copy_permissions=Optional(False)): | ||||
r1 | """ | |||
Creates a repository. | ||||
r1153 | * If the repository name contains "/", repository will be created inside | |||
a repository group or nested repository groups | ||||
r1 | ||||
r1153 | For example "foo/bar/repo1" will create |repo| called "repo1" inside | |||
group "foo/bar". You have to have permissions to access and write to | ||||
the last repository group ("bar" in this example) | ||||
r1 | ||||
This command can only be run using an |authtoken| with at least | ||||
r1153 | permissions to create repositories, or write permissions to | |||
parent repository groups. | ||||
r1 | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repo_name: Set the repository name. | ||||
:type repo_name: str | ||||
:param repo_type: Set the repository type; 'hg','git', or 'svn'. | ||||
:type repo_type: str | ||||
:param owner: user_id or username | ||||
:type owner: Optional(str) | ||||
:param description: Set the repository description. | ||||
:type description: Optional(str) | ||||
r1153 | :param private: set repository as private | |||
r1 | :type private: bool | |||
r1153 | :param clone_uri: set clone_uri | |||
r1 | :type clone_uri: str | |||
r2562 | :param push_uri: set push_uri | |||
:type push_uri: str | ||||
r3881 | :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd | |||
r1 | :type landing_rev: str | |||
:param enable_locking: | ||||
:type enable_locking: bool | ||||
:param enable_downloads: | ||||
:type enable_downloads: bool | ||||
:param enable_statistics: | ||||
:type enable_statistics: bool | ||||
:param copy_permissions: Copy permission from group in which the | ||||
repository is being created. | ||||
:type copy_permissions: bool | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg": "Created new repository `<reponame>`", | ||||
"success": true, | ||||
"task": "<celery task id or None if done sync>" | ||||
} | ||||
error: null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
r1120 | 'failed to create repository `<repo_name>`' | |||
r1 | } | |||
""" | ||||
r1153 | owner = validate_set_owner_permissions(apiuser, owner) | |||
r1 | ||||
r1153 | description = Optional.extract(description) | |||
copy_permissions = Optional.extract(copy_permissions) | ||||
clone_uri = Optional.extract(clone_uri) | ||||
r2562 | push_uri = Optional.extract(push_uri) | |||
r1 | ||||
defs = SettingsModel().get_default_repo_settings(strip_prefix=True) | ||||
if isinstance(private, Optional): | ||||
private = defs.get('repo_private') or Optional.extract(private) | ||||
if isinstance(repo_type, Optional): | ||||
repo_type = defs.get('repo_type') | ||||
if isinstance(enable_statistics, Optional): | ||||
enable_statistics = defs.get('repo_enable_statistics') | ||||
if isinstance(enable_locking, Optional): | ||||
enable_locking = defs.get('repo_enable_locking') | ||||
if isinstance(enable_downloads, Optional): | ||||
enable_downloads = defs.get('repo_enable_downloads') | ||||
r3881 | landing_ref, _label = ScmModel.backend_landing_ref(repo_type) | |||
ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate) | ||||
ref_choices = list(set(ref_choices + [landing_ref])) | ||||
landing_commit_ref = Optional.extract(landing_rev) or landing_ref | ||||
r1153 | schema = repo_schema.RepoSchema().bind( | |||
repo_type_options=rhodecode.BACKENDS.keys(), | ||||
r3881 | repo_ref_options=ref_choices, | |||
r2664 | repo_type=repo_type, | |||
r1153 | # user caller | |||
user=apiuser) | ||||
r1 | ||||
try: | ||||
r1153 | schema_data = schema.deserialize(dict( | |||
repo_name=repo_name, | ||||
repo_type=repo_type, | ||||
repo_owner=owner.username, | ||||
repo_description=description, | ||||
repo_landing_commit_ref=landing_commit_ref, | ||||
repo_clone_uri=clone_uri, | ||||
r2562 | repo_push_uri=push_uri, | |||
r1153 | repo_private=private, | |||
repo_copy_permissions=copy_permissions, | ||||
repo_enable_statistics=enable_statistics, | ||||
repo_enable_downloads=enable_downloads, | ||||
repo_enable_locking=enable_locking)) | ||||
except validation_schema.Invalid as err: | ||||
raise JSONRPCValidationError(colander_exc=err) | ||||
try: | ||||
r1 | data = { | |||
'owner': owner, | ||||
r1153 | 'repo_name': schema_data['repo_group']['repo_name_without_group'], | |||
'repo_name_full': schema_data['repo_name'], | ||||
'repo_group': schema_data['repo_group']['repo_group_id'], | ||||
'repo_type': schema_data['repo_type'], | ||||
'repo_description': schema_data['repo_description'], | ||||
'repo_private': schema_data['repo_private'], | ||||
'clone_uri': schema_data['repo_clone_uri'], | ||||
r2562 | 'push_uri': schema_data['repo_push_uri'], | |||
r1153 | 'repo_landing_rev': schema_data['repo_landing_commit_ref'], | |||
'enable_statistics': schema_data['repo_enable_statistics'], | ||||
'enable_locking': schema_data['repo_enable_locking'], | ||||
'enable_downloads': schema_data['repo_enable_downloads'], | ||||
'repo_copy_permissions': schema_data['repo_copy_permissions'], | ||||
r1 | } | |||
r2683 | task = RepoModel().create(form_data=data, cur_user=owner.user_id) | |||
r2359 | task_id = get_task_id(task) | |||
r1 | # no commit, it's done in RepoModel, or async via celery | |||
return { | ||||
r5092 | 'msg': "Created new repository `{}`".format(schema_data['repo_name']), | |||
r1 | 'success': True, # cannot return the repo data here since fork | |||
r1153 | # can be done async | |||
r1 | 'task': task_id | |||
} | ||||
except Exception: | ||||
log.exception( | ||||
r5092 | "Exception while trying to create the repository %s", | |||
r1153 | schema_data['repo_name']) | |||
r1 | raise JSONRPCError( | |||
r5092 | 'failed to create repository `{}`'.format(schema_data['repo_name'])) | |||
r1 | ||||
@jsonrpc_method() | ||||
def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''), | ||||
description=Optional('')): | ||||
""" | ||||
Adds an extra field to a repository. | ||||
This command can only be run using an |authtoken| with at least | ||||
write permissions to the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository id. | ||||
:type repoid: str or int | ||||
:param key: Create a unique field key for this repository. | ||||
:type key: str | ||||
:param label: | ||||
:type label: Optional(str) | ||||
:param description: | ||||
:type description: Optional(str) | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
label = Optional.extract(label) or key | ||||
description = Optional.extract(description) | ||||
field = RepositoryField.get_by_key_name(key, repo) | ||||
if field: | ||||
raise JSONRPCError('Field with key ' | ||||
'`%s` exists for repo `%s`' % (key, repoid)) | ||||
try: | ||||
RepoModel().add_repo_field(repo, key, field_label=label, | ||||
field_desc=description) | ||||
Session().commit() | ||||
return { | ||||
r5092 | 'msg': "Added new repository field `{}`".format(key), | |||
r1 | 'success': True, | |||
} | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to add field to repo") | ||||
raise JSONRPCError( | ||||
r5092 | 'failed to create new field for repository `{}`'.format(repoid)) | |||
r1 | ||||
@jsonrpc_method() | ||||
def remove_field_from_repo(request, apiuser, repoid, key): | ||||
""" | ||||
Removes an extra field from a repository. | ||||
This command can only be run using an |authtoken| with at least | ||||
write permissions to the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param key: Set the unique field key for this repository. | ||||
:type key: str | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
field = RepositoryField.get_by_key_name(key, repo) | ||||
if not field: | ||||
raise JSONRPCError('Field with key `%s` does not ' | ||||
'exists for repo `%s`' % (key, repoid)) | ||||
try: | ||||
RepoModel().delete_repo_field(repo, field_key=key) | ||||
Session().commit() | ||||
return { | ||||
r5092 | 'msg': "Deleted repository field `{}`".format(key), | |||
r1 | 'success': True, | |||
} | ||||
except Exception: | ||||
log.exception( | ||||
"Exception occurred while trying to delete field from repo") | ||||
raise JSONRPCError( | ||||
r5092 | 'failed to delete field for repository `{}`'.format(repoid)) | |||
r1 | ||||
@jsonrpc_method() | ||||
r1153 | def update_repo( | |||
request, apiuser, repoid, repo_name=Optional(None), | ||||
owner=Optional(OAttr('apiuser')), description=Optional(''), | ||||
r2563 | private=Optional(False), | |||
clone_uri=Optional(None), push_uri=Optional(None), | ||||
r3881 | landing_rev=Optional(None), fork_of=Optional(None), | |||
r1153 | enable_statistics=Optional(False), | |||
enable_locking=Optional(False), | ||||
enable_downloads=Optional(False), fields=Optional('')): | ||||
r5092 | r""" | |||
r1 | Updates a repository with the given information. | |||
This command can only be run using an |authtoken| with at least | ||||
r1153 | admin permissions to the |repo|. | |||
* If the repository name contains "/", repository will be updated | ||||
accordingly with a repository group or nested repository groups | ||||
For example repoid=repo-test name="foo/bar/repo-test" will update |repo| | ||||
called "repo-test" and place it inside group "foo/bar". | ||||
You have to have permissions to access and write to the last repository | ||||
group ("bar" in this example) | ||||
r1 | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: repository name or repository ID. | ||||
:type repoid: str or int | ||||
r1153 | :param repo_name: Update the |repo| name, including the | |||
repository group it's in. | ||||
:type repo_name: str | ||||
r1 | :param owner: Set the |repo| owner. | |||
:type owner: str | ||||
r1153 | :param fork_of: Set the |repo| as fork of another |repo|. | |||
r1 | :type fork_of: str | |||
:param description: Update the |repo| description. | ||||
:type description: str | ||||
:param private: Set the |repo| as private. (True | False) | ||||
:type private: bool | ||||
:param clone_uri: Update the |repo| clone URI. | ||||
:type clone_uri: str | ||||
r3881 | :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd | |||
r1 | :type landing_rev: str | |||
r1153 | :param enable_statistics: Enable statistics on the |repo|, (True | False). | |||
r1 | :type enable_statistics: bool | |||
:param enable_locking: Enable |repo| locking. | ||||
:type enable_locking: bool | ||||
r1153 | :param enable_downloads: Enable downloads from the |repo|, (True | False). | |||
r1 | :type enable_downloads: bool | |||
:param fields: Add extra fields to the |repo|. Use the following | ||||
example format: ``field_key=field_val,field_key2=fieldval2``. | ||||
Escape ', ' with \, | ||||
:type fields: str | ||||
""" | ||||
r1153 | ||||
r1 | repo = get_repo_or_error(repoid) | |||
r1153 | ||||
r1 | include_secrets = False | |||
r1153 | if not has_superadmin_permission(apiuser): | |||
r4474 | _perms = ('repository.admin',) | |||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
r1153 | else: | |||
r1 | include_secrets = True | |||
r1153 | ||||
updates = dict( | ||||
repo_name=repo_name | ||||
if not isinstance(repo_name, Optional) else repo.repo_name, | ||||
fork_id=fork_of | ||||
if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None, | ||||
user=owner | ||||
if not isinstance(owner, Optional) else repo.user.username, | ||||
repo_description=description | ||||
if not isinstance(description, Optional) else repo.description, | ||||
repo_private=private | ||||
if not isinstance(private, Optional) else repo.private, | ||||
clone_uri=clone_uri | ||||
if not isinstance(clone_uri, Optional) else repo.clone_uri, | ||||
r2563 | push_uri=push_uri | |||
if not isinstance(push_uri, Optional) else repo.push_uri, | ||||
r1153 | repo_landing_rev=landing_rev | |||
if not isinstance(landing_rev, Optional) else repo._landing_revision, | ||||
repo_enable_statistics=enable_statistics | ||||
if not isinstance(enable_statistics, Optional) else repo.enable_statistics, | ||||
repo_enable_locking=enable_locking | ||||
if not isinstance(enable_locking, Optional) else repo.enable_locking, | ||||
repo_enable_downloads=enable_downloads | ||||
if not isinstance(enable_downloads, Optional) else repo.enable_downloads) | ||||
r3881 | landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type) | |||
r2358 | ref_choices, _labels = ScmModel().get_repo_landing_revs( | |||
request.translate, repo=repo) | ||||
r3881 | ref_choices = list(set(ref_choices + [landing_ref])) | |||
r1 | ||||
r1829 | old_values = repo.get_api_data() | |||
r2664 | repo_type = repo.repo_type | |||
r1153 | schema = repo_schema.RepoSchema().bind( | |||
repo_type_options=rhodecode.BACKENDS.keys(), | ||||
repo_ref_options=ref_choices, | ||||
r2664 | repo_type=repo_type, | |||
r1153 | # user caller | |||
user=apiuser, | ||||
r1829 | old_values=old_values) | |||
r1153 | try: | |||
schema_data = schema.deserialize(dict( | ||||
# we save old value, users cannot change type | ||||
r2664 | repo_type=repo_type, | |||
r1153 | ||||
repo_name=updates['repo_name'], | ||||
repo_owner=updates['user'], | ||||
repo_description=updates['repo_description'], | ||||
repo_clone_uri=updates['clone_uri'], | ||||
r2562 | repo_push_uri=updates['push_uri'], | |||
r1153 | repo_fork_of=updates['fork_id'], | |||
repo_private=updates['repo_private'], | ||||
repo_landing_commit_ref=updates['repo_landing_rev'], | ||||
repo_enable_statistics=updates['repo_enable_statistics'], | ||||
repo_enable_downloads=updates['repo_enable_downloads'], | ||||
repo_enable_locking=updates['repo_enable_locking'])) | ||||
except validation_schema.Invalid as err: | ||||
raise JSONRPCValidationError(colander_exc=err) | ||||
r1 | ||||
r1153 | # save validated data back into the updates dict | |||
validated_updates = dict( | ||||
repo_name=schema_data['repo_group']['repo_name_without_group'], | ||||
repo_group=schema_data['repo_group']['repo_group_id'], | ||||
user=schema_data['repo_owner'], | ||||
repo_description=schema_data['repo_description'], | ||||
repo_private=schema_data['repo_private'], | ||||
clone_uri=schema_data['repo_clone_uri'], | ||||
r2562 | push_uri=schema_data['repo_push_uri'], | |||
r1153 | repo_landing_rev=schema_data['repo_landing_commit_ref'], | |||
repo_enable_statistics=schema_data['repo_enable_statistics'], | ||||
repo_enable_locking=schema_data['repo_enable_locking'], | ||||
repo_enable_downloads=schema_data['repo_enable_downloads'], | ||||
) | ||||
if schema_data['repo_fork_of']: | ||||
fork_repo = get_repo_or_error(schema_data['repo_fork_of']) | ||||
validated_updates['fork_id'] = fork_repo.repo_id | ||||
# extra fields | ||||
fields = parse_args(Optional.extract(fields), key_prefix='ex_') | ||||
if fields: | ||||
validated_updates.update(fields) | ||||
r1 | ||||
try: | ||||
r1153 | RepoModel().update(repo, **validated_updates) | |||
r1829 | audit_logger.store_api( | |||
'repo.edit', action_data={'old_data': old_values}, | ||||
user=apiuser, repo=repo) | ||||
r1 | Session().commit() | |||
return { | ||||
r5092 | 'msg': 'updated repo ID:{} {}'.format(repo.repo_id, repo.repo_name), | |||
r1153 | 'repository': repo.get_api_data(include_secrets=include_secrets) | |||
r1 | } | |||
except Exception: | ||||
log.exception( | ||||
r5092 | "Exception while trying to update the repository %s", | |||
r1 | repoid) | |||
raise JSONRPCError('failed to update repo `%s`' % repoid) | ||||
@jsonrpc_method() | ||||
def fork_repo(request, apiuser, repoid, fork_name, | ||||
owner=Optional(OAttr('apiuser')), | ||||
r1153 | description=Optional(''), | |||
private=Optional(False), | ||||
clone_uri=Optional(None), | ||||
r3881 | landing_rev=Optional(None), | |||
r1153 | copy_permissions=Optional(False)): | |||
r1 | """ | |||
Creates a fork of the specified |repo|. | ||||
r1153 | * If the fork_name contains "/", fork will be created inside | |||
a repository group or nested repository groups | ||||
r1 | ||||
r1153 | For example "foo/bar/fork-repo" will create fork called "fork-repo" | |||
inside group "foo/bar". You have to have permissions to access and | ||||
write to the last repository group ("bar" in this example) | ||||
This command can only be run using an |authtoken| with minimum | ||||
read permissions of the forked repo, create fork permissions for an user. | ||||
r1 | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set repository name or repository ID. | ||||
:type repoid: str or int | ||||
r1153 | :param fork_name: Set the fork name, including it's repository group membership. | |||
r1 | :type fork_name: str | |||
:param owner: Set the fork owner. | ||||
:type owner: str | ||||
r1153 | :param description: Set the fork description. | |||
r1 | :type description: str | |||
:param copy_permissions: Copy permissions from parent |repo|. The | ||||
default is False. | ||||
:type copy_permissions: bool | ||||
:param private: Make the fork private. The default is False. | ||||
:type private: bool | ||||
r3881 | :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd | |||
r1 | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_for_response> | ||||
api_key : "<api_key>" | ||||
args: { | ||||
"repoid" : "<reponame or repo_id>", | ||||
"fork_name": "<forkname>", | ||||
"owner": "<username or user_id = Optional(=apiuser)>", | ||||
"description": "<description>", | ||||
"copy_permissions": "<bool>", | ||||
"private": "<bool>", | ||||
"landing_rev": "<landing_rev>" | ||||
} | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg": "Created fork of `<reponame>` as `<forkname>`", | ||||
"success": true, | ||||
"task": "<celery task id or None if done sync>" | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
repo_name = repo.repo_name | ||||
if not has_superadmin_permission(apiuser): | ||||
# check if we have at least read permission for | ||||
# this repo that we fork ! | ||||
r4474 | _perms = ('repository.admin', 'repository.write', 'repository.read') | |||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
r1153 | # check if the regular user has at least fork permissions as well | |||
r4663 | if not HasPermissionAnyApi(PermissionModel.FORKING_ENABLED)(user=apiuser): | |||
r1153 | raise JSONRPCForbidden() | |||
# check if user can set owner parameter | ||||
owner = validate_set_owner_permissions(apiuser, owner) | ||||
r1 | ||||
r1153 | description = Optional.extract(description) | |||
copy_permissions = Optional.extract(copy_permissions) | ||||
clone_uri = Optional.extract(clone_uri) | ||||
r3881 | ||||
landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type) | ||||
ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate) | ||||
ref_choices = list(set(ref_choices + [landing_ref])) | ||||
landing_commit_ref = Optional.extract(landing_rev) or landing_ref | ||||
r1153 | private = Optional.extract(private) | |||
r1 | ||||
r1153 | schema = repo_schema.RepoSchema().bind( | |||
repo_type_options=rhodecode.BACKENDS.keys(), | ||||
r3881 | repo_ref_options=ref_choices, | |||
r2664 | repo_type=repo.repo_type, | |||
r1153 | # user caller | |||
user=apiuser) | ||||
r1 | ||||
try: | ||||
r1153 | schema_data = schema.deserialize(dict( | |||
repo_name=fork_name, | ||||
repo_type=repo.repo_type, | ||||
repo_owner=owner.username, | ||||
repo_description=description, | ||||
repo_landing_commit_ref=landing_commit_ref, | ||||
repo_clone_uri=clone_uri, | ||||
repo_private=private, | ||||
repo_copy_permissions=copy_permissions)) | ||||
except validation_schema.Invalid as err: | ||||
raise JSONRPCValidationError(colander_exc=err) | ||||
try: | ||||
data = { | ||||
r1 | 'fork_parent_id': repo.repo_id, | |||
r1153 | ||||
'repo_name': schema_data['repo_group']['repo_name_without_group'], | ||||
'repo_name_full': schema_data['repo_name'], | ||||
'repo_group': schema_data['repo_group']['repo_group_id'], | ||||
'repo_type': schema_data['repo_type'], | ||||
'description': schema_data['repo_description'], | ||||
'private': schema_data['repo_private'], | ||||
'copy_permissions': schema_data['repo_copy_permissions'], | ||||
'landing_rev': schema_data['repo_landing_commit_ref'], | ||||
r1 | } | |||
r2683 | task = RepoModel().create_fork(data, cur_user=owner.user_id) | |||
r1 | # no commit, it's done in RepoModel, or async via celery | |||
r2359 | task_id = get_task_id(task) | |||
r1 | return { | |||
r5092 | 'msg': 'Created fork of `{}` as `{}`'.format( | |||
r1153 | repo.repo_name, schema_data['repo_name']), | |||
r1 | 'success': True, # cannot return the repo data here since fork | |||
# can be done async | ||||
'task': task_id | ||||
} | ||||
except Exception: | ||||
r1153 | log.exception( | |||
r5092 | "Exception while trying to create fork %s", | |||
r1153 | schema_data['repo_name']) | |||
r1 | raise JSONRPCError( | |||
r5092 | 'failed to fork repository `{}` as `{}`'.format( | |||
r1153 | repo_name, schema_data['repo_name'])) | |||
r1 | ||||
@jsonrpc_method() | ||||
def delete_repo(request, apiuser, repoid, forks=Optional('')): | ||||
""" | ||||
Deletes a repository. | ||||
* When the `forks` parameter is set it's possible to detach or delete | ||||
forks of deleted repository. | ||||
This command can only be run using an |authtoken| with admin | ||||
permissions on the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param forks: Set to `detach` or `delete` forks from the |repo|. | ||||
:type forks: Optional(str) | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg": "Deleted repository `<reponame>`", | ||||
"success": true | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
r1752 | repo_name = repo.repo_name | |||
r1 | if not has_superadmin_permission(apiuser): | |||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
try: | ||||
handle_forks = Optional.extract(forks) | ||||
_forks_msg = '' | ||||
_forks = [f for f in repo.forks] | ||||
if handle_forks == 'detach': | ||||
_forks_msg = ' ' + 'Detached %s forks' % len(_forks) | ||||
elif handle_forks == 'delete': | ||||
_forks_msg = ' ' + 'Deleted %s forks' % len(_forks) | ||||
elif _forks: | ||||
raise JSONRPCError( | ||||
'Cannot delete `%s` it still contains attached forks' % | ||||
(repo.repo_name,) | ||||
) | ||||
r1829 | old_data = repo.get_api_data() | |||
r1752 | RepoModel().delete(repo, forks=forks) | |||
r1 | ||||
r1752 | repo = audit_logger.RepoWrap(repo_id=None, | |||
repo_name=repo.repo_name) | ||||
r1806 | audit_logger.store_api( | |||
r1829 | 'repo.delete', action_data={'old_data': old_data}, | |||
r1806 | user=apiuser, repo=repo) | |||
r1752 | ||||
ScmModel().mark_for_invalidation(repo_name, delete=True) | ||||
r1 | Session().commit() | |||
return { | ||||
r5092 | 'msg': 'Deleted repository `{}`{}'.format(repo_name, _forks_msg), | |||
r1 | 'success': True | |||
} | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to delete repo") | ||||
raise JSONRPCError( | ||||
r5092 | 'failed to delete repository `{}`'.format(repo_name) | |||
r1 | ) | |||
#TODO: marcink, change name ? | ||||
@jsonrpc_method() | ||||
def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)): | ||||
""" | ||||
Invalidates the cache for the specified repository. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Sets the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param delete_keys: This deletes the invalidated keys instead of | ||||
just flagging them. | ||||
:type delete_keys: Optional(``True`` | ``False``) | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
'msg': Cache for repository `<repository name>` was invalidated, | ||||
'repository': <repository name> | ||||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
'Error occurred during cache invalidation action' | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin', 'repository.write',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
delete = Optional.extract(delete_keys) | ||||
try: | ||||
ScmModel().mark_for_invalidation(repo.repo_name, delete=delete) | ||||
return { | ||||
r5092 | 'msg': 'Cache for repository `{}` was invalidated'.format(repoid), | |||
r1 | 'repository': repo.repo_name | |||
} | ||||
except Exception: | ||||
log.exception( | ||||
"Exception occurred while trying to invalidate repo cache") | ||||
raise JSONRPCError( | ||||
'Error occurred during cache invalidation action' | ||||
) | ||||
#TODO: marcink, change name ? | ||||
@jsonrpc_method() | ||||
def lock(request, apiuser, repoid, locked=Optional(None), | ||||
userid=Optional(OAttr('apiuser'))): | ||||
""" | ||||
Sets the lock state of the specified |repo| by the given user. | ||||
From more information, see :ref:`repo-locking`. | ||||
* If the ``userid`` option is not set, the repository is locked to the | ||||
user who called the method. | ||||
* If the ``locked`` parameter is not set, the current lock state of the | ||||
repository is displayed. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Sets the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param locked: Sets the lock state. | ||||
:type locked: Optional(``True`` | ``False``) | ||||
:param userid: Set the repository lock to this user. | ||||
:type userid: Optional(str or int) | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
'repo': '<reponame>', | ||||
'locked': <bool: lock state>, | ||||
'locked_since': <int: lock timestamp>, | ||||
'locked_by': <username of person who made the lock>, | ||||
'lock_reason': <str: reason for locking>, | ||||
'lock_state_changed': <bool: True if lock state has been changed in this request>, | ||||
'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.' | ||||
or | ||||
'msg': 'Repo `<repository name>` not locked.' | ||||
or | ||||
'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`' | ||||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
r1120 | 'Error occurred locking repository `<reponame>`' | |||
r1 | } | |||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
# check if we have at least write permission for this repo ! | ||||
_perms = ('repository.admin', 'repository.write',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
# make sure normal user does not pass someone else userid, | ||||
# he is not allowed to do that | ||||
if not isinstance(userid, Optional) and userid != apiuser.user_id: | ||||
raise JSONRPCError('userid is not the same as your user') | ||||
if isinstance(userid, Optional): | ||||
userid = apiuser.user_id | ||||
user = get_user_or_error(userid) | ||||
if isinstance(locked, Optional): | ||||
lockobj = repo.locked | ||||
if lockobj[0] is None: | ||||
_d = { | ||||
'repo': repo.repo_name, | ||||
'locked': False, | ||||
'locked_since': None, | ||||
'locked_by': None, | ||||
'lock_reason': None, | ||||
'lock_state_changed': False, | ||||
'msg': 'Repo `%s` not locked.' % repo.repo_name | ||||
} | ||||
return _d | ||||
else: | ||||
_user_id, _time, _reason = lockobj | ||||
lock_user = get_user_or_error(userid) | ||||
_d = { | ||||
'repo': repo.repo_name, | ||||
'locked': True, | ||||
'locked_since': _time, | ||||
'locked_by': lock_user.username, | ||||
'lock_reason': _reason, | ||||
'lock_state_changed': False, | ||||
'msg': ('Repo `%s` locked by `%s` on `%s`.' | ||||
% (repo.repo_name, lock_user.username, | ||||
json.dumps(time_to_datetime(_time)))) | ||||
} | ||||
return _d | ||||
# force locked state through a flag | ||||
else: | ||||
locked = str2bool(locked) | ||||
lock_reason = Repository.LOCK_API | ||||
try: | ||||
if locked: | ||||
lock_time = time.time() | ||||
Repository.lock(repo, user.user_id, lock_time, lock_reason) | ||||
else: | ||||
lock_time = None | ||||
Repository.unlock(repo) | ||||
_d = { | ||||
'repo': repo.repo_name, | ||||
'locked': locked, | ||||
'locked_since': lock_time, | ||||
'locked_by': user.username, | ||||
'lock_reason': lock_reason, | ||||
'lock_state_changed': True, | ||||
'msg': ('User `%s` set lock state for repo `%s` to `%s`' | ||||
% (user.username, repo.repo_name, locked)) | ||||
} | ||||
return _d | ||||
except Exception: | ||||
log.exception( | ||||
"Exception occurred while trying to lock repository") | ||||
raise JSONRPCError( | ||||
'Error occurred locking repository `%s`' % repo.repo_name | ||||
) | ||||
@jsonrpc_method() | ||||
def comment_commit( | ||||
r1337 | request, apiuser, repoid, commit_id, message, status=Optional(None), | |||
comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE), | ||||
r4049 | resolves_comment_id=Optional(None), extra_recipients=Optional([]), | |||
r4196 | userid=Optional(OAttr('apiuser')), send_email=Optional(True)): | |||
r1 | """ | |||
Set a commit comment, and optionally change the status of the commit. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param commit_id: Specify the commit_id for which to set a comment. | ||||
:type commit_id: str | ||||
:param message: The comment text. | ||||
:type message: str | ||||
r1337 | :param status: (**Optional**) status of commit, one of: 'not_reviewed', | |||
'approved', 'rejected', 'under_review' | ||||
:type status: str | ||||
:param comment_type: Comment type, one of: 'note', 'todo' | ||||
:type comment_type: Optional(str), default: 'note' | ||||
r4049 | :param resolves_comment_id: id of comment which this one will resolve | |||
:type resolves_comment_id: Optional(int) | ||||
:param extra_recipients: list of user ids or usernames to add | ||||
notifications for this comment. Acts like a CC for notification | ||||
:type extra_recipients: Optional(list) | ||||
r1 | :param userid: Set the user name of the comment creator. | |||
:type userid: Optional(str or int) | ||||
r4196 | :param send_email: Define if this comment should also send email notification | |||
:type send_email: Optional(bool) | ||||
r1 | ||||
Example error output: | ||||
r1337 | .. code-block:: bash | |||
r1 | ||||
{ | ||||
"id" : <id_given_in_input>, | ||||
"result" : { | ||||
"msg": "Commented on commit `<commit_id>` for repository `<repoid>`", | ||||
"status_change": null or <status>, | ||||
"success": true | ||||
}, | ||||
"error" : null | ||||
} | ||||
""" | ||||
r4505 | _ = request.translate | |||
r1 | repo = get_repo_or_error(repoid) | |||
if not has_superadmin_permission(apiuser): | ||||
r352 | _perms = ('repository.read', 'repository.write', 'repository.admin') | |||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r4505 | db_repo_name = repo.repo_name | |||
r1 | ||||
r1416 | try: | |||
r4305 | commit = repo.scm_instance().get_commit(commit_id=commit_id) | |||
commit_id = commit.raw_id | ||||
r1416 | except Exception as e: | |||
log.exception('Failed to fetch commit') | ||||
r3104 | raise JSONRPCError(safe_str(e)) | |||
r1416 | ||||
r1 | if isinstance(userid, Optional): | |||
userid = apiuser.user_id | ||||
user = get_user_or_error(userid) | ||||
status = Optional.extract(status) | ||||
r1337 | comment_type = Optional.extract(comment_type) | |||
r1338 | resolves_comment_id = Optional.extract(resolves_comment_id) | |||
r4049 | extra_recipients = Optional.extract(extra_recipients) | |||
r4196 | send_email = Optional.extract(send_email, binary=True) | |||
r1 | ||||
allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES] | ||||
if status and status not in allowed_statuses: | ||||
raise JSONRPCError('Bad status, must be on ' | ||||
'of %s got %s' % (allowed_statuses, status,)) | ||||
r1338 | if resolves_comment_id: | |||
comment = ChangesetComment.get(resolves_comment_id) | ||||
if not comment: | ||||
raise JSONRPCError( | ||||
'Invalid resolves_comment_id `%s` for this commit.' | ||||
% resolves_comment_id) | ||||
if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO: | ||||
raise JSONRPCError( | ||||
'Comment `%s` is wrong type for setting status to resolved.' | ||||
% resolves_comment_id) | ||||
r1 | try: | |||
rc_config = SettingsModel().get_all_settings() | ||||
renderer = rc_config.get('rhodecode_markup_renderer', 'rst') | ||||
r548 | status_change_label = ChangesetStatus.get_status_lbl(status) | |||
r1829 | comment = CommentsModel().create( | |||
r1322 | message, repo, user, commit_id=commit_id, | |||
r548 | status_change=status_change_label, | |||
status_change_type=status, | ||||
r1337 | renderer=renderer, | |||
r1338 | comment_type=comment_type, | |||
r2728 | resolves_comment_id=resolves_comment_id, | |||
r4049 | auth_user=apiuser, | |||
r4196 | extra_recipients=extra_recipients, | |||
send_email=send_email | ||||
r1337 | ) | |||
r4519 | is_inline = comment.is_inline | |||
r4505 | ||||
r1 | if status: | |||
# also do a status change | ||||
try: | ||||
ChangesetStatusModel().set_status( | ||||
r1829 | repo, status, user, comment, revision=commit_id, | |||
r1 | dont_allow_on_closed_pull_request=True | |||
) | ||||
except StatusChangeOnClosedPullRequestError: | ||||
log.exception( | ||||
"Exception occurred while trying to change repo commit status") | ||||
r4304 | msg = ('Changing status on a commit associated with ' | |||
r1 | 'a closed pull request is not allowed') | |||
raise JSONRPCError(msg) | ||||
r4305 | CommentsModel().trigger_commit_comment_hook( | |||
repo, apiuser, 'create', | ||||
data={'comment': comment, 'commit': commit}) | ||||
r1 | Session().commit() | |||
r4505 | ||||
comment_broadcast_channel = channelstream.comment_channel( | ||||
db_repo_name, commit_obj=commit) | ||||
comment_data = {'comment': comment, 'comment_id': comment.comment_id} | ||||
comment_type = 'inline' if is_inline else 'general' | ||||
channelstream.comment_channelstream_push( | ||||
request, comment_broadcast_channel, apiuser, | ||||
_('posted a new {} comment').format(comment_type), | ||||
comment_data=comment_data) | ||||
r1 | return { | |||
'msg': ( | ||||
r5092 | 'Commented on commit `{}` for repository `{}`'.format( | |||
r1829 | comment.revision, repo.repo_name)), | |||
r1 | 'status_change': status, | |||
'success': True, | ||||
} | ||||
except JSONRPCError: | ||||
# catch any inside errors, and re-raise them to prevent from | ||||
# below global catch to silence them | ||||
raise | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to comment on commit") | ||||
raise JSONRPCError( | ||||
r5092 | 'failed to set comment on repository `{}`'.format(repo.repo_name) | |||
r1 | ) | |||
@jsonrpc_method() | ||||
r3435 | def get_repo_comments(request, apiuser, repoid, | |||
commit_id=Optional(None), comment_type=Optional(None), | ||||
userid=Optional(None)): | ||||
""" | ||||
Get all comments for a repository | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param commit_id: Optionally filter the comments by the commit_id | ||||
:type commit_id: Optional(str), default: None | ||||
:param comment_type: Optionally filter the comments by the comment_type | ||||
one of: 'note', 'todo' | ||||
:type comment_type: Optional(str), default: None | ||||
:param userid: Optionally filter the comments by the author of comment | ||||
:type userid: Optional(str or int), Default: None | ||||
Example error output: | ||||
.. code-block:: bash | ||||
{ | ||||
"id" : <id_given_in_input>, | ||||
"result" : [ | ||||
{ | ||||
"comment_author": <USER_DETAILS>, | ||||
"comment_created_on": "2017-02-01T14:38:16.309", | ||||
"comment_f_path": "file.txt", | ||||
"comment_id": 282, | ||||
"comment_lineno": "n1", | ||||
"comment_resolved_by": null, | ||||
"comment_status": [], | ||||
"comment_text": "This file needs a header", | ||||
r4440 | "comment_type": "todo", | |||
"comment_last_version: 0 | ||||
r3435 | } | |||
], | ||||
"error" : null | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.read', 'repository.write', 'repository.admin') | ||||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
commit_id = Optional.extract(commit_id) | ||||
userid = Optional.extract(userid) | ||||
if userid: | ||||
user = get_user_or_error(userid) | ||||
else: | ||||
user = None | ||||
comment_type = Optional.extract(comment_type) | ||||
if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES: | ||||
raise JSONRPCError( | ||||
'comment_type must be one of `{}` got {}'.format( | ||||
ChangesetComment.COMMENT_TYPES, comment_type) | ||||
) | ||||
comments = CommentsModel().get_repository_comments( | ||||
repo=repo, comment_type=comment_type, user=user, commit_id=commit_id) | ||||
return comments | ||||
@jsonrpc_method() | ||||
r4440 | def get_comment(request, apiuser, comment_id): | |||
""" | ||||
Get single comment from repository or pull_request | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param comment_id: comment id found in the URL of comment | ||||
:type comment_id: str or int | ||||
Example error output: | ||||
.. code-block:: bash | ||||
{ | ||||
"id" : <id_given_in_input>, | ||||
"result" : { | ||||
"comment_author": <USER_DETAILS>, | ||||
"comment_created_on": "2017-02-01T14:38:16.309", | ||||
"comment_f_path": "file.txt", | ||||
"comment_id": 282, | ||||
"comment_lineno": "n1", | ||||
"comment_resolved_by": null, | ||||
"comment_status": [], | ||||
"comment_text": "This file needs a header", | ||||
"comment_type": "todo", | ||||
"comment_last_version: 0 | ||||
}, | ||||
"error" : null | ||||
} | ||||
""" | ||||
comment = ChangesetComment.get(comment_id) | ||||
if not comment: | ||||
r5092 | raise JSONRPCError('comment `{}` does not exist'.format(comment_id)) | |||
r4440 | ||||
perms = ('repository.read', 'repository.write', 'repository.admin') | ||||
has_comment_perm = HasRepoPermissionAnyApi(*perms)\ | ||||
(user=apiuser, repo_name=comment.repo.repo_name) | ||||
if not has_comment_perm: | ||||
r5092 | raise JSONRPCError('comment `{}` does not exist'.format(comment_id)) | |||
r4440 | ||||
return comment | ||||
@jsonrpc_method() | ||||
def edit_comment(request, apiuser, message, comment_id, version, | ||||
userid=Optional(OAttr('apiuser'))): | ||||
""" | ||||
Edit comment on the pull request or commit, | ||||
specified by the `comment_id` and version. Initially version should be 0 | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param comment_id: Specify the comment_id for editing | ||||
:type comment_id: int | ||||
:param version: version of the comment that will be created, starts from 0 | ||||
:type version: int | ||||
:param message: The text content of the comment. | ||||
:type message: str | ||||
:param userid: Comment on the pull request as this user | ||||
:type userid: Optional(str or int) | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
"comment": "<comment data>", | ||||
"version": "<Integer>", | ||||
}, | ||||
error : null | ||||
""" | ||||
auth_user = apiuser | ||||
comment = ChangesetComment.get(comment_id) | ||||
if not comment: | ||||
r5092 | raise JSONRPCError('comment `{}` does not exist'.format(comment_id)) | |||
r4440 | ||||
is_super_admin = has_superadmin_permission(apiuser) | ||||
is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\ | ||||
(user=apiuser, repo_name=comment.repo.repo_name) | ||||
if not isinstance(userid, Optional): | ||||
if is_super_admin or is_repo_admin: | ||||
apiuser = get_user_or_error(userid) | ||||
auth_user = apiuser.AuthUser() | ||||
else: | ||||
raise JSONRPCError('userid is not the same as your user') | ||||
comment_author = comment.author.user_id == auth_user.user_id | ||||
r5047 | ||||
if comment.immutable: | ||||
raise JSONRPCError("Immutable comment cannot be edited") | ||||
if not (is_super_admin or is_repo_admin or comment_author): | ||||
r4440 | raise JSONRPCError("you don't have access to edit this comment") | |||
try: | ||||
comment_history = CommentsModel().edit( | ||||
comment_id=comment_id, | ||||
text=message, | ||||
auth_user=auth_user, | ||||
version=version, | ||||
) | ||||
Session().commit() | ||||
except CommentVersionMismatch: | ||||
raise JSONRPCError( | ||||
r5092 | f'comment ({comment_id}) version ({version}) mismatch' | |||
r4440 | ) | |||
if not comment_history and not message: | ||||
raise JSONRPCError( | ||||
r5092 | f"comment ({comment_id}) can't be changed with empty string" | |||
r4440 | ) | |||
r4444 | ||||
if comment.pull_request: | ||||
pull_request = comment.pull_request | ||||
PullRequestModel().trigger_pull_request_hook( | ||||
pull_request, apiuser, 'comment_edit', | ||||
data={'comment': comment}) | ||||
else: | ||||
db_repo = comment.repo | ||||
commit_id = comment.revision | ||||
commit = db_repo.get_commit(commit_id) | ||||
CommentsModel().trigger_commit_comment_hook( | ||||
db_repo, apiuser, 'edit', | ||||
data={'comment': comment, 'commit': commit}) | ||||
r4440 | data = { | |||
'comment': comment, | ||||
'version': comment_history.version if comment_history else None, | ||||
} | ||||
return data | ||||
# TODO(marcink): write this with all required logic for deleting a comments in PR or commits | ||||
# @jsonrpc_method() | ||||
# def delete_comment(request, apiuser, comment_id): | ||||
# auth_user = apiuser | ||||
# | ||||
# comment = ChangesetComment.get(comment_id) | ||||
# if not comment: | ||||
# raise JSONRPCError('comment `%s` does not exist' % (comment_id,)) | ||||
# | ||||
# is_super_admin = has_superadmin_permission(apiuser) | ||||
# is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\ | ||||
# (user=apiuser, repo_name=comment.repo.repo_name) | ||||
# | ||||
# comment_author = comment.author.user_id == auth_user.user_id | ||||
# if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author): | ||||
# raise JSONRPCError("you don't have access to edit this comment") | ||||
@jsonrpc_method() | ||||
r1 | def grant_user_permission(request, apiuser, repoid, userid, perm): | |||
""" | ||||
Grant permissions for the specified user on the given repository, | ||||
or update existing permissions if found. | ||||
This command can only be run using an |authtoken| with admin | ||||
permissions on the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param userid: Set the user name. | ||||
:type userid: str | ||||
:param perm: Set the user permissions, using the following format | ||||
``(repository.(none|read|write|admin))`` | ||||
:type perm: str | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`", | ||||
"success": true | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
user = get_user_or_error(userid) | ||||
perm = get_perm_or_error(perm) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
r3342 | perm_additions = [[user.user_id, perm.permission_name, "user"]] | |||
r1 | try: | |||
r3342 | changes = RepoModel().update_permissions( | |||
repo=repo, perm_additions=perm_additions, cur_user=apiuser) | ||||
r1 | ||||
r3342 | action_data = { | |||
'added': changes['added'], | ||||
'updated': changes['updated'], | ||||
'deleted': changes['deleted'], | ||||
} | ||||
audit_logger.store_api( | ||||
'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo) | ||||
r3824 | Session().commit() | |||
PermissionModel().flush_user_permission_caches(changes) | ||||
r1 | ||||
return { | ||||
r5092 | 'msg': 'Granted perm: `{}` for user: `{}` in repo: `{}`'.format( | |||
r1 | perm.permission_name, user.username, repo.repo_name | |||
), | ||||
'success': True | ||||
} | ||||
except Exception: | ||||
r3342 | log.exception("Exception occurred while trying edit permissions for repo") | |||
r1 | raise JSONRPCError( | |||
r5092 | 'failed to edit permission for user: `{}` in repo: `{}`'.format( | |||
r1 | userid, repoid | |||
) | ||||
) | ||||
@jsonrpc_method() | ||||
def revoke_user_permission(request, apiuser, repoid, userid): | ||||
""" | ||||
Revoke permission for a user on the specified repository. | ||||
This command can only be run using an |authtoken| with admin | ||||
permissions on the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param userid: Set the user name of revoked user. | ||||
:type userid: str or int | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`", | ||||
"success": true | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
user = get_user_or_error(userid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
r3342 | perm_deletions = [[user.user_id, None, "user"]] | |||
r1 | try: | |||
r3342 | changes = RepoModel().update_permissions( | |||
repo=repo, perm_deletions=perm_deletions, cur_user=user) | ||||
action_data = { | ||||
'added': changes['added'], | ||||
'updated': changes['updated'], | ||||
'deleted': changes['deleted'], | ||||
} | ||||
audit_logger.store_api( | ||||
'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo) | ||||
r3824 | Session().commit() | |||
PermissionModel().flush_user_permission_caches(changes) | ||||
r3342 | ||||
r1 | return { | |||
r5092 | 'msg': 'Revoked perm for user: `{}` in repo: `{}`'.format( | |||
r1 | user.username, repo.repo_name | |||
), | ||||
'success': True | ||||
} | ||||
except Exception: | ||||
r3342 | log.exception("Exception occurred while trying revoke permissions to repo") | |||
r1 | raise JSONRPCError( | |||
r5092 | 'failed to edit permission for user: `{}` in repo: `{}`'.format( | |||
r1 | userid, repoid | |||
) | ||||
) | ||||
@jsonrpc_method() | ||||
def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm): | ||||
""" | ||||
Grant permission for a user group on the specified repository, | ||||
or update existing permissions. | ||||
This command can only be run using an |authtoken| with admin | ||||
permissions on the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param usergroupid: Specify the ID of the user group. | ||||
:type usergroupid: str or int | ||||
:param perm: Set the user group permissions using the following | ||||
format: (repository.(none|read|write|admin)) | ||||
:type perm: str | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
"msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`", | ||||
"success": true | ||||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"failed to edit permission for user group: `<usergroup>` in repo `<repo>`' | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
perm = get_perm_or_error(perm) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
user_group = get_user_group_or_error(usergroupid) | ||||
if not has_superadmin_permission(apiuser): | ||||
# check if we have at least read permission for this user group ! | ||||
_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',) | ||||
if not HasUserGroupPermissionAnyApi(*_perms)( | ||||
user=apiuser, user_group_name=user_group.users_group_name): | ||||
raise JSONRPCError( | ||||
r5092 | 'user group `{}` does not exist'.format(usergroupid)) | |||
r1 | ||||
r3342 | perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]] | |||
r1 | try: | |||
r3342 | changes = RepoModel().update_permissions( | |||
repo=repo, perm_additions=perm_additions, cur_user=apiuser) | ||||
action_data = { | ||||
'added': changes['added'], | ||||
'updated': changes['updated'], | ||||
'deleted': changes['deleted'], | ||||
} | ||||
audit_logger.store_api( | ||||
'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo) | ||||
r3824 | Session().commit() | |||
PermissionModel().flush_user_permission_caches(changes) | ||||
r1 | ||||
return { | ||||
'msg': 'Granted perm: `%s` for user group: `%s` in ' | ||||
'repo: `%s`' % ( | ||||
perm.permission_name, user_group.users_group_name, | ||||
repo.repo_name | ||||
), | ||||
'success': True | ||||
} | ||||
except Exception: | ||||
log.exception( | ||||
"Exception occurred while trying change permission on repo") | ||||
raise JSONRPCError( | ||||
'failed to edit permission for user group: `%s` in ' | ||||
'repo: `%s`' % ( | ||||
usergroupid, repo.repo_name | ||||
) | ||||
) | ||||
@jsonrpc_method() | ||||
def revoke_user_group_permission(request, apiuser, repoid, usergroupid): | ||||
""" | ||||
Revoke the permissions of a user group on a given repository. | ||||
This command can only be run using an |authtoken| with admin | ||||
permissions on the |repo|. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: Set the repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param usergroupid: Specify the user group ID. | ||||
:type usergroupid: str or int | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
"msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`", | ||||
"success": true | ||||
} | ||||
error: null | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
user_group = get_user_group_or_error(usergroupid) | ||||
if not has_superadmin_permission(apiuser): | ||||
# check if we have at least read permission for this user group ! | ||||
_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',) | ||||
if not HasUserGroupPermissionAnyApi(*_perms)( | ||||
user=apiuser, user_group_name=user_group.users_group_name): | ||||
raise JSONRPCError( | ||||
r5092 | 'user group `{}` does not exist'.format(usergroupid)) | |||
r1 | ||||
r3342 | perm_deletions = [[user_group.users_group_id, None, "user_group"]] | |||
r1 | try: | |||
r3342 | changes = RepoModel().update_permissions( | |||
repo=repo, perm_deletions=perm_deletions, cur_user=apiuser) | ||||
action_data = { | ||||
'added': changes['added'], | ||||
'updated': changes['updated'], | ||||
'deleted': changes['deleted'], | ||||
} | ||||
audit_logger.store_api( | ||||
'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo) | ||||
r3824 | Session().commit() | |||
PermissionModel().flush_user_permission_caches(changes) | ||||
r1 | ||||
return { | ||||
r5092 | 'msg': 'Revoked perm for user group: `{}` in repo: `{}`'.format( | |||
r1 | user_group.users_group_name, repo.repo_name | |||
), | ||||
'success': True | ||||
} | ||||
except Exception: | ||||
log.exception("Exception occurred while trying revoke " | ||||
"user group permission on repo") | ||||
raise JSONRPCError( | ||||
'failed to edit permission for user group: `%s` in ' | ||||
'repo: `%s`' % ( | ||||
user_group.users_group_name, repo.repo_name | ||||
) | ||||
) | ||||
@jsonrpc_method() | ||||
r2563 | def pull(request, apiuser, repoid, remote_uri=Optional(None)): | |||
r1 | """ | |||
Triggers a pull on the given repository from a remote location. You | ||||
can use this to keep remote repositories up-to-date. | ||||
This command can only be run using an |authtoken| with admin | ||||
rights to the specified repository. For more information, | ||||
see :ref:`config-token-ref`. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
r2563 | :param remote_uri: Optional remote URI to pass in for pull | |||
:type remote_uri: str | ||||
r1 | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
r2563 | "msg": "Pulled from url `<remote_url>` on repo `<repository name>`" | |||
r1 | "repository": "<repository name>" | |||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
r2563 | "Unable to push changes from `<remote_url>`" | |||
r1 | } | |||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
r2563 | remote_uri = Optional.extract(remote_uri) | |||
remote_uri_display = remote_uri or repo.clone_uri_hidden | ||||
r1 | if not has_superadmin_permission(apiuser): | |||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
try: | ||||
r2563 | ScmModel().pull_changes( | |||
repo.repo_name, apiuser.username, remote_uri=remote_uri) | ||||
r1 | return { | |||
r5092 | 'msg': 'Pulled from url `{}` on repo `{}`'.format( | |||
r2563 | remote_uri_display, repo.repo_name), | |||
r1 | 'repository': repo.repo_name | |||
} | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to " | ||||
"pull changes from remote location") | ||||
raise JSONRPCError( | ||||
r2563 | 'Unable to pull changes from `%s`' % remote_uri_display | |||
r1 | ) | |||
@jsonrpc_method() | ||||
def strip(request, apiuser, repoid, revision, branch): | ||||
""" | ||||
Strips the given revision from the specified repository. | ||||
* This will remove the revision and all of its decendants. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
:param revision: The revision you wish to strip. | ||||
:type revision: str | ||||
:param branch: The branch from which to strip the revision. | ||||
:type branch: str | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
"msg": "'Stripped commit <commit_hash> from repo `<repository name>`'" | ||||
"repository": "<repository name>" | ||||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"Unable to strip commit <commit_hash> from repo `<repository name>`" | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
r1153 | validate_repo_permissions(apiuser, repoid, repo, _perms) | |||
r1 | ||||
try: | ||||
ScmModel().strip(repo, revision, branch) | ||||
r1829 | audit_logger.store_api( | |||
'repo.commit.strip', action_data={'commit_id': revision}, | ||||
repo=repo, | ||||
user=apiuser, commit=True) | ||||
r1 | return { | |||
r5092 | 'msg': 'Stripped commit {} from repo `{}`'.format( | |||
r1 | revision, repo.repo_name), | |||
'repository': repo.repo_name | ||||
} | ||||
except Exception: | ||||
log.exception("Exception while trying to strip") | ||||
raise JSONRPCError( | ||||
r5092 | 'Unable to strip commit {} from repo `{}`'.format( | |||
r1 | revision, repo.repo_name) | |||
) | ||||
Martin Bornhold
|
r387 | |||
@jsonrpc_method() | ||||
def get_repo_settings(request, apiuser, repoid, key=Optional(None)): | ||||
""" | ||||
Returns all settings for a repository. If key is given it only returns the | ||||
setting identified by the key or null. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository id. | ||||
:type repoid: str or int | ||||
:param key: Key of the setting to return. | ||||
:type: key: Optional(str) | ||||
Example output: | ||||
.. code-block:: bash | ||||
{ | ||||
"error": null, | ||||
"id": 237, | ||||
"result": { | ||||
"extensions_largefiles": true, | ||||
r1738 | "extensions_evolve": true, | |||
Martin Bornhold
|
r387 | "hooks_changegroup_push_logger": true, | ||
"hooks_changegroup_repo_size": false, | ||||
"hooks_outgoing_pull_logger": true, | ||||
"phases_publish": "True", | ||||
"rhodecode_hg_use_rebase_for_merging": true, | ||||
"rhodecode_pr_merge_enabled": true, | ||||
"rhodecode_use_outdated_comments": true | ||||
} | ||||
} | ||||
""" | ||||
r4474 | # Restrict access to this api method to super-admins, and repo admins only. | |||
repo = get_repo_or_error(repoid) | ||||
Martin Bornhold
|
r387 | if not has_superadmin_permission(apiuser): | ||
r4474 | _perms = ('repository.admin',) | |||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
Martin Bornhold
|
r387 | |||
try: | ||||
settings_model = VcsSettingsModel(repo=repo) | ||||
settings = settings_model.get_global_settings() | ||||
settings.update(settings_model.get_repo_settings()) | ||||
# If only a single setting is requested fetch it from all settings. | ||||
key = Optional.extract(key) | ||||
if key is not None: | ||||
settings = settings.get(key, None) | ||||
except Exception: | ||||
r5092 | msg = f'Failed to fetch settings for repository `{repoid}`' | |||
Martin Bornhold
|
r387 | log.exception(msg) | ||
raise JSONRPCError(msg) | ||||
return settings | ||||
@jsonrpc_method() | ||||
def set_repo_settings(request, apiuser, repoid, settings): | ||||
""" | ||||
Update repository settings. Returns true on success. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository id. | ||||
:type repoid: str or int | ||||
:param settings: The new settings for the repository. | ||||
:type: settings: dict | ||||
Example output: | ||||
.. code-block:: bash | ||||
{ | ||||
"error": null, | ||||
"id": 237, | ||||
"result": true | ||||
} | ||||
""" | ||||
r4474 | # Restrict access to this api method to super-admins, and repo admins only. | |||
repo = get_repo_or_error(repoid) | ||||
Martin Bornhold
|
r387 | if not has_superadmin_permission(apiuser): | ||
r4474 | _perms = ('repository.admin',) | |||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
Martin Bornhold
|
r387 | |||
if type(settings) is not dict: | ||||
raise JSONRPCError('Settings have to be a JSON Object.') | ||||
try: | ||||
settings_model = VcsSettingsModel(repo=repoid) | ||||
# Merge global, repo and incoming settings. | ||||
new_settings = settings_model.get_global_settings() | ||||
new_settings.update(settings_model.get_repo_settings()) | ||||
new_settings.update(settings) | ||||
# Update the settings. | ||||
inherit_global_settings = new_settings.get( | ||||
'inherit_global_settings', False) | ||||
settings_model.create_or_update_repo_settings( | ||||
new_settings, inherit_global_settings=inherit_global_settings) | ||||
Session().commit() | ||||
except Exception: | ||||
r5092 | msg = f'Failed to update settings for repository `{repoid}`' | |||
Martin Bornhold
|
r387 | log.exception(msg) | ||
raise JSONRPCError(msg) | ||||
# Indicate success. | ||||
return True | ||||
r1742 | ||||
@jsonrpc_method() | ||||
def maintenance(request, apiuser, repoid): | ||||
""" | ||||
Triggers a maintenance on the given repository. | ||||
This command can only be run using an |authtoken| with admin | ||||
rights to the specified repository. For more information, | ||||
see :ref:`config-token-ref`. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param repoid: The repository name or repository ID. | ||||
:type repoid: str or int | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
"msg": "executed maintenance command", | ||||
"executed_actions": [ | ||||
<action_message>, <action_message2>... | ||||
], | ||||
"repository": "<repository name>" | ||||
} | ||||
error : null | ||||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"Unable to execute maintenance on `<reponame>`" | ||||
} | ||||
""" | ||||
repo = get_repo_or_error(repoid) | ||||
if not has_superadmin_permission(apiuser): | ||||
_perms = ('repository.admin',) | ||||
validate_repo_permissions(apiuser, repoid, repo, _perms) | ||||
try: | ||||
maintenance = repo_maintenance.RepoMaintenance() | ||||
executed_actions = maintenance.execute(repo) | ||||
return { | ||||
'msg': 'executed maintenance command', | ||||
'executed_actions': executed_actions, | ||||
'repository': repo.repo_name | ||||
} | ||||
except Exception: | ||||
log.exception("Exception occurred while trying to run maintenance") | ||||
raise JSONRPCError( | ||||
'Unable to execute maintenance on `%s`' % repo.repo_name) | ||||