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