##// END OF EJS Templates
api: expose new functions for FTS...
marcink -
r3460:866fba74 default
parent child Browse files
Show More
@@ -1,2166 +1,2280 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
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 str2bool, time_to_datetime, safe_str
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 from rhodecode.model.changeset_status import ChangesetStatusModel
39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 from rhodecode.model.comment import CommentsModel
40 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
43 ChangesetComment)
43 ChangesetComment)
44 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.scm import ScmModel, RepoList
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 from rhodecode.model import validation_schema
47 from rhodecode.model import validation_schema
48 from rhodecode.model.validation_schema.schemas import repo_schema
48 from rhodecode.model.validation_schema.schemas import repo_schema
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 @jsonrpc_method()
53 @jsonrpc_method()
54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
55 """
55 """
56 Gets an existing repository by its name or repository_id.
56 Gets an existing repository by its name or repository_id.
57
57
58 The members section so the output returns users groups or users
58 The members section so the output returns users groups or users
59 associated with that repository.
59 associated with that repository.
60
60
61 This command can only be run using an |authtoken| with admin rights,
61 This command can only be run using an |authtoken| with admin rights,
62 or users with at least read rights to the |repo|.
62 or users with at least read rights to the |repo|.
63
63
64 :param apiuser: This is filled automatically from the |authtoken|.
64 :param apiuser: This is filled automatically from the |authtoken|.
65 :type apiuser: AuthUser
65 :type apiuser: AuthUser
66 :param repoid: The repository name or repository id.
66 :param repoid: The repository name or repository id.
67 :type repoid: str or int
67 :type repoid: str or int
68 :param cache: use the cached value for last changeset
68 :param cache: use the cached value for last changeset
69 :type: cache: Optional(bool)
69 :type: cache: Optional(bool)
70
70
71 Example output:
71 Example output:
72
72
73 .. code-block:: bash
73 .. code-block:: bash
74
74
75 {
75 {
76 "error": null,
76 "error": null,
77 "id": <repo_id>,
77 "id": <repo_id>,
78 "result": {
78 "result": {
79 "clone_uri": null,
79 "clone_uri": null,
80 "created_on": "timestamp",
80 "created_on": "timestamp",
81 "description": "repo description",
81 "description": "repo description",
82 "enable_downloads": false,
82 "enable_downloads": false,
83 "enable_locking": false,
83 "enable_locking": false,
84 "enable_statistics": false,
84 "enable_statistics": false,
85 "followers": [
85 "followers": [
86 {
86 {
87 "active": true,
87 "active": true,
88 "admin": false,
88 "admin": false,
89 "api_key": "****************************************",
89 "api_key": "****************************************",
90 "api_keys": [
90 "api_keys": [
91 "****************************************"
91 "****************************************"
92 ],
92 ],
93 "email": "user@example.com",
93 "email": "user@example.com",
94 "emails": [
94 "emails": [
95 "user@example.com"
95 "user@example.com"
96 ],
96 ],
97 "extern_name": "rhodecode",
97 "extern_name": "rhodecode",
98 "extern_type": "rhodecode",
98 "extern_type": "rhodecode",
99 "firstname": "username",
99 "firstname": "username",
100 "ip_addresses": [],
100 "ip_addresses": [],
101 "language": null,
101 "language": null,
102 "last_login": "2015-09-16T17:16:35.854",
102 "last_login": "2015-09-16T17:16:35.854",
103 "lastname": "surname",
103 "lastname": "surname",
104 "user_id": <user_id>,
104 "user_id": <user_id>,
105 "username": "name"
105 "username": "name"
106 }
106 }
107 ],
107 ],
108 "fork_of": "parent-repo",
108 "fork_of": "parent-repo",
109 "landing_rev": [
109 "landing_rev": [
110 "rev",
110 "rev",
111 "tip"
111 "tip"
112 ],
112 ],
113 "last_changeset": {
113 "last_changeset": {
114 "author": "User <user@example.com>",
114 "author": "User <user@example.com>",
115 "branch": "default",
115 "branch": "default",
116 "date": "timestamp",
116 "date": "timestamp",
117 "message": "last commit message",
117 "message": "last commit message",
118 "parents": [
118 "parents": [
119 {
119 {
120 "raw_id": "commit-id"
120 "raw_id": "commit-id"
121 }
121 }
122 ],
122 ],
123 "raw_id": "commit-id",
123 "raw_id": "commit-id",
124 "revision": <revision number>,
124 "revision": <revision number>,
125 "short_id": "short id"
125 "short_id": "short id"
126 },
126 },
127 "lock_reason": null,
127 "lock_reason": null,
128 "locked_by": null,
128 "locked_by": null,
129 "locked_date": null,
129 "locked_date": null,
130 "owner": "owner-name",
130 "owner": "owner-name",
131 "permissions": [
131 "permissions": [
132 {
132 {
133 "name": "super-admin-name",
133 "name": "super-admin-name",
134 "origin": "super-admin",
134 "origin": "super-admin",
135 "permission": "repository.admin",
135 "permission": "repository.admin",
136 "type": "user"
136 "type": "user"
137 },
137 },
138 {
138 {
139 "name": "owner-name",
139 "name": "owner-name",
140 "origin": "owner",
140 "origin": "owner",
141 "permission": "repository.admin",
141 "permission": "repository.admin",
142 "type": "user"
142 "type": "user"
143 },
143 },
144 {
144 {
145 "name": "user-group-name",
145 "name": "user-group-name",
146 "origin": "permission",
146 "origin": "permission",
147 "permission": "repository.write",
147 "permission": "repository.write",
148 "type": "user_group"
148 "type": "user_group"
149 }
149 }
150 ],
150 ],
151 "private": true,
151 "private": true,
152 "repo_id": 676,
152 "repo_id": 676,
153 "repo_name": "user-group/repo-name",
153 "repo_name": "user-group/repo-name",
154 "repo_type": "hg"
154 "repo_type": "hg"
155 }
155 }
156 }
156 }
157 """
157 """
158
158
159 repo = get_repo_or_error(repoid)
159 repo = get_repo_or_error(repoid)
160 cache = Optional.extract(cache)
160 cache = Optional.extract(cache)
161
161
162 include_secrets = False
162 include_secrets = False
163 if has_superadmin_permission(apiuser):
163 if has_superadmin_permission(apiuser):
164 include_secrets = True
164 include_secrets = True
165 else:
165 else:
166 # check if we have at least read permission for this repo !
166 # check if we have at least read permission for this repo !
167 _perms = (
167 _perms = (
168 'repository.admin', 'repository.write', 'repository.read',)
168 'repository.admin', 'repository.write', 'repository.read',)
169 validate_repo_permissions(apiuser, repoid, repo, _perms)
169 validate_repo_permissions(apiuser, repoid, repo, _perms)
170
170
171 permissions = []
171 permissions = []
172 for _user in repo.permissions():
172 for _user in repo.permissions():
173 user_data = {
173 user_data = {
174 'name': _user.username,
174 'name': _user.username,
175 'permission': _user.permission,
175 'permission': _user.permission,
176 'origin': get_origin(_user),
176 'origin': get_origin(_user),
177 'type': "user",
177 'type': "user",
178 }
178 }
179 permissions.append(user_data)
179 permissions.append(user_data)
180
180
181 for _user_group in repo.permission_user_groups():
181 for _user_group in repo.permission_user_groups():
182 user_group_data = {
182 user_group_data = {
183 'name': _user_group.users_group_name,
183 'name': _user_group.users_group_name,
184 'permission': _user_group.permission,
184 'permission': _user_group.permission,
185 'origin': get_origin(_user_group),
185 'origin': get_origin(_user_group),
186 'type': "user_group",
186 'type': "user_group",
187 }
187 }
188 permissions.append(user_group_data)
188 permissions.append(user_group_data)
189
189
190 following_users = [
190 following_users = [
191 user.user.get_api_data(include_secrets=include_secrets)
191 user.user.get_api_data(include_secrets=include_secrets)
192 for user in repo.followers]
192 for user in repo.followers]
193
193
194 if not cache:
194 if not cache:
195 repo.update_commit_cache()
195 repo.update_commit_cache()
196 data = repo.get_api_data(include_secrets=include_secrets)
196 data = repo.get_api_data(include_secrets=include_secrets)
197 data['permissions'] = permissions
197 data['permissions'] = permissions
198 data['followers'] = following_users
198 data['followers'] = following_users
199 return data
199 return data
200
200
201
201
202 @jsonrpc_method()
202 @jsonrpc_method()
203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
204 """
204 """
205 Lists all existing repositories.
205 Lists all existing repositories.
206
206
207 This command can only be run using an |authtoken| with admin rights,
207 This command can only be run using an |authtoken| with admin rights,
208 or users with at least read rights to |repos|.
208 or users with at least read rights to |repos|.
209
209
210 :param apiuser: This is filled automatically from the |authtoken|.
210 :param apiuser: This is filled automatically from the |authtoken|.
211 :type apiuser: AuthUser
211 :type apiuser: AuthUser
212 :param root: specify root repository group to fetch repositories.
212 :param root: specify root repository group to fetch repositories.
213 filters the returned repositories to be members of given root group.
213 filters the returned repositories to be members of given root group.
214 :type root: Optional(None)
214 :type root: Optional(None)
215 :param traverse: traverse given root into subrepositories. With this flag
215 :param traverse: traverse given root into subrepositories. With this flag
216 set to False, it will only return top-level repositories from `root`.
216 set to False, it will only return top-level repositories from `root`.
217 if root is empty it will return just top-level repositories.
217 if root is empty it will return just top-level repositories.
218 :type traverse: Optional(True)
218 :type traverse: Optional(True)
219
219
220
220
221 Example output:
221 Example output:
222
222
223 .. code-block:: bash
223 .. code-block:: bash
224
224
225 id : <id_given_in_input>
225 id : <id_given_in_input>
226 result: [
226 result: [
227 {
227 {
228 "repo_id" : "<repo_id>",
228 "repo_id" : "<repo_id>",
229 "repo_name" : "<reponame>"
229 "repo_name" : "<reponame>"
230 "repo_type" : "<repo_type>",
230 "repo_type" : "<repo_type>",
231 "clone_uri" : "<clone_uri>",
231 "clone_uri" : "<clone_uri>",
232 "private": : "<bool>",
232 "private": : "<bool>",
233 "created_on" : "<datetimecreated>",
233 "created_on" : "<datetimecreated>",
234 "description" : "<description>",
234 "description" : "<description>",
235 "landing_rev": "<landing_rev>",
235 "landing_rev": "<landing_rev>",
236 "owner": "<repo_owner>",
236 "owner": "<repo_owner>",
237 "fork_of": "<name_of_fork_parent>",
237 "fork_of": "<name_of_fork_parent>",
238 "enable_downloads": "<bool>",
238 "enable_downloads": "<bool>",
239 "enable_locking": "<bool>",
239 "enable_locking": "<bool>",
240 "enable_statistics": "<bool>",
240 "enable_statistics": "<bool>",
241 },
241 },
242 ...
242 ...
243 ]
243 ]
244 error: null
244 error: null
245 """
245 """
246
246
247 include_secrets = has_superadmin_permission(apiuser)
247 include_secrets = has_superadmin_permission(apiuser)
248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
249 extras = {'user': apiuser}
249 extras = {'user': apiuser}
250
250
251 root = Optional.extract(root)
251 root = Optional.extract(root)
252 traverse = Optional.extract(traverse, binary=True)
252 traverse = Optional.extract(traverse, binary=True)
253
253
254 if root:
254 if root:
255 # verify parent existance, if it's empty return an error
255 # verify parent existance, if it's empty return an error
256 parent = RepoGroup.get_by_group_name(root)
256 parent = RepoGroup.get_by_group_name(root)
257 if not parent:
257 if not parent:
258 raise JSONRPCError(
258 raise JSONRPCError(
259 'Root repository group `{}` does not exist'.format(root))
259 'Root repository group `{}` does not exist'.format(root))
260
260
261 if traverse:
261 if traverse:
262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
263 else:
263 else:
264 repos = RepoModel().get_repos_for_root(root=parent)
264 repos = RepoModel().get_repos_for_root(root=parent)
265 else:
265 else:
266 if traverse:
266 if traverse:
267 repos = RepoModel().get_all()
267 repos = RepoModel().get_all()
268 else:
268 else:
269 # return just top-level
269 # return just top-level
270 repos = RepoModel().get_repos_for_root(root=None)
270 repos = RepoModel().get_repos_for_root(root=None)
271
271
272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
273 return [repo.get_api_data(include_secrets=include_secrets)
273 return [repo.get_api_data(include_secrets=include_secrets)
274 for repo in repo_list]
274 for repo in repo_list]
275
275
276
276
277 @jsonrpc_method()
277 @jsonrpc_method()
278 def get_repo_changeset(request, apiuser, repoid, revision,
278 def get_repo_changeset(request, apiuser, repoid, revision,
279 details=Optional('basic')):
279 details=Optional('basic')):
280 """
280 """
281 Returns information about a changeset.
281 Returns information about a changeset.
282
282
283 Additionally parameters define the amount of details returned by
283 Additionally parameters define the amount of details returned by
284 this function.
284 this function.
285
285
286 This command can only be run using an |authtoken| with admin rights,
286 This command can only be run using an |authtoken| with admin rights,
287 or users with at least read rights to the |repo|.
287 or users with at least read rights to the |repo|.
288
288
289 :param apiuser: This is filled automatically from the |authtoken|.
289 :param apiuser: This is filled automatically from the |authtoken|.
290 :type apiuser: AuthUser
290 :type apiuser: AuthUser
291 :param repoid: The repository name or repository id
291 :param repoid: The repository name or repository id
292 :type repoid: str or int
292 :type repoid: str or int
293 :param revision: revision for which listing should be done
293 :param revision: revision for which listing should be done
294 :type revision: str
294 :type revision: str
295 :param details: details can be 'basic|extended|full' full gives diff
295 :param details: details can be 'basic|extended|full' full gives diff
296 info details like the diff itself, and number of changed files etc.
296 info details like the diff itself, and number of changed files etc.
297 :type details: Optional(str)
297 :type details: Optional(str)
298
298
299 """
299 """
300 repo = get_repo_or_error(repoid)
300 repo = get_repo_or_error(repoid)
301 if not has_superadmin_permission(apiuser):
301 if not has_superadmin_permission(apiuser):
302 _perms = (
302 _perms = (
303 'repository.admin', 'repository.write', 'repository.read',)
303 'repository.admin', 'repository.write', 'repository.read',)
304 validate_repo_permissions(apiuser, repoid, repo, _perms)
304 validate_repo_permissions(apiuser, repoid, repo, _perms)
305
305
306 changes_details = Optional.extract(details)
306 changes_details = Optional.extract(details)
307 _changes_details_types = ['basic', 'extended', 'full']
307 _changes_details_types = ['basic', 'extended', 'full']
308 if changes_details not in _changes_details_types:
308 if changes_details not in _changes_details_types:
309 raise JSONRPCError(
309 raise JSONRPCError(
310 'ret_type must be one of %s' % (
310 'ret_type must be one of %s' % (
311 ','.join(_changes_details_types)))
311 ','.join(_changes_details_types)))
312
312
313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
314 'status', '_commit', '_file_paths']
314 'status', '_commit', '_file_paths']
315
315
316 try:
316 try:
317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
318 except TypeError as e:
318 except TypeError as e:
319 raise JSONRPCError(safe_str(e))
319 raise JSONRPCError(safe_str(e))
320 _cs_json = cs.__json__()
320 _cs_json = cs.__json__()
321 _cs_json['diff'] = build_commit_data(cs, changes_details)
321 _cs_json['diff'] = build_commit_data(cs, changes_details)
322 if changes_details == 'full':
322 if changes_details == 'full':
323 _cs_json['refs'] = cs._get_refs()
323 _cs_json['refs'] = cs._get_refs()
324 return _cs_json
324 return _cs_json
325
325
326
326
327 @jsonrpc_method()
327 @jsonrpc_method()
328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
329 details=Optional('basic')):
329 details=Optional('basic')):
330 """
330 """
331 Returns a set of commits limited by the number starting
331 Returns a set of commits limited by the number starting
332 from the `start_rev` option.
332 from the `start_rev` option.
333
333
334 Additional parameters define the amount of details returned by this
334 Additional parameters define the amount of details returned by this
335 function.
335 function.
336
336
337 This command can only be run using an |authtoken| with admin rights,
337 This command can only be run using an |authtoken| with admin rights,
338 or users with at least read rights to |repos|.
338 or users with at least read rights to |repos|.
339
339
340 :param apiuser: This is filled automatically from the |authtoken|.
340 :param apiuser: This is filled automatically from the |authtoken|.
341 :type apiuser: AuthUser
341 :type apiuser: AuthUser
342 :param repoid: The repository name or repository ID.
342 :param repoid: The repository name or repository ID.
343 :type repoid: str or int
343 :type repoid: str or int
344 :param start_rev: The starting revision from where to get changesets.
344 :param start_rev: The starting revision from where to get changesets.
345 :type start_rev: str
345 :type start_rev: str
346 :param limit: Limit the number of commits to this amount
346 :param limit: Limit the number of commits to this amount
347 :type limit: str or int
347 :type limit: str or int
348 :param details: Set the level of detail returned. Valid option are:
348 :param details: Set the level of detail returned. Valid option are:
349 ``basic``, ``extended`` and ``full``.
349 ``basic``, ``extended`` and ``full``.
350 :type details: Optional(str)
350 :type details: Optional(str)
351
351
352 .. note::
352 .. note::
353
353
354 Setting the parameter `details` to the value ``full`` is extensive
354 Setting the parameter `details` to the value ``full`` is extensive
355 and returns details like the diff itself, and the number
355 and returns details like the diff itself, and the number
356 of changed files.
356 of changed files.
357
357
358 """
358 """
359 repo = get_repo_or_error(repoid)
359 repo = get_repo_or_error(repoid)
360 if not has_superadmin_permission(apiuser):
360 if not has_superadmin_permission(apiuser):
361 _perms = (
361 _perms = (
362 'repository.admin', 'repository.write', 'repository.read',)
362 'repository.admin', 'repository.write', 'repository.read',)
363 validate_repo_permissions(apiuser, repoid, repo, _perms)
363 validate_repo_permissions(apiuser, repoid, repo, _perms)
364
364
365 changes_details = Optional.extract(details)
365 changes_details = Optional.extract(details)
366 _changes_details_types = ['basic', 'extended', 'full']
366 _changes_details_types = ['basic', 'extended', 'full']
367 if changes_details not in _changes_details_types:
367 if changes_details not in _changes_details_types:
368 raise JSONRPCError(
368 raise JSONRPCError(
369 'ret_type must be one of %s' % (
369 'ret_type must be one of %s' % (
370 ','.join(_changes_details_types)))
370 ','.join(_changes_details_types)))
371
371
372 limit = int(limit)
372 limit = int(limit)
373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
374 'status', '_commit', '_file_paths']
374 'status', '_commit', '_file_paths']
375
375
376 vcs_repo = repo.scm_instance()
376 vcs_repo = repo.scm_instance()
377 # SVN needs a special case to distinguish its index and commit id
377 # SVN needs a special case to distinguish its index and commit id
378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
379 start_rev = vcs_repo.commit_ids[0]
379 start_rev = vcs_repo.commit_ids[0]
380
380
381 try:
381 try:
382 commits = vcs_repo.get_commits(
382 commits = vcs_repo.get_commits(
383 start_id=start_rev, pre_load=pre_load)
383 start_id=start_rev, pre_load=pre_load)
384 except TypeError as e:
384 except TypeError as e:
385 raise JSONRPCError(safe_str(e))
385 raise JSONRPCError(safe_str(e))
386 except Exception:
386 except Exception:
387 log.exception('Fetching of commits failed')
387 log.exception('Fetching of commits failed')
388 raise JSONRPCError('Error occurred during commit fetching')
388 raise JSONRPCError('Error occurred during commit fetching')
389
389
390 ret = []
390 ret = []
391 for cnt, commit in enumerate(commits):
391 for cnt, commit in enumerate(commits):
392 if cnt >= limit != -1:
392 if cnt >= limit != -1:
393 break
393 break
394 _cs_json = commit.__json__()
394 _cs_json = commit.__json__()
395 _cs_json['diff'] = build_commit_data(commit, changes_details)
395 _cs_json['diff'] = build_commit_data(commit, changes_details)
396 if changes_details == 'full':
396 if changes_details == 'full':
397 _cs_json['refs'] = {
397 _cs_json['refs'] = {
398 'branches': [commit.branch],
398 'branches': [commit.branch],
399 'bookmarks': getattr(commit, 'bookmarks', []),
399 'bookmarks': getattr(commit, 'bookmarks', []),
400 'tags': commit.tags
400 'tags': commit.tags
401 }
401 }
402 ret.append(_cs_json)
402 ret.append(_cs_json)
403 return ret
403 return ret
404
404
405
405
406 @jsonrpc_method()
406 @jsonrpc_method()
407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
408 ret_type=Optional('all'), details=Optional('basic'),
408 ret_type=Optional('all'), details=Optional('basic'),
409 max_file_bytes=Optional(None)):
409 max_file_bytes=Optional(None)):
410 """
410 """
411 Returns a list of nodes and children in a flat list for a given
411 Returns a list of nodes and children in a flat list for a given
412 path at given revision.
412 path at given revision.
413
413
414 It's possible to specify ret_type to show only `files` or `dirs`.
414 It's possible to specify ret_type to show only `files` or `dirs`.
415
415
416 This command can only be run using an |authtoken| with admin rights,
416 This command can only be run using an |authtoken| with admin rights,
417 or users with at least read rights to |repos|.
417 or users with at least read rights to |repos|.
418
418
419 :param apiuser: This is filled automatically from the |authtoken|.
419 :param apiuser: This is filled automatically from the |authtoken|.
420 :type apiuser: AuthUser
420 :type apiuser: AuthUser
421 :param repoid: The repository name or repository ID.
421 :param repoid: The repository name or repository ID.
422 :type repoid: str or int
422 :type repoid: str or int
423 :param revision: The revision for which listing should be done.
423 :param revision: The revision for which listing should be done.
424 :type revision: str
424 :type revision: str
425 :param root_path: The path from which to start displaying.
425 :param root_path: The path from which to start displaying.
426 :type root_path: str
426 :type root_path: str
427 :param ret_type: Set the return type. Valid options are
427 :param ret_type: Set the return type. Valid options are
428 ``all`` (default), ``files`` and ``dirs``.
428 ``all`` (default), ``files`` and ``dirs``.
429 :type ret_type: Optional(str)
429 :type ret_type: Optional(str)
430 :param details: Returns extended information about nodes, such as
430 :param details: Returns extended information about nodes, such as
431 md5, binary, and or content. The valid options are ``basic`` and
431 md5, binary, and or content.
432 ``full``.
432 The valid options are ``basic`` and ``full``.
433 :type details: Optional(str)
433 :type details: Optional(str)
434 :param max_file_bytes: Only return file content under this file size bytes
434 :param max_file_bytes: Only return file content under this file size bytes
435 :type details: Optional(int)
435 :type details: Optional(int)
436
436
437 Example output:
437 Example output:
438
438
439 .. code-block:: bash
439 .. code-block:: bash
440
440
441 id : <id_given_in_input>
441 id : <id_given_in_input>
442 result: [
442 result: [
443 {
443 {
444 "name" : "<name>"
444 "binary": false,
445 "type" : "<type>",
445 "content": "File line\nLine2\n",
446 "binary": "<true|false>" (only in extended mode)
446 "extension": "md",
447 "md5" : "<md5 of file content>" (only in extended mode)
447 "lines": 2,
448 },
448 "md5": "059fa5d29b19c0657e384749480f6422",
449 "mimetype": "text/x-minidsrc",
450 "name": "file.md",
451 "size": 580,
452 "type": "file"
453 },
449 ...
454 ...
450 ]
455 ]
451 error: null
456 error: null
452 """
457 """
453
458
454 repo = get_repo_or_error(repoid)
459 repo = get_repo_or_error(repoid)
455 if not has_superadmin_permission(apiuser):
460 if not has_superadmin_permission(apiuser):
456 _perms = (
461 _perms = ('repository.admin', 'repository.write', 'repository.read',)
457 'repository.admin', 'repository.write', 'repository.read',)
458 validate_repo_permissions(apiuser, repoid, repo, _perms)
462 validate_repo_permissions(apiuser, repoid, repo, _perms)
459
463
460 ret_type = Optional.extract(ret_type)
464 ret_type = Optional.extract(ret_type)
461 details = Optional.extract(details)
465 details = Optional.extract(details)
462 _extended_types = ['basic', 'full']
466 _extended_types = ['basic', 'full']
463 if details not in _extended_types:
467 if details not in _extended_types:
464 raise JSONRPCError(
468 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
465 'ret_type must be one of %s' % (','.join(_extended_types)))
466 extended_info = False
469 extended_info = False
467 content = False
470 content = False
468 if details == 'basic':
471 if details == 'basic':
469 extended_info = True
472 extended_info = True
470
473
471 if details == 'full':
474 if details == 'full':
472 extended_info = content = True
475 extended_info = content = True
473
476
474 _map = {}
477 _map = {}
475 try:
478 try:
476 # check if repo is not empty by any chance, skip quicker if it is.
479 # check if repo is not empty by any chance, skip quicker if it is.
477 _scm = repo.scm_instance()
480 _scm = repo.scm_instance()
478 if _scm.is_empty():
481 if _scm.is_empty():
479 return []
482 return []
480
483
481 _d, _f = ScmModel().get_nodes(
484 _d, _f = ScmModel().get_nodes(
482 repo, revision, root_path, flat=False,
485 repo, revision, root_path, flat=False,
483 extended_info=extended_info, content=content,
486 extended_info=extended_info, content=content,
484 max_file_bytes=max_file_bytes)
487 max_file_bytes=max_file_bytes)
485 _map = {
488 _map = {
486 'all': _d + _f,
489 'all': _d + _f,
487 'files': _f,
490 'files': _f,
488 'dirs': _d,
491 'dirs': _d,
489 }
492 }
490 return _map[ret_type]
493 return _map[ret_type]
491 except KeyError:
494 except KeyError:
492 raise JSONRPCError(
495 raise JSONRPCError(
493 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
496 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
494 except Exception:
497 except Exception:
495 log.exception("Exception occurred while trying to get repo nodes")
498 log.exception("Exception occurred while trying to get repo nodes")
496 raise JSONRPCError(
499 raise JSONRPCError(
497 'failed to get repo: `%s` nodes' % repo.repo_name
500 'failed to get repo: `%s` nodes' % repo.repo_name
498 )
501 )
499
502
500
503
501 @jsonrpc_method()
504 @jsonrpc_method()
505 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
506 max_file_bytes=Optional(None), details=Optional('basic')):
507 """
508 Returns a single file from repository at given revision.
509
510 This command can only be run using an |authtoken| with admin rights,
511 or users with at least read rights to |repos|.
512
513 :param apiuser: This is filled automatically from the |authtoken|.
514 :type apiuser: AuthUser
515 :param repoid: The repository name or repository ID.
516 :type repoid: str or int
517 :param commit_id: The revision for which listing should be done.
518 :type commit_id: str
519 :param file_path: The path from which to start displaying.
520 :type file_path: str
521 :param details: Returns different set of information about nodes.
522 The valid options are ``minimal`` ``basic`` and ``full``.
523 :type details: Optional(str)
524 :param max_file_bytes: Only return file content under this file size bytes
525 :type details: Optional(int)
526
527 Example output:
528
529 .. code-block:: bash
530
531 id : <id_given_in_input>
532 result: {
533 "binary": false,
534 "extension": "py",
535 "lines": 35,
536 "content": "....",
537 "md5": "76318336366b0f17ee249e11b0c99c41",
538 "mimetype": "text/x-python",
539 "name": "python.py",
540 "size": 817,
541 "type": "file",
542 }
543 error: null
544 """
545
546 repo = get_repo_or_error(repoid)
547 if not has_superadmin_permission(apiuser):
548 _perms = ('repository.admin', 'repository.write', 'repository.read',)
549 validate_repo_permissions(apiuser, repoid, repo, _perms)
550
551 details = Optional.extract(details)
552 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
553 if details not in _extended_types:
554 raise JSONRPCError(
555 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
556 extended_info = False
557 content = False
558
559 if details == 'minimal':
560 extended_info = False
561
562 elif details == 'basic':
563 extended_info = True
564
565 elif details == 'full':
566 extended_info = content = True
567
568 try:
569 # check if repo is not empty by any chance, skip quicker if it is.
570 _scm = repo.scm_instance()
571 if _scm.is_empty():
572 return None
573
574 node = ScmModel().get_node(
575 repo, commit_id, file_path, extended_info=extended_info,
576 content=content, max_file_bytes=max_file_bytes)
577
578 except Exception:
579 log.exception("Exception occurred while trying to get repo node")
580 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
581
582 return node
583
584
585 @jsonrpc_method()
586 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
587 """
588 Returns a list of tree nodes for path at given revision. This api is built
589 strictly for usage in full text search building, and shouldn't be consumed
590
591 This command can only be run using an |authtoken| with admin rights,
592 or users with at least read rights to |repos|.
593
594 """
595
596 repo = get_repo_or_error(repoid)
597 if not has_superadmin_permission(apiuser):
598 _perms = ('repository.admin', 'repository.write', 'repository.read',)
599 validate_repo_permissions(apiuser, repoid, repo, _perms)
600
601 try:
602 # check if repo is not empty by any chance, skip quicker if it is.
603 _scm = repo.scm_instance()
604 if _scm.is_empty():
605 return []
606
607 tree_files = ScmModel().get_fts_data(repo, commit_id, root_path)
608 return tree_files
609
610 except Exception:
611 log.exception("Exception occurred while trying to get repo nodes")
612 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
613
614
615 @jsonrpc_method()
502 def get_repo_refs(request, apiuser, repoid):
616 def get_repo_refs(request, apiuser, repoid):
503 """
617 """
504 Returns a dictionary of current references. It returns
618 Returns a dictionary of current references. It returns
505 bookmarks, branches, closed_branches, and tags for given repository
619 bookmarks, branches, closed_branches, and tags for given repository
506
620
507 It's possible to specify ret_type to show only `files` or `dirs`.
621 It's possible to specify ret_type to show only `files` or `dirs`.
508
622
509 This command can only be run using an |authtoken| with admin rights,
623 This command can only be run using an |authtoken| with admin rights,
510 or users with at least read rights to |repos|.
624 or users with at least read rights to |repos|.
511
625
512 :param apiuser: This is filled automatically from the |authtoken|.
626 :param apiuser: This is filled automatically from the |authtoken|.
513 :type apiuser: AuthUser
627 :type apiuser: AuthUser
514 :param repoid: The repository name or repository ID.
628 :param repoid: The repository name or repository ID.
515 :type repoid: str or int
629 :type repoid: str or int
516
630
517 Example output:
631 Example output:
518
632
519 .. code-block:: bash
633 .. code-block:: bash
520
634
521 id : <id_given_in_input>
635 id : <id_given_in_input>
522 "result": {
636 "result": {
523 "bookmarks": {
637 "bookmarks": {
524 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
638 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
525 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
639 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
526 },
640 },
527 "branches": {
641 "branches": {
528 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
642 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
529 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
643 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
530 },
644 },
531 "branches_closed": {},
645 "branches_closed": {},
532 "tags": {
646 "tags": {
533 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
647 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
534 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
648 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
535 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
649 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
536 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
650 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
537 }
651 }
538 }
652 }
539 error: null
653 error: null
540 """
654 """
541
655
542 repo = get_repo_or_error(repoid)
656 repo = get_repo_or_error(repoid)
543 if not has_superadmin_permission(apiuser):
657 if not has_superadmin_permission(apiuser):
544 _perms = ('repository.admin', 'repository.write', 'repository.read',)
658 _perms = ('repository.admin', 'repository.write', 'repository.read',)
545 validate_repo_permissions(apiuser, repoid, repo, _perms)
659 validate_repo_permissions(apiuser, repoid, repo, _perms)
546
660
547 try:
661 try:
548 # check if repo is not empty by any chance, skip quicker if it is.
662 # check if repo is not empty by any chance, skip quicker if it is.
549 vcs_instance = repo.scm_instance()
663 vcs_instance = repo.scm_instance()
550 refs = vcs_instance.refs()
664 refs = vcs_instance.refs()
551 return refs
665 return refs
552 except Exception:
666 except Exception:
553 log.exception("Exception occurred while trying to get repo refs")
667 log.exception("Exception occurred while trying to get repo refs")
554 raise JSONRPCError(
668 raise JSONRPCError(
555 'failed to get repo: `%s` references' % repo.repo_name
669 'failed to get repo: `%s` references' % repo.repo_name
556 )
670 )
557
671
558
672
559 @jsonrpc_method()
673 @jsonrpc_method()
560 def create_repo(
674 def create_repo(
561 request, apiuser, repo_name, repo_type,
675 request, apiuser, repo_name, repo_type,
562 owner=Optional(OAttr('apiuser')),
676 owner=Optional(OAttr('apiuser')),
563 description=Optional(''),
677 description=Optional(''),
564 private=Optional(False),
678 private=Optional(False),
565 clone_uri=Optional(None),
679 clone_uri=Optional(None),
566 push_uri=Optional(None),
680 push_uri=Optional(None),
567 landing_rev=Optional('rev:tip'),
681 landing_rev=Optional('rev:tip'),
568 enable_statistics=Optional(False),
682 enable_statistics=Optional(False),
569 enable_locking=Optional(False),
683 enable_locking=Optional(False),
570 enable_downloads=Optional(False),
684 enable_downloads=Optional(False),
571 copy_permissions=Optional(False)):
685 copy_permissions=Optional(False)):
572 """
686 """
573 Creates a repository.
687 Creates a repository.
574
688
575 * If the repository name contains "/", repository will be created inside
689 * If the repository name contains "/", repository will be created inside
576 a repository group or nested repository groups
690 a repository group or nested repository groups
577
691
578 For example "foo/bar/repo1" will create |repo| called "repo1" inside
692 For example "foo/bar/repo1" will create |repo| called "repo1" inside
579 group "foo/bar". You have to have permissions to access and write to
693 group "foo/bar". You have to have permissions to access and write to
580 the last repository group ("bar" in this example)
694 the last repository group ("bar" in this example)
581
695
582 This command can only be run using an |authtoken| with at least
696 This command can only be run using an |authtoken| with at least
583 permissions to create repositories, or write permissions to
697 permissions to create repositories, or write permissions to
584 parent repository groups.
698 parent repository groups.
585
699
586 :param apiuser: This is filled automatically from the |authtoken|.
700 :param apiuser: This is filled automatically from the |authtoken|.
587 :type apiuser: AuthUser
701 :type apiuser: AuthUser
588 :param repo_name: Set the repository name.
702 :param repo_name: Set the repository name.
589 :type repo_name: str
703 :type repo_name: str
590 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
704 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
591 :type repo_type: str
705 :type repo_type: str
592 :param owner: user_id or username
706 :param owner: user_id or username
593 :type owner: Optional(str)
707 :type owner: Optional(str)
594 :param description: Set the repository description.
708 :param description: Set the repository description.
595 :type description: Optional(str)
709 :type description: Optional(str)
596 :param private: set repository as private
710 :param private: set repository as private
597 :type private: bool
711 :type private: bool
598 :param clone_uri: set clone_uri
712 :param clone_uri: set clone_uri
599 :type clone_uri: str
713 :type clone_uri: str
600 :param push_uri: set push_uri
714 :param push_uri: set push_uri
601 :type push_uri: str
715 :type push_uri: str
602 :param landing_rev: <rev_type>:<rev>
716 :param landing_rev: <rev_type>:<rev>
603 :type landing_rev: str
717 :type landing_rev: str
604 :param enable_locking:
718 :param enable_locking:
605 :type enable_locking: bool
719 :type enable_locking: bool
606 :param enable_downloads:
720 :param enable_downloads:
607 :type enable_downloads: bool
721 :type enable_downloads: bool
608 :param enable_statistics:
722 :param enable_statistics:
609 :type enable_statistics: bool
723 :type enable_statistics: bool
610 :param copy_permissions: Copy permission from group in which the
724 :param copy_permissions: Copy permission from group in which the
611 repository is being created.
725 repository is being created.
612 :type copy_permissions: bool
726 :type copy_permissions: bool
613
727
614
728
615 Example output:
729 Example output:
616
730
617 .. code-block:: bash
731 .. code-block:: bash
618
732
619 id : <id_given_in_input>
733 id : <id_given_in_input>
620 result: {
734 result: {
621 "msg": "Created new repository `<reponame>`",
735 "msg": "Created new repository `<reponame>`",
622 "success": true,
736 "success": true,
623 "task": "<celery task id or None if done sync>"
737 "task": "<celery task id or None if done sync>"
624 }
738 }
625 error: null
739 error: null
626
740
627
741
628 Example error output:
742 Example error output:
629
743
630 .. code-block:: bash
744 .. code-block:: bash
631
745
632 id : <id_given_in_input>
746 id : <id_given_in_input>
633 result : null
747 result : null
634 error : {
748 error : {
635 'failed to create repository `<repo_name>`'
749 'failed to create repository `<repo_name>`'
636 }
750 }
637
751
638 """
752 """
639
753
640 owner = validate_set_owner_permissions(apiuser, owner)
754 owner = validate_set_owner_permissions(apiuser, owner)
641
755
642 description = Optional.extract(description)
756 description = Optional.extract(description)
643 copy_permissions = Optional.extract(copy_permissions)
757 copy_permissions = Optional.extract(copy_permissions)
644 clone_uri = Optional.extract(clone_uri)
758 clone_uri = Optional.extract(clone_uri)
645 push_uri = Optional.extract(push_uri)
759 push_uri = Optional.extract(push_uri)
646 landing_commit_ref = Optional.extract(landing_rev)
760 landing_commit_ref = Optional.extract(landing_rev)
647
761
648 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
762 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
649 if isinstance(private, Optional):
763 if isinstance(private, Optional):
650 private = defs.get('repo_private') or Optional.extract(private)
764 private = defs.get('repo_private') or Optional.extract(private)
651 if isinstance(repo_type, Optional):
765 if isinstance(repo_type, Optional):
652 repo_type = defs.get('repo_type')
766 repo_type = defs.get('repo_type')
653 if isinstance(enable_statistics, Optional):
767 if isinstance(enable_statistics, Optional):
654 enable_statistics = defs.get('repo_enable_statistics')
768 enable_statistics = defs.get('repo_enable_statistics')
655 if isinstance(enable_locking, Optional):
769 if isinstance(enable_locking, Optional):
656 enable_locking = defs.get('repo_enable_locking')
770 enable_locking = defs.get('repo_enable_locking')
657 if isinstance(enable_downloads, Optional):
771 if isinstance(enable_downloads, Optional):
658 enable_downloads = defs.get('repo_enable_downloads')
772 enable_downloads = defs.get('repo_enable_downloads')
659
773
660 schema = repo_schema.RepoSchema().bind(
774 schema = repo_schema.RepoSchema().bind(
661 repo_type_options=rhodecode.BACKENDS.keys(),
775 repo_type_options=rhodecode.BACKENDS.keys(),
662 repo_type=repo_type,
776 repo_type=repo_type,
663 # user caller
777 # user caller
664 user=apiuser)
778 user=apiuser)
665
779
666 try:
780 try:
667 schema_data = schema.deserialize(dict(
781 schema_data = schema.deserialize(dict(
668 repo_name=repo_name,
782 repo_name=repo_name,
669 repo_type=repo_type,
783 repo_type=repo_type,
670 repo_owner=owner.username,
784 repo_owner=owner.username,
671 repo_description=description,
785 repo_description=description,
672 repo_landing_commit_ref=landing_commit_ref,
786 repo_landing_commit_ref=landing_commit_ref,
673 repo_clone_uri=clone_uri,
787 repo_clone_uri=clone_uri,
674 repo_push_uri=push_uri,
788 repo_push_uri=push_uri,
675 repo_private=private,
789 repo_private=private,
676 repo_copy_permissions=copy_permissions,
790 repo_copy_permissions=copy_permissions,
677 repo_enable_statistics=enable_statistics,
791 repo_enable_statistics=enable_statistics,
678 repo_enable_downloads=enable_downloads,
792 repo_enable_downloads=enable_downloads,
679 repo_enable_locking=enable_locking))
793 repo_enable_locking=enable_locking))
680 except validation_schema.Invalid as err:
794 except validation_schema.Invalid as err:
681 raise JSONRPCValidationError(colander_exc=err)
795 raise JSONRPCValidationError(colander_exc=err)
682
796
683 try:
797 try:
684 data = {
798 data = {
685 'owner': owner,
799 'owner': owner,
686 'repo_name': schema_data['repo_group']['repo_name_without_group'],
800 'repo_name': schema_data['repo_group']['repo_name_without_group'],
687 'repo_name_full': schema_data['repo_name'],
801 'repo_name_full': schema_data['repo_name'],
688 'repo_group': schema_data['repo_group']['repo_group_id'],
802 'repo_group': schema_data['repo_group']['repo_group_id'],
689 'repo_type': schema_data['repo_type'],
803 'repo_type': schema_data['repo_type'],
690 'repo_description': schema_data['repo_description'],
804 'repo_description': schema_data['repo_description'],
691 'repo_private': schema_data['repo_private'],
805 'repo_private': schema_data['repo_private'],
692 'clone_uri': schema_data['repo_clone_uri'],
806 'clone_uri': schema_data['repo_clone_uri'],
693 'push_uri': schema_data['repo_push_uri'],
807 'push_uri': schema_data['repo_push_uri'],
694 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
808 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
695 'enable_statistics': schema_data['repo_enable_statistics'],
809 'enable_statistics': schema_data['repo_enable_statistics'],
696 'enable_locking': schema_data['repo_enable_locking'],
810 'enable_locking': schema_data['repo_enable_locking'],
697 'enable_downloads': schema_data['repo_enable_downloads'],
811 'enable_downloads': schema_data['repo_enable_downloads'],
698 'repo_copy_permissions': schema_data['repo_copy_permissions'],
812 'repo_copy_permissions': schema_data['repo_copy_permissions'],
699 }
813 }
700
814
701 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
815 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
702 task_id = get_task_id(task)
816 task_id = get_task_id(task)
703 # no commit, it's done in RepoModel, or async via celery
817 # no commit, it's done in RepoModel, or async via celery
704 return {
818 return {
705 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
819 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
706 'success': True, # cannot return the repo data here since fork
820 'success': True, # cannot return the repo data here since fork
707 # can be done async
821 # can be done async
708 'task': task_id
822 'task': task_id
709 }
823 }
710 except Exception:
824 except Exception:
711 log.exception(
825 log.exception(
712 u"Exception while trying to create the repository %s",
826 u"Exception while trying to create the repository %s",
713 schema_data['repo_name'])
827 schema_data['repo_name'])
714 raise JSONRPCError(
828 raise JSONRPCError(
715 'failed to create repository `%s`' % (schema_data['repo_name'],))
829 'failed to create repository `%s`' % (schema_data['repo_name'],))
716
830
717
831
718 @jsonrpc_method()
832 @jsonrpc_method()
719 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
833 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
720 description=Optional('')):
834 description=Optional('')):
721 """
835 """
722 Adds an extra field to a repository.
836 Adds an extra field to a repository.
723
837
724 This command can only be run using an |authtoken| with at least
838 This command can only be run using an |authtoken| with at least
725 write permissions to the |repo|.
839 write permissions to the |repo|.
726
840
727 :param apiuser: This is filled automatically from the |authtoken|.
841 :param apiuser: This is filled automatically from the |authtoken|.
728 :type apiuser: AuthUser
842 :type apiuser: AuthUser
729 :param repoid: Set the repository name or repository id.
843 :param repoid: Set the repository name or repository id.
730 :type repoid: str or int
844 :type repoid: str or int
731 :param key: Create a unique field key for this repository.
845 :param key: Create a unique field key for this repository.
732 :type key: str
846 :type key: str
733 :param label:
847 :param label:
734 :type label: Optional(str)
848 :type label: Optional(str)
735 :param description:
849 :param description:
736 :type description: Optional(str)
850 :type description: Optional(str)
737 """
851 """
738 repo = get_repo_or_error(repoid)
852 repo = get_repo_or_error(repoid)
739 if not has_superadmin_permission(apiuser):
853 if not has_superadmin_permission(apiuser):
740 _perms = ('repository.admin',)
854 _perms = ('repository.admin',)
741 validate_repo_permissions(apiuser, repoid, repo, _perms)
855 validate_repo_permissions(apiuser, repoid, repo, _perms)
742
856
743 label = Optional.extract(label) or key
857 label = Optional.extract(label) or key
744 description = Optional.extract(description)
858 description = Optional.extract(description)
745
859
746 field = RepositoryField.get_by_key_name(key, repo)
860 field = RepositoryField.get_by_key_name(key, repo)
747 if field:
861 if field:
748 raise JSONRPCError('Field with key '
862 raise JSONRPCError('Field with key '
749 '`%s` exists for repo `%s`' % (key, repoid))
863 '`%s` exists for repo `%s`' % (key, repoid))
750
864
751 try:
865 try:
752 RepoModel().add_repo_field(repo, key, field_label=label,
866 RepoModel().add_repo_field(repo, key, field_label=label,
753 field_desc=description)
867 field_desc=description)
754 Session().commit()
868 Session().commit()
755 return {
869 return {
756 'msg': "Added new repository field `%s`" % (key,),
870 'msg': "Added new repository field `%s`" % (key,),
757 'success': True,
871 'success': True,
758 }
872 }
759 except Exception:
873 except Exception:
760 log.exception("Exception occurred while trying to add field to repo")
874 log.exception("Exception occurred while trying to add field to repo")
761 raise JSONRPCError(
875 raise JSONRPCError(
762 'failed to create new field for repository `%s`' % (repoid,))
876 'failed to create new field for repository `%s`' % (repoid,))
763
877
764
878
765 @jsonrpc_method()
879 @jsonrpc_method()
766 def remove_field_from_repo(request, apiuser, repoid, key):
880 def remove_field_from_repo(request, apiuser, repoid, key):
767 """
881 """
768 Removes an extra field from a repository.
882 Removes an extra field from a repository.
769
883
770 This command can only be run using an |authtoken| with at least
884 This command can only be run using an |authtoken| with at least
771 write permissions to the |repo|.
885 write permissions to the |repo|.
772
886
773 :param apiuser: This is filled automatically from the |authtoken|.
887 :param apiuser: This is filled automatically from the |authtoken|.
774 :type apiuser: AuthUser
888 :type apiuser: AuthUser
775 :param repoid: Set the repository name or repository ID.
889 :param repoid: Set the repository name or repository ID.
776 :type repoid: str or int
890 :type repoid: str or int
777 :param key: Set the unique field key for this repository.
891 :param key: Set the unique field key for this repository.
778 :type key: str
892 :type key: str
779 """
893 """
780
894
781 repo = get_repo_or_error(repoid)
895 repo = get_repo_or_error(repoid)
782 if not has_superadmin_permission(apiuser):
896 if not has_superadmin_permission(apiuser):
783 _perms = ('repository.admin',)
897 _perms = ('repository.admin',)
784 validate_repo_permissions(apiuser, repoid, repo, _perms)
898 validate_repo_permissions(apiuser, repoid, repo, _perms)
785
899
786 field = RepositoryField.get_by_key_name(key, repo)
900 field = RepositoryField.get_by_key_name(key, repo)
787 if not field:
901 if not field:
788 raise JSONRPCError('Field with key `%s` does not '
902 raise JSONRPCError('Field with key `%s` does not '
789 'exists for repo `%s`' % (key, repoid))
903 'exists for repo `%s`' % (key, repoid))
790
904
791 try:
905 try:
792 RepoModel().delete_repo_field(repo, field_key=key)
906 RepoModel().delete_repo_field(repo, field_key=key)
793 Session().commit()
907 Session().commit()
794 return {
908 return {
795 'msg': "Deleted repository field `%s`" % (key,),
909 'msg': "Deleted repository field `%s`" % (key,),
796 'success': True,
910 'success': True,
797 }
911 }
798 except Exception:
912 except Exception:
799 log.exception(
913 log.exception(
800 "Exception occurred while trying to delete field from repo")
914 "Exception occurred while trying to delete field from repo")
801 raise JSONRPCError(
915 raise JSONRPCError(
802 'failed to delete field for repository `%s`' % (repoid,))
916 'failed to delete field for repository `%s`' % (repoid,))
803
917
804
918
805 @jsonrpc_method()
919 @jsonrpc_method()
806 def update_repo(
920 def update_repo(
807 request, apiuser, repoid, repo_name=Optional(None),
921 request, apiuser, repoid, repo_name=Optional(None),
808 owner=Optional(OAttr('apiuser')), description=Optional(''),
922 owner=Optional(OAttr('apiuser')), description=Optional(''),
809 private=Optional(False),
923 private=Optional(False),
810 clone_uri=Optional(None), push_uri=Optional(None),
924 clone_uri=Optional(None), push_uri=Optional(None),
811 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
925 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
812 enable_statistics=Optional(False),
926 enable_statistics=Optional(False),
813 enable_locking=Optional(False),
927 enable_locking=Optional(False),
814 enable_downloads=Optional(False), fields=Optional('')):
928 enable_downloads=Optional(False), fields=Optional('')):
815 """
929 """
816 Updates a repository with the given information.
930 Updates a repository with the given information.
817
931
818 This command can only be run using an |authtoken| with at least
932 This command can only be run using an |authtoken| with at least
819 admin permissions to the |repo|.
933 admin permissions to the |repo|.
820
934
821 * If the repository name contains "/", repository will be updated
935 * If the repository name contains "/", repository will be updated
822 accordingly with a repository group or nested repository groups
936 accordingly with a repository group or nested repository groups
823
937
824 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
938 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
825 called "repo-test" and place it inside group "foo/bar".
939 called "repo-test" and place it inside group "foo/bar".
826 You have to have permissions to access and write to the last repository
940 You have to have permissions to access and write to the last repository
827 group ("bar" in this example)
941 group ("bar" in this example)
828
942
829 :param apiuser: This is filled automatically from the |authtoken|.
943 :param apiuser: This is filled automatically from the |authtoken|.
830 :type apiuser: AuthUser
944 :type apiuser: AuthUser
831 :param repoid: repository name or repository ID.
945 :param repoid: repository name or repository ID.
832 :type repoid: str or int
946 :type repoid: str or int
833 :param repo_name: Update the |repo| name, including the
947 :param repo_name: Update the |repo| name, including the
834 repository group it's in.
948 repository group it's in.
835 :type repo_name: str
949 :type repo_name: str
836 :param owner: Set the |repo| owner.
950 :param owner: Set the |repo| owner.
837 :type owner: str
951 :type owner: str
838 :param fork_of: Set the |repo| as fork of another |repo|.
952 :param fork_of: Set the |repo| as fork of another |repo|.
839 :type fork_of: str
953 :type fork_of: str
840 :param description: Update the |repo| description.
954 :param description: Update the |repo| description.
841 :type description: str
955 :type description: str
842 :param private: Set the |repo| as private. (True | False)
956 :param private: Set the |repo| as private. (True | False)
843 :type private: bool
957 :type private: bool
844 :param clone_uri: Update the |repo| clone URI.
958 :param clone_uri: Update the |repo| clone URI.
845 :type clone_uri: str
959 :type clone_uri: str
846 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
960 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
847 :type landing_rev: str
961 :type landing_rev: str
848 :param enable_statistics: Enable statistics on the |repo|, (True | False).
962 :param enable_statistics: Enable statistics on the |repo|, (True | False).
849 :type enable_statistics: bool
963 :type enable_statistics: bool
850 :param enable_locking: Enable |repo| locking.
964 :param enable_locking: Enable |repo| locking.
851 :type enable_locking: bool
965 :type enable_locking: bool
852 :param enable_downloads: Enable downloads from the |repo|, (True | False).
966 :param enable_downloads: Enable downloads from the |repo|, (True | False).
853 :type enable_downloads: bool
967 :type enable_downloads: bool
854 :param fields: Add extra fields to the |repo|. Use the following
968 :param fields: Add extra fields to the |repo|. Use the following
855 example format: ``field_key=field_val,field_key2=fieldval2``.
969 example format: ``field_key=field_val,field_key2=fieldval2``.
856 Escape ', ' with \,
970 Escape ', ' with \,
857 :type fields: str
971 :type fields: str
858 """
972 """
859
973
860 repo = get_repo_or_error(repoid)
974 repo = get_repo_or_error(repoid)
861
975
862 include_secrets = False
976 include_secrets = False
863 if not has_superadmin_permission(apiuser):
977 if not has_superadmin_permission(apiuser):
864 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
978 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
865 else:
979 else:
866 include_secrets = True
980 include_secrets = True
867
981
868 updates = dict(
982 updates = dict(
869 repo_name=repo_name
983 repo_name=repo_name
870 if not isinstance(repo_name, Optional) else repo.repo_name,
984 if not isinstance(repo_name, Optional) else repo.repo_name,
871
985
872 fork_id=fork_of
986 fork_id=fork_of
873 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
987 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
874
988
875 user=owner
989 user=owner
876 if not isinstance(owner, Optional) else repo.user.username,
990 if not isinstance(owner, Optional) else repo.user.username,
877
991
878 repo_description=description
992 repo_description=description
879 if not isinstance(description, Optional) else repo.description,
993 if not isinstance(description, Optional) else repo.description,
880
994
881 repo_private=private
995 repo_private=private
882 if not isinstance(private, Optional) else repo.private,
996 if not isinstance(private, Optional) else repo.private,
883
997
884 clone_uri=clone_uri
998 clone_uri=clone_uri
885 if not isinstance(clone_uri, Optional) else repo.clone_uri,
999 if not isinstance(clone_uri, Optional) else repo.clone_uri,
886
1000
887 push_uri=push_uri
1001 push_uri=push_uri
888 if not isinstance(push_uri, Optional) else repo.push_uri,
1002 if not isinstance(push_uri, Optional) else repo.push_uri,
889
1003
890 repo_landing_rev=landing_rev
1004 repo_landing_rev=landing_rev
891 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1005 if not isinstance(landing_rev, Optional) else repo._landing_revision,
892
1006
893 repo_enable_statistics=enable_statistics
1007 repo_enable_statistics=enable_statistics
894 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1008 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
895
1009
896 repo_enable_locking=enable_locking
1010 repo_enable_locking=enable_locking
897 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1011 if not isinstance(enable_locking, Optional) else repo.enable_locking,
898
1012
899 repo_enable_downloads=enable_downloads
1013 repo_enable_downloads=enable_downloads
900 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1014 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
901
1015
902 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1016 ref_choices, _labels = ScmModel().get_repo_landing_revs(
903 request.translate, repo=repo)
1017 request.translate, repo=repo)
904
1018
905 old_values = repo.get_api_data()
1019 old_values = repo.get_api_data()
906 repo_type = repo.repo_type
1020 repo_type = repo.repo_type
907 schema = repo_schema.RepoSchema().bind(
1021 schema = repo_schema.RepoSchema().bind(
908 repo_type_options=rhodecode.BACKENDS.keys(),
1022 repo_type_options=rhodecode.BACKENDS.keys(),
909 repo_ref_options=ref_choices,
1023 repo_ref_options=ref_choices,
910 repo_type=repo_type,
1024 repo_type=repo_type,
911 # user caller
1025 # user caller
912 user=apiuser,
1026 user=apiuser,
913 old_values=old_values)
1027 old_values=old_values)
914 try:
1028 try:
915 schema_data = schema.deserialize(dict(
1029 schema_data = schema.deserialize(dict(
916 # we save old value, users cannot change type
1030 # we save old value, users cannot change type
917 repo_type=repo_type,
1031 repo_type=repo_type,
918
1032
919 repo_name=updates['repo_name'],
1033 repo_name=updates['repo_name'],
920 repo_owner=updates['user'],
1034 repo_owner=updates['user'],
921 repo_description=updates['repo_description'],
1035 repo_description=updates['repo_description'],
922 repo_clone_uri=updates['clone_uri'],
1036 repo_clone_uri=updates['clone_uri'],
923 repo_push_uri=updates['push_uri'],
1037 repo_push_uri=updates['push_uri'],
924 repo_fork_of=updates['fork_id'],
1038 repo_fork_of=updates['fork_id'],
925 repo_private=updates['repo_private'],
1039 repo_private=updates['repo_private'],
926 repo_landing_commit_ref=updates['repo_landing_rev'],
1040 repo_landing_commit_ref=updates['repo_landing_rev'],
927 repo_enable_statistics=updates['repo_enable_statistics'],
1041 repo_enable_statistics=updates['repo_enable_statistics'],
928 repo_enable_downloads=updates['repo_enable_downloads'],
1042 repo_enable_downloads=updates['repo_enable_downloads'],
929 repo_enable_locking=updates['repo_enable_locking']))
1043 repo_enable_locking=updates['repo_enable_locking']))
930 except validation_schema.Invalid as err:
1044 except validation_schema.Invalid as err:
931 raise JSONRPCValidationError(colander_exc=err)
1045 raise JSONRPCValidationError(colander_exc=err)
932
1046
933 # save validated data back into the updates dict
1047 # save validated data back into the updates dict
934 validated_updates = dict(
1048 validated_updates = dict(
935 repo_name=schema_data['repo_group']['repo_name_without_group'],
1049 repo_name=schema_data['repo_group']['repo_name_without_group'],
936 repo_group=schema_data['repo_group']['repo_group_id'],
1050 repo_group=schema_data['repo_group']['repo_group_id'],
937
1051
938 user=schema_data['repo_owner'],
1052 user=schema_data['repo_owner'],
939 repo_description=schema_data['repo_description'],
1053 repo_description=schema_data['repo_description'],
940 repo_private=schema_data['repo_private'],
1054 repo_private=schema_data['repo_private'],
941 clone_uri=schema_data['repo_clone_uri'],
1055 clone_uri=schema_data['repo_clone_uri'],
942 push_uri=schema_data['repo_push_uri'],
1056 push_uri=schema_data['repo_push_uri'],
943 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1057 repo_landing_rev=schema_data['repo_landing_commit_ref'],
944 repo_enable_statistics=schema_data['repo_enable_statistics'],
1058 repo_enable_statistics=schema_data['repo_enable_statistics'],
945 repo_enable_locking=schema_data['repo_enable_locking'],
1059 repo_enable_locking=schema_data['repo_enable_locking'],
946 repo_enable_downloads=schema_data['repo_enable_downloads'],
1060 repo_enable_downloads=schema_data['repo_enable_downloads'],
947 )
1061 )
948
1062
949 if schema_data['repo_fork_of']:
1063 if schema_data['repo_fork_of']:
950 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1064 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
951 validated_updates['fork_id'] = fork_repo.repo_id
1065 validated_updates['fork_id'] = fork_repo.repo_id
952
1066
953 # extra fields
1067 # extra fields
954 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1068 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
955 if fields:
1069 if fields:
956 validated_updates.update(fields)
1070 validated_updates.update(fields)
957
1071
958 try:
1072 try:
959 RepoModel().update(repo, **validated_updates)
1073 RepoModel().update(repo, **validated_updates)
960 audit_logger.store_api(
1074 audit_logger.store_api(
961 'repo.edit', action_data={'old_data': old_values},
1075 'repo.edit', action_data={'old_data': old_values},
962 user=apiuser, repo=repo)
1076 user=apiuser, repo=repo)
963 Session().commit()
1077 Session().commit()
964 return {
1078 return {
965 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1079 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
966 'repository': repo.get_api_data(include_secrets=include_secrets)
1080 'repository': repo.get_api_data(include_secrets=include_secrets)
967 }
1081 }
968 except Exception:
1082 except Exception:
969 log.exception(
1083 log.exception(
970 u"Exception while trying to update the repository %s",
1084 u"Exception while trying to update the repository %s",
971 repoid)
1085 repoid)
972 raise JSONRPCError('failed to update repo `%s`' % repoid)
1086 raise JSONRPCError('failed to update repo `%s`' % repoid)
973
1087
974
1088
975 @jsonrpc_method()
1089 @jsonrpc_method()
976 def fork_repo(request, apiuser, repoid, fork_name,
1090 def fork_repo(request, apiuser, repoid, fork_name,
977 owner=Optional(OAttr('apiuser')),
1091 owner=Optional(OAttr('apiuser')),
978 description=Optional(''),
1092 description=Optional(''),
979 private=Optional(False),
1093 private=Optional(False),
980 clone_uri=Optional(None),
1094 clone_uri=Optional(None),
981 landing_rev=Optional('rev:tip'),
1095 landing_rev=Optional('rev:tip'),
982 copy_permissions=Optional(False)):
1096 copy_permissions=Optional(False)):
983 """
1097 """
984 Creates a fork of the specified |repo|.
1098 Creates a fork of the specified |repo|.
985
1099
986 * If the fork_name contains "/", fork will be created inside
1100 * If the fork_name contains "/", fork will be created inside
987 a repository group or nested repository groups
1101 a repository group or nested repository groups
988
1102
989 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1103 For example "foo/bar/fork-repo" will create fork called "fork-repo"
990 inside group "foo/bar". You have to have permissions to access and
1104 inside group "foo/bar". You have to have permissions to access and
991 write to the last repository group ("bar" in this example)
1105 write to the last repository group ("bar" in this example)
992
1106
993 This command can only be run using an |authtoken| with minimum
1107 This command can only be run using an |authtoken| with minimum
994 read permissions of the forked repo, create fork permissions for an user.
1108 read permissions of the forked repo, create fork permissions for an user.
995
1109
996 :param apiuser: This is filled automatically from the |authtoken|.
1110 :param apiuser: This is filled automatically from the |authtoken|.
997 :type apiuser: AuthUser
1111 :type apiuser: AuthUser
998 :param repoid: Set repository name or repository ID.
1112 :param repoid: Set repository name or repository ID.
999 :type repoid: str or int
1113 :type repoid: str or int
1000 :param fork_name: Set the fork name, including it's repository group membership.
1114 :param fork_name: Set the fork name, including it's repository group membership.
1001 :type fork_name: str
1115 :type fork_name: str
1002 :param owner: Set the fork owner.
1116 :param owner: Set the fork owner.
1003 :type owner: str
1117 :type owner: str
1004 :param description: Set the fork description.
1118 :param description: Set the fork description.
1005 :type description: str
1119 :type description: str
1006 :param copy_permissions: Copy permissions from parent |repo|. The
1120 :param copy_permissions: Copy permissions from parent |repo|. The
1007 default is False.
1121 default is False.
1008 :type copy_permissions: bool
1122 :type copy_permissions: bool
1009 :param private: Make the fork private. The default is False.
1123 :param private: Make the fork private. The default is False.
1010 :type private: bool
1124 :type private: bool
1011 :param landing_rev: Set the landing revision. The default is tip.
1125 :param landing_rev: Set the landing revision. The default is tip.
1012
1126
1013 Example output:
1127 Example output:
1014
1128
1015 .. code-block:: bash
1129 .. code-block:: bash
1016
1130
1017 id : <id_for_response>
1131 id : <id_for_response>
1018 api_key : "<api_key>"
1132 api_key : "<api_key>"
1019 args: {
1133 args: {
1020 "repoid" : "<reponame or repo_id>",
1134 "repoid" : "<reponame or repo_id>",
1021 "fork_name": "<forkname>",
1135 "fork_name": "<forkname>",
1022 "owner": "<username or user_id = Optional(=apiuser)>",
1136 "owner": "<username or user_id = Optional(=apiuser)>",
1023 "description": "<description>",
1137 "description": "<description>",
1024 "copy_permissions": "<bool>",
1138 "copy_permissions": "<bool>",
1025 "private": "<bool>",
1139 "private": "<bool>",
1026 "landing_rev": "<landing_rev>"
1140 "landing_rev": "<landing_rev>"
1027 }
1141 }
1028
1142
1029 Example error output:
1143 Example error output:
1030
1144
1031 .. code-block:: bash
1145 .. code-block:: bash
1032
1146
1033 id : <id_given_in_input>
1147 id : <id_given_in_input>
1034 result: {
1148 result: {
1035 "msg": "Created fork of `<reponame>` as `<forkname>`",
1149 "msg": "Created fork of `<reponame>` as `<forkname>`",
1036 "success": true,
1150 "success": true,
1037 "task": "<celery task id or None if done sync>"
1151 "task": "<celery task id or None if done sync>"
1038 }
1152 }
1039 error: null
1153 error: null
1040
1154
1041 """
1155 """
1042
1156
1043 repo = get_repo_or_error(repoid)
1157 repo = get_repo_or_error(repoid)
1044 repo_name = repo.repo_name
1158 repo_name = repo.repo_name
1045
1159
1046 if not has_superadmin_permission(apiuser):
1160 if not has_superadmin_permission(apiuser):
1047 # check if we have at least read permission for
1161 # check if we have at least read permission for
1048 # this repo that we fork !
1162 # this repo that we fork !
1049 _perms = (
1163 _perms = (
1050 'repository.admin', 'repository.write', 'repository.read')
1164 'repository.admin', 'repository.write', 'repository.read')
1051 validate_repo_permissions(apiuser, repoid, repo, _perms)
1165 validate_repo_permissions(apiuser, repoid, repo, _perms)
1052
1166
1053 # check if the regular user has at least fork permissions as well
1167 # check if the regular user has at least fork permissions as well
1054 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1168 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1055 raise JSONRPCForbidden()
1169 raise JSONRPCForbidden()
1056
1170
1057 # check if user can set owner parameter
1171 # check if user can set owner parameter
1058 owner = validate_set_owner_permissions(apiuser, owner)
1172 owner = validate_set_owner_permissions(apiuser, owner)
1059
1173
1060 description = Optional.extract(description)
1174 description = Optional.extract(description)
1061 copy_permissions = Optional.extract(copy_permissions)
1175 copy_permissions = Optional.extract(copy_permissions)
1062 clone_uri = Optional.extract(clone_uri)
1176 clone_uri = Optional.extract(clone_uri)
1063 landing_commit_ref = Optional.extract(landing_rev)
1177 landing_commit_ref = Optional.extract(landing_rev)
1064 private = Optional.extract(private)
1178 private = Optional.extract(private)
1065
1179
1066 schema = repo_schema.RepoSchema().bind(
1180 schema = repo_schema.RepoSchema().bind(
1067 repo_type_options=rhodecode.BACKENDS.keys(),
1181 repo_type_options=rhodecode.BACKENDS.keys(),
1068 repo_type=repo.repo_type,
1182 repo_type=repo.repo_type,
1069 # user caller
1183 # user caller
1070 user=apiuser)
1184 user=apiuser)
1071
1185
1072 try:
1186 try:
1073 schema_data = schema.deserialize(dict(
1187 schema_data = schema.deserialize(dict(
1074 repo_name=fork_name,
1188 repo_name=fork_name,
1075 repo_type=repo.repo_type,
1189 repo_type=repo.repo_type,
1076 repo_owner=owner.username,
1190 repo_owner=owner.username,
1077 repo_description=description,
1191 repo_description=description,
1078 repo_landing_commit_ref=landing_commit_ref,
1192 repo_landing_commit_ref=landing_commit_ref,
1079 repo_clone_uri=clone_uri,
1193 repo_clone_uri=clone_uri,
1080 repo_private=private,
1194 repo_private=private,
1081 repo_copy_permissions=copy_permissions))
1195 repo_copy_permissions=copy_permissions))
1082 except validation_schema.Invalid as err:
1196 except validation_schema.Invalid as err:
1083 raise JSONRPCValidationError(colander_exc=err)
1197 raise JSONRPCValidationError(colander_exc=err)
1084
1198
1085 try:
1199 try:
1086 data = {
1200 data = {
1087 'fork_parent_id': repo.repo_id,
1201 'fork_parent_id': repo.repo_id,
1088
1202
1089 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1203 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1090 'repo_name_full': schema_data['repo_name'],
1204 'repo_name_full': schema_data['repo_name'],
1091 'repo_group': schema_data['repo_group']['repo_group_id'],
1205 'repo_group': schema_data['repo_group']['repo_group_id'],
1092 'repo_type': schema_data['repo_type'],
1206 'repo_type': schema_data['repo_type'],
1093 'description': schema_data['repo_description'],
1207 'description': schema_data['repo_description'],
1094 'private': schema_data['repo_private'],
1208 'private': schema_data['repo_private'],
1095 'copy_permissions': schema_data['repo_copy_permissions'],
1209 'copy_permissions': schema_data['repo_copy_permissions'],
1096 'landing_rev': schema_data['repo_landing_commit_ref'],
1210 'landing_rev': schema_data['repo_landing_commit_ref'],
1097 }
1211 }
1098
1212
1099 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1213 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1100 # no commit, it's done in RepoModel, or async via celery
1214 # no commit, it's done in RepoModel, or async via celery
1101 task_id = get_task_id(task)
1215 task_id = get_task_id(task)
1102
1216
1103 return {
1217 return {
1104 'msg': 'Created fork of `%s` as `%s`' % (
1218 'msg': 'Created fork of `%s` as `%s`' % (
1105 repo.repo_name, schema_data['repo_name']),
1219 repo.repo_name, schema_data['repo_name']),
1106 'success': True, # cannot return the repo data here since fork
1220 'success': True, # cannot return the repo data here since fork
1107 # can be done async
1221 # can be done async
1108 'task': task_id
1222 'task': task_id
1109 }
1223 }
1110 except Exception:
1224 except Exception:
1111 log.exception(
1225 log.exception(
1112 u"Exception while trying to create fork %s",
1226 u"Exception while trying to create fork %s",
1113 schema_data['repo_name'])
1227 schema_data['repo_name'])
1114 raise JSONRPCError(
1228 raise JSONRPCError(
1115 'failed to fork repository `%s` as `%s`' % (
1229 'failed to fork repository `%s` as `%s`' % (
1116 repo_name, schema_data['repo_name']))
1230 repo_name, schema_data['repo_name']))
1117
1231
1118
1232
1119 @jsonrpc_method()
1233 @jsonrpc_method()
1120 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1234 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1121 """
1235 """
1122 Deletes a repository.
1236 Deletes a repository.
1123
1237
1124 * When the `forks` parameter is set it's possible to detach or delete
1238 * When the `forks` parameter is set it's possible to detach or delete
1125 forks of deleted repository.
1239 forks of deleted repository.
1126
1240
1127 This command can only be run using an |authtoken| with admin
1241 This command can only be run using an |authtoken| with admin
1128 permissions on the |repo|.
1242 permissions on the |repo|.
1129
1243
1130 :param apiuser: This is filled automatically from the |authtoken|.
1244 :param apiuser: This is filled automatically from the |authtoken|.
1131 :type apiuser: AuthUser
1245 :type apiuser: AuthUser
1132 :param repoid: Set the repository name or repository ID.
1246 :param repoid: Set the repository name or repository ID.
1133 :type repoid: str or int
1247 :type repoid: str or int
1134 :param forks: Set to `detach` or `delete` forks from the |repo|.
1248 :param forks: Set to `detach` or `delete` forks from the |repo|.
1135 :type forks: Optional(str)
1249 :type forks: Optional(str)
1136
1250
1137 Example error output:
1251 Example error output:
1138
1252
1139 .. code-block:: bash
1253 .. code-block:: bash
1140
1254
1141 id : <id_given_in_input>
1255 id : <id_given_in_input>
1142 result: {
1256 result: {
1143 "msg": "Deleted repository `<reponame>`",
1257 "msg": "Deleted repository `<reponame>`",
1144 "success": true
1258 "success": true
1145 }
1259 }
1146 error: null
1260 error: null
1147 """
1261 """
1148
1262
1149 repo = get_repo_or_error(repoid)
1263 repo = get_repo_or_error(repoid)
1150 repo_name = repo.repo_name
1264 repo_name = repo.repo_name
1151 if not has_superadmin_permission(apiuser):
1265 if not has_superadmin_permission(apiuser):
1152 _perms = ('repository.admin',)
1266 _perms = ('repository.admin',)
1153 validate_repo_permissions(apiuser, repoid, repo, _perms)
1267 validate_repo_permissions(apiuser, repoid, repo, _perms)
1154
1268
1155 try:
1269 try:
1156 handle_forks = Optional.extract(forks)
1270 handle_forks = Optional.extract(forks)
1157 _forks_msg = ''
1271 _forks_msg = ''
1158 _forks = [f for f in repo.forks]
1272 _forks = [f for f in repo.forks]
1159 if handle_forks == 'detach':
1273 if handle_forks == 'detach':
1160 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1274 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1161 elif handle_forks == 'delete':
1275 elif handle_forks == 'delete':
1162 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1276 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1163 elif _forks:
1277 elif _forks:
1164 raise JSONRPCError(
1278 raise JSONRPCError(
1165 'Cannot delete `%s` it still contains attached forks' %
1279 'Cannot delete `%s` it still contains attached forks' %
1166 (repo.repo_name,)
1280 (repo.repo_name,)
1167 )
1281 )
1168 old_data = repo.get_api_data()
1282 old_data = repo.get_api_data()
1169 RepoModel().delete(repo, forks=forks)
1283 RepoModel().delete(repo, forks=forks)
1170
1284
1171 repo = audit_logger.RepoWrap(repo_id=None,
1285 repo = audit_logger.RepoWrap(repo_id=None,
1172 repo_name=repo.repo_name)
1286 repo_name=repo.repo_name)
1173
1287
1174 audit_logger.store_api(
1288 audit_logger.store_api(
1175 'repo.delete', action_data={'old_data': old_data},
1289 'repo.delete', action_data={'old_data': old_data},
1176 user=apiuser, repo=repo)
1290 user=apiuser, repo=repo)
1177
1291
1178 ScmModel().mark_for_invalidation(repo_name, delete=True)
1292 ScmModel().mark_for_invalidation(repo_name, delete=True)
1179 Session().commit()
1293 Session().commit()
1180 return {
1294 return {
1181 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1295 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1182 'success': True
1296 'success': True
1183 }
1297 }
1184 except Exception:
1298 except Exception:
1185 log.exception("Exception occurred while trying to delete repo")
1299 log.exception("Exception occurred while trying to delete repo")
1186 raise JSONRPCError(
1300 raise JSONRPCError(
1187 'failed to delete repository `%s`' % (repo_name,)
1301 'failed to delete repository `%s`' % (repo_name,)
1188 )
1302 )
1189
1303
1190
1304
1191 #TODO: marcink, change name ?
1305 #TODO: marcink, change name ?
1192 @jsonrpc_method()
1306 @jsonrpc_method()
1193 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1307 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1194 """
1308 """
1195 Invalidates the cache for the specified repository.
1309 Invalidates the cache for the specified repository.
1196
1310
1197 This command can only be run using an |authtoken| with admin rights to
1311 This command can only be run using an |authtoken| with admin rights to
1198 the specified repository.
1312 the specified repository.
1199
1313
1200 This command takes the following options:
1314 This command takes the following options:
1201
1315
1202 :param apiuser: This is filled automatically from |authtoken|.
1316 :param apiuser: This is filled automatically from |authtoken|.
1203 :type apiuser: AuthUser
1317 :type apiuser: AuthUser
1204 :param repoid: Sets the repository name or repository ID.
1318 :param repoid: Sets the repository name or repository ID.
1205 :type repoid: str or int
1319 :type repoid: str or int
1206 :param delete_keys: This deletes the invalidated keys instead of
1320 :param delete_keys: This deletes the invalidated keys instead of
1207 just flagging them.
1321 just flagging them.
1208 :type delete_keys: Optional(``True`` | ``False``)
1322 :type delete_keys: Optional(``True`` | ``False``)
1209
1323
1210 Example output:
1324 Example output:
1211
1325
1212 .. code-block:: bash
1326 .. code-block:: bash
1213
1327
1214 id : <id_given_in_input>
1328 id : <id_given_in_input>
1215 result : {
1329 result : {
1216 'msg': Cache for repository `<repository name>` was invalidated,
1330 'msg': Cache for repository `<repository name>` was invalidated,
1217 'repository': <repository name>
1331 'repository': <repository name>
1218 }
1332 }
1219 error : null
1333 error : null
1220
1334
1221 Example error output:
1335 Example error output:
1222
1336
1223 .. code-block:: bash
1337 .. code-block:: bash
1224
1338
1225 id : <id_given_in_input>
1339 id : <id_given_in_input>
1226 result : null
1340 result : null
1227 error : {
1341 error : {
1228 'Error occurred during cache invalidation action'
1342 'Error occurred during cache invalidation action'
1229 }
1343 }
1230
1344
1231 """
1345 """
1232
1346
1233 repo = get_repo_or_error(repoid)
1347 repo = get_repo_or_error(repoid)
1234 if not has_superadmin_permission(apiuser):
1348 if not has_superadmin_permission(apiuser):
1235 _perms = ('repository.admin', 'repository.write',)
1349 _perms = ('repository.admin', 'repository.write',)
1236 validate_repo_permissions(apiuser, repoid, repo, _perms)
1350 validate_repo_permissions(apiuser, repoid, repo, _perms)
1237
1351
1238 delete = Optional.extract(delete_keys)
1352 delete = Optional.extract(delete_keys)
1239 try:
1353 try:
1240 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1354 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1241 return {
1355 return {
1242 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1356 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1243 'repository': repo.repo_name
1357 'repository': repo.repo_name
1244 }
1358 }
1245 except Exception:
1359 except Exception:
1246 log.exception(
1360 log.exception(
1247 "Exception occurred while trying to invalidate repo cache")
1361 "Exception occurred while trying to invalidate repo cache")
1248 raise JSONRPCError(
1362 raise JSONRPCError(
1249 'Error occurred during cache invalidation action'
1363 'Error occurred during cache invalidation action'
1250 )
1364 )
1251
1365
1252
1366
1253 #TODO: marcink, change name ?
1367 #TODO: marcink, change name ?
1254 @jsonrpc_method()
1368 @jsonrpc_method()
1255 def lock(request, apiuser, repoid, locked=Optional(None),
1369 def lock(request, apiuser, repoid, locked=Optional(None),
1256 userid=Optional(OAttr('apiuser'))):
1370 userid=Optional(OAttr('apiuser'))):
1257 """
1371 """
1258 Sets the lock state of the specified |repo| by the given user.
1372 Sets the lock state of the specified |repo| by the given user.
1259 From more information, see :ref:`repo-locking`.
1373 From more information, see :ref:`repo-locking`.
1260
1374
1261 * If the ``userid`` option is not set, the repository is locked to the
1375 * If the ``userid`` option is not set, the repository is locked to the
1262 user who called the method.
1376 user who called the method.
1263 * If the ``locked`` parameter is not set, the current lock state of the
1377 * If the ``locked`` parameter is not set, the current lock state of the
1264 repository is displayed.
1378 repository is displayed.
1265
1379
1266 This command can only be run using an |authtoken| with admin rights to
1380 This command can only be run using an |authtoken| with admin rights to
1267 the specified repository.
1381 the specified repository.
1268
1382
1269 This command takes the following options:
1383 This command takes the following options:
1270
1384
1271 :param apiuser: This is filled automatically from the |authtoken|.
1385 :param apiuser: This is filled automatically from the |authtoken|.
1272 :type apiuser: AuthUser
1386 :type apiuser: AuthUser
1273 :param repoid: Sets the repository name or repository ID.
1387 :param repoid: Sets the repository name or repository ID.
1274 :type repoid: str or int
1388 :type repoid: str or int
1275 :param locked: Sets the lock state.
1389 :param locked: Sets the lock state.
1276 :type locked: Optional(``True`` | ``False``)
1390 :type locked: Optional(``True`` | ``False``)
1277 :param userid: Set the repository lock to this user.
1391 :param userid: Set the repository lock to this user.
1278 :type userid: Optional(str or int)
1392 :type userid: Optional(str or int)
1279
1393
1280 Example error output:
1394 Example error output:
1281
1395
1282 .. code-block:: bash
1396 .. code-block:: bash
1283
1397
1284 id : <id_given_in_input>
1398 id : <id_given_in_input>
1285 result : {
1399 result : {
1286 'repo': '<reponame>',
1400 'repo': '<reponame>',
1287 'locked': <bool: lock state>,
1401 'locked': <bool: lock state>,
1288 'locked_since': <int: lock timestamp>,
1402 'locked_since': <int: lock timestamp>,
1289 'locked_by': <username of person who made the lock>,
1403 'locked_by': <username of person who made the lock>,
1290 'lock_reason': <str: reason for locking>,
1404 'lock_reason': <str: reason for locking>,
1291 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1405 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1292 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1406 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1293 or
1407 or
1294 'msg': 'Repo `<repository name>` not locked.'
1408 'msg': 'Repo `<repository name>` not locked.'
1295 or
1409 or
1296 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1410 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1297 }
1411 }
1298 error : null
1412 error : null
1299
1413
1300 Example error output:
1414 Example error output:
1301
1415
1302 .. code-block:: bash
1416 .. code-block:: bash
1303
1417
1304 id : <id_given_in_input>
1418 id : <id_given_in_input>
1305 result : null
1419 result : null
1306 error : {
1420 error : {
1307 'Error occurred locking repository `<reponame>`'
1421 'Error occurred locking repository `<reponame>`'
1308 }
1422 }
1309 """
1423 """
1310
1424
1311 repo = get_repo_or_error(repoid)
1425 repo = get_repo_or_error(repoid)
1312 if not has_superadmin_permission(apiuser):
1426 if not has_superadmin_permission(apiuser):
1313 # check if we have at least write permission for this repo !
1427 # check if we have at least write permission for this repo !
1314 _perms = ('repository.admin', 'repository.write',)
1428 _perms = ('repository.admin', 'repository.write',)
1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1429 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316
1430
1317 # make sure normal user does not pass someone else userid,
1431 # make sure normal user does not pass someone else userid,
1318 # he is not allowed to do that
1432 # he is not allowed to do that
1319 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1433 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1320 raise JSONRPCError('userid is not the same as your user')
1434 raise JSONRPCError('userid is not the same as your user')
1321
1435
1322 if isinstance(userid, Optional):
1436 if isinstance(userid, Optional):
1323 userid = apiuser.user_id
1437 userid = apiuser.user_id
1324
1438
1325 user = get_user_or_error(userid)
1439 user = get_user_or_error(userid)
1326
1440
1327 if isinstance(locked, Optional):
1441 if isinstance(locked, Optional):
1328 lockobj = repo.locked
1442 lockobj = repo.locked
1329
1443
1330 if lockobj[0] is None:
1444 if lockobj[0] is None:
1331 _d = {
1445 _d = {
1332 'repo': repo.repo_name,
1446 'repo': repo.repo_name,
1333 'locked': False,
1447 'locked': False,
1334 'locked_since': None,
1448 'locked_since': None,
1335 'locked_by': None,
1449 'locked_by': None,
1336 'lock_reason': None,
1450 'lock_reason': None,
1337 'lock_state_changed': False,
1451 'lock_state_changed': False,
1338 'msg': 'Repo `%s` not locked.' % repo.repo_name
1452 'msg': 'Repo `%s` not locked.' % repo.repo_name
1339 }
1453 }
1340 return _d
1454 return _d
1341 else:
1455 else:
1342 _user_id, _time, _reason = lockobj
1456 _user_id, _time, _reason = lockobj
1343 lock_user = get_user_or_error(userid)
1457 lock_user = get_user_or_error(userid)
1344 _d = {
1458 _d = {
1345 'repo': repo.repo_name,
1459 'repo': repo.repo_name,
1346 'locked': True,
1460 'locked': True,
1347 'locked_since': _time,
1461 'locked_since': _time,
1348 'locked_by': lock_user.username,
1462 'locked_by': lock_user.username,
1349 'lock_reason': _reason,
1463 'lock_reason': _reason,
1350 'lock_state_changed': False,
1464 'lock_state_changed': False,
1351 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1465 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1352 % (repo.repo_name, lock_user.username,
1466 % (repo.repo_name, lock_user.username,
1353 json.dumps(time_to_datetime(_time))))
1467 json.dumps(time_to_datetime(_time))))
1354 }
1468 }
1355 return _d
1469 return _d
1356
1470
1357 # force locked state through a flag
1471 # force locked state through a flag
1358 else:
1472 else:
1359 locked = str2bool(locked)
1473 locked = str2bool(locked)
1360 lock_reason = Repository.LOCK_API
1474 lock_reason = Repository.LOCK_API
1361 try:
1475 try:
1362 if locked:
1476 if locked:
1363 lock_time = time.time()
1477 lock_time = time.time()
1364 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1478 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1365 else:
1479 else:
1366 lock_time = None
1480 lock_time = None
1367 Repository.unlock(repo)
1481 Repository.unlock(repo)
1368 _d = {
1482 _d = {
1369 'repo': repo.repo_name,
1483 'repo': repo.repo_name,
1370 'locked': locked,
1484 'locked': locked,
1371 'locked_since': lock_time,
1485 'locked_since': lock_time,
1372 'locked_by': user.username,
1486 'locked_by': user.username,
1373 'lock_reason': lock_reason,
1487 'lock_reason': lock_reason,
1374 'lock_state_changed': True,
1488 'lock_state_changed': True,
1375 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1489 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1376 % (user.username, repo.repo_name, locked))
1490 % (user.username, repo.repo_name, locked))
1377 }
1491 }
1378 return _d
1492 return _d
1379 except Exception:
1493 except Exception:
1380 log.exception(
1494 log.exception(
1381 "Exception occurred while trying to lock repository")
1495 "Exception occurred while trying to lock repository")
1382 raise JSONRPCError(
1496 raise JSONRPCError(
1383 'Error occurred locking repository `%s`' % repo.repo_name
1497 'Error occurred locking repository `%s`' % repo.repo_name
1384 )
1498 )
1385
1499
1386
1500
1387 @jsonrpc_method()
1501 @jsonrpc_method()
1388 def comment_commit(
1502 def comment_commit(
1389 request, apiuser, repoid, commit_id, message, status=Optional(None),
1503 request, apiuser, repoid, commit_id, message, status=Optional(None),
1390 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1504 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1391 resolves_comment_id=Optional(None),
1505 resolves_comment_id=Optional(None),
1392 userid=Optional(OAttr('apiuser'))):
1506 userid=Optional(OAttr('apiuser'))):
1393 """
1507 """
1394 Set a commit comment, and optionally change the status of the commit.
1508 Set a commit comment, and optionally change the status of the commit.
1395
1509
1396 :param apiuser: This is filled automatically from the |authtoken|.
1510 :param apiuser: This is filled automatically from the |authtoken|.
1397 :type apiuser: AuthUser
1511 :type apiuser: AuthUser
1398 :param repoid: Set the repository name or repository ID.
1512 :param repoid: Set the repository name or repository ID.
1399 :type repoid: str or int
1513 :type repoid: str or int
1400 :param commit_id: Specify the commit_id for which to set a comment.
1514 :param commit_id: Specify the commit_id for which to set a comment.
1401 :type commit_id: str
1515 :type commit_id: str
1402 :param message: The comment text.
1516 :param message: The comment text.
1403 :type message: str
1517 :type message: str
1404 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1518 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1405 'approved', 'rejected', 'under_review'
1519 'approved', 'rejected', 'under_review'
1406 :type status: str
1520 :type status: str
1407 :param comment_type: Comment type, one of: 'note', 'todo'
1521 :param comment_type: Comment type, one of: 'note', 'todo'
1408 :type comment_type: Optional(str), default: 'note'
1522 :type comment_type: Optional(str), default: 'note'
1409 :param userid: Set the user name of the comment creator.
1523 :param userid: Set the user name of the comment creator.
1410 :type userid: Optional(str or int)
1524 :type userid: Optional(str or int)
1411
1525
1412 Example error output:
1526 Example error output:
1413
1527
1414 .. code-block:: bash
1528 .. code-block:: bash
1415
1529
1416 {
1530 {
1417 "id" : <id_given_in_input>,
1531 "id" : <id_given_in_input>,
1418 "result" : {
1532 "result" : {
1419 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1533 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1420 "status_change": null or <status>,
1534 "status_change": null or <status>,
1421 "success": true
1535 "success": true
1422 },
1536 },
1423 "error" : null
1537 "error" : null
1424 }
1538 }
1425
1539
1426 """
1540 """
1427 repo = get_repo_or_error(repoid)
1541 repo = get_repo_or_error(repoid)
1428 if not has_superadmin_permission(apiuser):
1542 if not has_superadmin_permission(apiuser):
1429 _perms = ('repository.read', 'repository.write', 'repository.admin')
1543 _perms = ('repository.read', 'repository.write', 'repository.admin')
1430 validate_repo_permissions(apiuser, repoid, repo, _perms)
1544 validate_repo_permissions(apiuser, repoid, repo, _perms)
1431
1545
1432 try:
1546 try:
1433 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1547 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1434 except Exception as e:
1548 except Exception as e:
1435 log.exception('Failed to fetch commit')
1549 log.exception('Failed to fetch commit')
1436 raise JSONRPCError(safe_str(e))
1550 raise JSONRPCError(safe_str(e))
1437
1551
1438 if isinstance(userid, Optional):
1552 if isinstance(userid, Optional):
1439 userid = apiuser.user_id
1553 userid = apiuser.user_id
1440
1554
1441 user = get_user_or_error(userid)
1555 user = get_user_or_error(userid)
1442 status = Optional.extract(status)
1556 status = Optional.extract(status)
1443 comment_type = Optional.extract(comment_type)
1557 comment_type = Optional.extract(comment_type)
1444 resolves_comment_id = Optional.extract(resolves_comment_id)
1558 resolves_comment_id = Optional.extract(resolves_comment_id)
1445
1559
1446 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1560 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1447 if status and status not in allowed_statuses:
1561 if status and status not in allowed_statuses:
1448 raise JSONRPCError('Bad status, must be on '
1562 raise JSONRPCError('Bad status, must be on '
1449 'of %s got %s' % (allowed_statuses, status,))
1563 'of %s got %s' % (allowed_statuses, status,))
1450
1564
1451 if resolves_comment_id:
1565 if resolves_comment_id:
1452 comment = ChangesetComment.get(resolves_comment_id)
1566 comment = ChangesetComment.get(resolves_comment_id)
1453 if not comment:
1567 if not comment:
1454 raise JSONRPCError(
1568 raise JSONRPCError(
1455 'Invalid resolves_comment_id `%s` for this commit.'
1569 'Invalid resolves_comment_id `%s` for this commit.'
1456 % resolves_comment_id)
1570 % resolves_comment_id)
1457 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1571 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1458 raise JSONRPCError(
1572 raise JSONRPCError(
1459 'Comment `%s` is wrong type for setting status to resolved.'
1573 'Comment `%s` is wrong type for setting status to resolved.'
1460 % resolves_comment_id)
1574 % resolves_comment_id)
1461
1575
1462 try:
1576 try:
1463 rc_config = SettingsModel().get_all_settings()
1577 rc_config = SettingsModel().get_all_settings()
1464 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1578 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1465 status_change_label = ChangesetStatus.get_status_lbl(status)
1579 status_change_label = ChangesetStatus.get_status_lbl(status)
1466 comment = CommentsModel().create(
1580 comment = CommentsModel().create(
1467 message, repo, user, commit_id=commit_id,
1581 message, repo, user, commit_id=commit_id,
1468 status_change=status_change_label,
1582 status_change=status_change_label,
1469 status_change_type=status,
1583 status_change_type=status,
1470 renderer=renderer,
1584 renderer=renderer,
1471 comment_type=comment_type,
1585 comment_type=comment_type,
1472 resolves_comment_id=resolves_comment_id,
1586 resolves_comment_id=resolves_comment_id,
1473 auth_user=apiuser
1587 auth_user=apiuser
1474 )
1588 )
1475 if status:
1589 if status:
1476 # also do a status change
1590 # also do a status change
1477 try:
1591 try:
1478 ChangesetStatusModel().set_status(
1592 ChangesetStatusModel().set_status(
1479 repo, status, user, comment, revision=commit_id,
1593 repo, status, user, comment, revision=commit_id,
1480 dont_allow_on_closed_pull_request=True
1594 dont_allow_on_closed_pull_request=True
1481 )
1595 )
1482 except StatusChangeOnClosedPullRequestError:
1596 except StatusChangeOnClosedPullRequestError:
1483 log.exception(
1597 log.exception(
1484 "Exception occurred while trying to change repo commit status")
1598 "Exception occurred while trying to change repo commit status")
1485 msg = ('Changing status on a changeset associated with '
1599 msg = ('Changing status on a changeset associated with '
1486 'a closed pull request is not allowed')
1600 'a closed pull request is not allowed')
1487 raise JSONRPCError(msg)
1601 raise JSONRPCError(msg)
1488
1602
1489 Session().commit()
1603 Session().commit()
1490 return {
1604 return {
1491 'msg': (
1605 'msg': (
1492 'Commented on commit `%s` for repository `%s`' % (
1606 'Commented on commit `%s` for repository `%s`' % (
1493 comment.revision, repo.repo_name)),
1607 comment.revision, repo.repo_name)),
1494 'status_change': status,
1608 'status_change': status,
1495 'success': True,
1609 'success': True,
1496 }
1610 }
1497 except JSONRPCError:
1611 except JSONRPCError:
1498 # catch any inside errors, and re-raise them to prevent from
1612 # catch any inside errors, and re-raise them to prevent from
1499 # below global catch to silence them
1613 # below global catch to silence them
1500 raise
1614 raise
1501 except Exception:
1615 except Exception:
1502 log.exception("Exception occurred while trying to comment on commit")
1616 log.exception("Exception occurred while trying to comment on commit")
1503 raise JSONRPCError(
1617 raise JSONRPCError(
1504 'failed to set comment on repository `%s`' % (repo.repo_name,)
1618 'failed to set comment on repository `%s`' % (repo.repo_name,)
1505 )
1619 )
1506
1620
1507
1621
1508 @jsonrpc_method()
1622 @jsonrpc_method()
1509 def get_repo_comments(request, apiuser, repoid,
1623 def get_repo_comments(request, apiuser, repoid,
1510 commit_id=Optional(None), comment_type=Optional(None),
1624 commit_id=Optional(None), comment_type=Optional(None),
1511 userid=Optional(None)):
1625 userid=Optional(None)):
1512 """
1626 """
1513 Get all comments for a repository
1627 Get all comments for a repository
1514
1628
1515 :param apiuser: This is filled automatically from the |authtoken|.
1629 :param apiuser: This is filled automatically from the |authtoken|.
1516 :type apiuser: AuthUser
1630 :type apiuser: AuthUser
1517 :param repoid: Set the repository name or repository ID.
1631 :param repoid: Set the repository name or repository ID.
1518 :type repoid: str or int
1632 :type repoid: str or int
1519 :param commit_id: Optionally filter the comments by the commit_id
1633 :param commit_id: Optionally filter the comments by the commit_id
1520 :type commit_id: Optional(str), default: None
1634 :type commit_id: Optional(str), default: None
1521 :param comment_type: Optionally filter the comments by the comment_type
1635 :param comment_type: Optionally filter the comments by the comment_type
1522 one of: 'note', 'todo'
1636 one of: 'note', 'todo'
1523 :type comment_type: Optional(str), default: None
1637 :type comment_type: Optional(str), default: None
1524 :param userid: Optionally filter the comments by the author of comment
1638 :param userid: Optionally filter the comments by the author of comment
1525 :type userid: Optional(str or int), Default: None
1639 :type userid: Optional(str or int), Default: None
1526
1640
1527 Example error output:
1641 Example error output:
1528
1642
1529 .. code-block:: bash
1643 .. code-block:: bash
1530
1644
1531 {
1645 {
1532 "id" : <id_given_in_input>,
1646 "id" : <id_given_in_input>,
1533 "result" : [
1647 "result" : [
1534 {
1648 {
1535 "comment_author": <USER_DETAILS>,
1649 "comment_author": <USER_DETAILS>,
1536 "comment_created_on": "2017-02-01T14:38:16.309",
1650 "comment_created_on": "2017-02-01T14:38:16.309",
1537 "comment_f_path": "file.txt",
1651 "comment_f_path": "file.txt",
1538 "comment_id": 282,
1652 "comment_id": 282,
1539 "comment_lineno": "n1",
1653 "comment_lineno": "n1",
1540 "comment_resolved_by": null,
1654 "comment_resolved_by": null,
1541 "comment_status": [],
1655 "comment_status": [],
1542 "comment_text": "This file needs a header",
1656 "comment_text": "This file needs a header",
1543 "comment_type": "todo"
1657 "comment_type": "todo"
1544 }
1658 }
1545 ],
1659 ],
1546 "error" : null
1660 "error" : null
1547 }
1661 }
1548
1662
1549 """
1663 """
1550 repo = get_repo_or_error(repoid)
1664 repo = get_repo_or_error(repoid)
1551 if not has_superadmin_permission(apiuser):
1665 if not has_superadmin_permission(apiuser):
1552 _perms = ('repository.read', 'repository.write', 'repository.admin')
1666 _perms = ('repository.read', 'repository.write', 'repository.admin')
1553 validate_repo_permissions(apiuser, repoid, repo, _perms)
1667 validate_repo_permissions(apiuser, repoid, repo, _perms)
1554
1668
1555 commit_id = Optional.extract(commit_id)
1669 commit_id = Optional.extract(commit_id)
1556
1670
1557 userid = Optional.extract(userid)
1671 userid = Optional.extract(userid)
1558 if userid:
1672 if userid:
1559 user = get_user_or_error(userid)
1673 user = get_user_or_error(userid)
1560 else:
1674 else:
1561 user = None
1675 user = None
1562
1676
1563 comment_type = Optional.extract(comment_type)
1677 comment_type = Optional.extract(comment_type)
1564 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1678 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1565 raise JSONRPCError(
1679 raise JSONRPCError(
1566 'comment_type must be one of `{}` got {}'.format(
1680 'comment_type must be one of `{}` got {}'.format(
1567 ChangesetComment.COMMENT_TYPES, comment_type)
1681 ChangesetComment.COMMENT_TYPES, comment_type)
1568 )
1682 )
1569
1683
1570 comments = CommentsModel().get_repository_comments(
1684 comments = CommentsModel().get_repository_comments(
1571 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1685 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1572 return comments
1686 return comments
1573
1687
1574
1688
1575 @jsonrpc_method()
1689 @jsonrpc_method()
1576 def grant_user_permission(request, apiuser, repoid, userid, perm):
1690 def grant_user_permission(request, apiuser, repoid, userid, perm):
1577 """
1691 """
1578 Grant permissions for the specified user on the given repository,
1692 Grant permissions for the specified user on the given repository,
1579 or update existing permissions if found.
1693 or update existing permissions if found.
1580
1694
1581 This command can only be run using an |authtoken| with admin
1695 This command can only be run using an |authtoken| with admin
1582 permissions on the |repo|.
1696 permissions on the |repo|.
1583
1697
1584 :param apiuser: This is filled automatically from the |authtoken|.
1698 :param apiuser: This is filled automatically from the |authtoken|.
1585 :type apiuser: AuthUser
1699 :type apiuser: AuthUser
1586 :param repoid: Set the repository name or repository ID.
1700 :param repoid: Set the repository name or repository ID.
1587 :type repoid: str or int
1701 :type repoid: str or int
1588 :param userid: Set the user name.
1702 :param userid: Set the user name.
1589 :type userid: str
1703 :type userid: str
1590 :param perm: Set the user permissions, using the following format
1704 :param perm: Set the user permissions, using the following format
1591 ``(repository.(none|read|write|admin))``
1705 ``(repository.(none|read|write|admin))``
1592 :type perm: str
1706 :type perm: str
1593
1707
1594 Example output:
1708 Example output:
1595
1709
1596 .. code-block:: bash
1710 .. code-block:: bash
1597
1711
1598 id : <id_given_in_input>
1712 id : <id_given_in_input>
1599 result: {
1713 result: {
1600 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1714 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1601 "success": true
1715 "success": true
1602 }
1716 }
1603 error: null
1717 error: null
1604 """
1718 """
1605
1719
1606 repo = get_repo_or_error(repoid)
1720 repo = get_repo_or_error(repoid)
1607 user = get_user_or_error(userid)
1721 user = get_user_or_error(userid)
1608 perm = get_perm_or_error(perm)
1722 perm = get_perm_or_error(perm)
1609 if not has_superadmin_permission(apiuser):
1723 if not has_superadmin_permission(apiuser):
1610 _perms = ('repository.admin',)
1724 _perms = ('repository.admin',)
1611 validate_repo_permissions(apiuser, repoid, repo, _perms)
1725 validate_repo_permissions(apiuser, repoid, repo, _perms)
1612
1726
1613 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1727 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1614 try:
1728 try:
1615 changes = RepoModel().update_permissions(
1729 changes = RepoModel().update_permissions(
1616 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1730 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1617
1731
1618 action_data = {
1732 action_data = {
1619 'added': changes['added'],
1733 'added': changes['added'],
1620 'updated': changes['updated'],
1734 'updated': changes['updated'],
1621 'deleted': changes['deleted'],
1735 'deleted': changes['deleted'],
1622 }
1736 }
1623 audit_logger.store_api(
1737 audit_logger.store_api(
1624 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1738 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1625
1739
1626 Session().commit()
1740 Session().commit()
1627 return {
1741 return {
1628 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1742 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1629 perm.permission_name, user.username, repo.repo_name
1743 perm.permission_name, user.username, repo.repo_name
1630 ),
1744 ),
1631 'success': True
1745 'success': True
1632 }
1746 }
1633 except Exception:
1747 except Exception:
1634 log.exception("Exception occurred while trying edit permissions for repo")
1748 log.exception("Exception occurred while trying edit permissions for repo")
1635 raise JSONRPCError(
1749 raise JSONRPCError(
1636 'failed to edit permission for user: `%s` in repo: `%s`' % (
1750 'failed to edit permission for user: `%s` in repo: `%s`' % (
1637 userid, repoid
1751 userid, repoid
1638 )
1752 )
1639 )
1753 )
1640
1754
1641
1755
1642 @jsonrpc_method()
1756 @jsonrpc_method()
1643 def revoke_user_permission(request, apiuser, repoid, userid):
1757 def revoke_user_permission(request, apiuser, repoid, userid):
1644 """
1758 """
1645 Revoke permission for a user on the specified repository.
1759 Revoke permission for a user on the specified repository.
1646
1760
1647 This command can only be run using an |authtoken| with admin
1761 This command can only be run using an |authtoken| with admin
1648 permissions on the |repo|.
1762 permissions on the |repo|.
1649
1763
1650 :param apiuser: This is filled automatically from the |authtoken|.
1764 :param apiuser: This is filled automatically from the |authtoken|.
1651 :type apiuser: AuthUser
1765 :type apiuser: AuthUser
1652 :param repoid: Set the repository name or repository ID.
1766 :param repoid: Set the repository name or repository ID.
1653 :type repoid: str or int
1767 :type repoid: str or int
1654 :param userid: Set the user name of revoked user.
1768 :param userid: Set the user name of revoked user.
1655 :type userid: str or int
1769 :type userid: str or int
1656
1770
1657 Example error output:
1771 Example error output:
1658
1772
1659 .. code-block:: bash
1773 .. code-block:: bash
1660
1774
1661 id : <id_given_in_input>
1775 id : <id_given_in_input>
1662 result: {
1776 result: {
1663 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1777 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1664 "success": true
1778 "success": true
1665 }
1779 }
1666 error: null
1780 error: null
1667 """
1781 """
1668
1782
1669 repo = get_repo_or_error(repoid)
1783 repo = get_repo_or_error(repoid)
1670 user = get_user_or_error(userid)
1784 user = get_user_or_error(userid)
1671 if not has_superadmin_permission(apiuser):
1785 if not has_superadmin_permission(apiuser):
1672 _perms = ('repository.admin',)
1786 _perms = ('repository.admin',)
1673 validate_repo_permissions(apiuser, repoid, repo, _perms)
1787 validate_repo_permissions(apiuser, repoid, repo, _perms)
1674
1788
1675 perm_deletions = [[user.user_id, None, "user"]]
1789 perm_deletions = [[user.user_id, None, "user"]]
1676 try:
1790 try:
1677 changes = RepoModel().update_permissions(
1791 changes = RepoModel().update_permissions(
1678 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1792 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1679
1793
1680 action_data = {
1794 action_data = {
1681 'added': changes['added'],
1795 'added': changes['added'],
1682 'updated': changes['updated'],
1796 'updated': changes['updated'],
1683 'deleted': changes['deleted'],
1797 'deleted': changes['deleted'],
1684 }
1798 }
1685 audit_logger.store_api(
1799 audit_logger.store_api(
1686 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1800 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1687
1801
1688 Session().commit()
1802 Session().commit()
1689 return {
1803 return {
1690 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1804 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1691 user.username, repo.repo_name
1805 user.username, repo.repo_name
1692 ),
1806 ),
1693 'success': True
1807 'success': True
1694 }
1808 }
1695 except Exception:
1809 except Exception:
1696 log.exception("Exception occurred while trying revoke permissions to repo")
1810 log.exception("Exception occurred while trying revoke permissions to repo")
1697 raise JSONRPCError(
1811 raise JSONRPCError(
1698 'failed to edit permission for user: `%s` in repo: `%s`' % (
1812 'failed to edit permission for user: `%s` in repo: `%s`' % (
1699 userid, repoid
1813 userid, repoid
1700 )
1814 )
1701 )
1815 )
1702
1816
1703
1817
1704 @jsonrpc_method()
1818 @jsonrpc_method()
1705 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1819 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1706 """
1820 """
1707 Grant permission for a user group on the specified repository,
1821 Grant permission for a user group on the specified repository,
1708 or update existing permissions.
1822 or update existing permissions.
1709
1823
1710 This command can only be run using an |authtoken| with admin
1824 This command can only be run using an |authtoken| with admin
1711 permissions on the |repo|.
1825 permissions on the |repo|.
1712
1826
1713 :param apiuser: This is filled automatically from the |authtoken|.
1827 :param apiuser: This is filled automatically from the |authtoken|.
1714 :type apiuser: AuthUser
1828 :type apiuser: AuthUser
1715 :param repoid: Set the repository name or repository ID.
1829 :param repoid: Set the repository name or repository ID.
1716 :type repoid: str or int
1830 :type repoid: str or int
1717 :param usergroupid: Specify the ID of the user group.
1831 :param usergroupid: Specify the ID of the user group.
1718 :type usergroupid: str or int
1832 :type usergroupid: str or int
1719 :param perm: Set the user group permissions using the following
1833 :param perm: Set the user group permissions using the following
1720 format: (repository.(none|read|write|admin))
1834 format: (repository.(none|read|write|admin))
1721 :type perm: str
1835 :type perm: str
1722
1836
1723 Example output:
1837 Example output:
1724
1838
1725 .. code-block:: bash
1839 .. code-block:: bash
1726
1840
1727 id : <id_given_in_input>
1841 id : <id_given_in_input>
1728 result : {
1842 result : {
1729 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1843 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1730 "success": true
1844 "success": true
1731
1845
1732 }
1846 }
1733 error : null
1847 error : null
1734
1848
1735 Example error output:
1849 Example error output:
1736
1850
1737 .. code-block:: bash
1851 .. code-block:: bash
1738
1852
1739 id : <id_given_in_input>
1853 id : <id_given_in_input>
1740 result : null
1854 result : null
1741 error : {
1855 error : {
1742 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1856 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1743 }
1857 }
1744
1858
1745 """
1859 """
1746
1860
1747 repo = get_repo_or_error(repoid)
1861 repo = get_repo_or_error(repoid)
1748 perm = get_perm_or_error(perm)
1862 perm = get_perm_or_error(perm)
1749 if not has_superadmin_permission(apiuser):
1863 if not has_superadmin_permission(apiuser):
1750 _perms = ('repository.admin',)
1864 _perms = ('repository.admin',)
1751 validate_repo_permissions(apiuser, repoid, repo, _perms)
1865 validate_repo_permissions(apiuser, repoid, repo, _perms)
1752
1866
1753 user_group = get_user_group_or_error(usergroupid)
1867 user_group = get_user_group_or_error(usergroupid)
1754 if not has_superadmin_permission(apiuser):
1868 if not has_superadmin_permission(apiuser):
1755 # check if we have at least read permission for this user group !
1869 # check if we have at least read permission for this user group !
1756 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1870 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1757 if not HasUserGroupPermissionAnyApi(*_perms)(
1871 if not HasUserGroupPermissionAnyApi(*_perms)(
1758 user=apiuser, user_group_name=user_group.users_group_name):
1872 user=apiuser, user_group_name=user_group.users_group_name):
1759 raise JSONRPCError(
1873 raise JSONRPCError(
1760 'user group `%s` does not exist' % (usergroupid,))
1874 'user group `%s` does not exist' % (usergroupid,))
1761
1875
1762 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1876 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1763 try:
1877 try:
1764 changes = RepoModel().update_permissions(
1878 changes = RepoModel().update_permissions(
1765 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1879 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1766 action_data = {
1880 action_data = {
1767 'added': changes['added'],
1881 'added': changes['added'],
1768 'updated': changes['updated'],
1882 'updated': changes['updated'],
1769 'deleted': changes['deleted'],
1883 'deleted': changes['deleted'],
1770 }
1884 }
1771 audit_logger.store_api(
1885 audit_logger.store_api(
1772 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1886 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1773
1887
1774 Session().commit()
1888 Session().commit()
1775 return {
1889 return {
1776 'msg': 'Granted perm: `%s` for user group: `%s` in '
1890 'msg': 'Granted perm: `%s` for user group: `%s` in '
1777 'repo: `%s`' % (
1891 'repo: `%s`' % (
1778 perm.permission_name, user_group.users_group_name,
1892 perm.permission_name, user_group.users_group_name,
1779 repo.repo_name
1893 repo.repo_name
1780 ),
1894 ),
1781 'success': True
1895 'success': True
1782 }
1896 }
1783 except Exception:
1897 except Exception:
1784 log.exception(
1898 log.exception(
1785 "Exception occurred while trying change permission on repo")
1899 "Exception occurred while trying change permission on repo")
1786 raise JSONRPCError(
1900 raise JSONRPCError(
1787 'failed to edit permission for user group: `%s` in '
1901 'failed to edit permission for user group: `%s` in '
1788 'repo: `%s`' % (
1902 'repo: `%s`' % (
1789 usergroupid, repo.repo_name
1903 usergroupid, repo.repo_name
1790 )
1904 )
1791 )
1905 )
1792
1906
1793
1907
1794 @jsonrpc_method()
1908 @jsonrpc_method()
1795 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1909 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1796 """
1910 """
1797 Revoke the permissions of a user group on a given repository.
1911 Revoke the permissions of a user group on a given repository.
1798
1912
1799 This command can only be run using an |authtoken| with admin
1913 This command can only be run using an |authtoken| with admin
1800 permissions on the |repo|.
1914 permissions on the |repo|.
1801
1915
1802 :param apiuser: This is filled automatically from the |authtoken|.
1916 :param apiuser: This is filled automatically from the |authtoken|.
1803 :type apiuser: AuthUser
1917 :type apiuser: AuthUser
1804 :param repoid: Set the repository name or repository ID.
1918 :param repoid: Set the repository name or repository ID.
1805 :type repoid: str or int
1919 :type repoid: str or int
1806 :param usergroupid: Specify the user group ID.
1920 :param usergroupid: Specify the user group ID.
1807 :type usergroupid: str or int
1921 :type usergroupid: str or int
1808
1922
1809 Example output:
1923 Example output:
1810
1924
1811 .. code-block:: bash
1925 .. code-block:: bash
1812
1926
1813 id : <id_given_in_input>
1927 id : <id_given_in_input>
1814 result: {
1928 result: {
1815 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1929 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1816 "success": true
1930 "success": true
1817 }
1931 }
1818 error: null
1932 error: null
1819 """
1933 """
1820
1934
1821 repo = get_repo_or_error(repoid)
1935 repo = get_repo_or_error(repoid)
1822 if not has_superadmin_permission(apiuser):
1936 if not has_superadmin_permission(apiuser):
1823 _perms = ('repository.admin',)
1937 _perms = ('repository.admin',)
1824 validate_repo_permissions(apiuser, repoid, repo, _perms)
1938 validate_repo_permissions(apiuser, repoid, repo, _perms)
1825
1939
1826 user_group = get_user_group_or_error(usergroupid)
1940 user_group = get_user_group_or_error(usergroupid)
1827 if not has_superadmin_permission(apiuser):
1941 if not has_superadmin_permission(apiuser):
1828 # check if we have at least read permission for this user group !
1942 # check if we have at least read permission for this user group !
1829 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1943 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1830 if not HasUserGroupPermissionAnyApi(*_perms)(
1944 if not HasUserGroupPermissionAnyApi(*_perms)(
1831 user=apiuser, user_group_name=user_group.users_group_name):
1945 user=apiuser, user_group_name=user_group.users_group_name):
1832 raise JSONRPCError(
1946 raise JSONRPCError(
1833 'user group `%s` does not exist' % (usergroupid,))
1947 'user group `%s` does not exist' % (usergroupid,))
1834
1948
1835 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1949 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
1836 try:
1950 try:
1837 changes = RepoModel().update_permissions(
1951 changes = RepoModel().update_permissions(
1838 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1952 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
1839 action_data = {
1953 action_data = {
1840 'added': changes['added'],
1954 'added': changes['added'],
1841 'updated': changes['updated'],
1955 'updated': changes['updated'],
1842 'deleted': changes['deleted'],
1956 'deleted': changes['deleted'],
1843 }
1957 }
1844 audit_logger.store_api(
1958 audit_logger.store_api(
1845 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1959 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1846
1960
1847 Session().commit()
1961 Session().commit()
1848 return {
1962 return {
1849 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1963 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1850 user_group.users_group_name, repo.repo_name
1964 user_group.users_group_name, repo.repo_name
1851 ),
1965 ),
1852 'success': True
1966 'success': True
1853 }
1967 }
1854 except Exception:
1968 except Exception:
1855 log.exception("Exception occurred while trying revoke "
1969 log.exception("Exception occurred while trying revoke "
1856 "user group permission on repo")
1970 "user group permission on repo")
1857 raise JSONRPCError(
1971 raise JSONRPCError(
1858 'failed to edit permission for user group: `%s` in '
1972 'failed to edit permission for user group: `%s` in '
1859 'repo: `%s`' % (
1973 'repo: `%s`' % (
1860 user_group.users_group_name, repo.repo_name
1974 user_group.users_group_name, repo.repo_name
1861 )
1975 )
1862 )
1976 )
1863
1977
1864
1978
1865 @jsonrpc_method()
1979 @jsonrpc_method()
1866 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1980 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1867 """
1981 """
1868 Triggers a pull on the given repository from a remote location. You
1982 Triggers a pull on the given repository from a remote location. You
1869 can use this to keep remote repositories up-to-date.
1983 can use this to keep remote repositories up-to-date.
1870
1984
1871 This command can only be run using an |authtoken| with admin
1985 This command can only be run using an |authtoken| with admin
1872 rights to the specified repository. For more information,
1986 rights to the specified repository. For more information,
1873 see :ref:`config-token-ref`.
1987 see :ref:`config-token-ref`.
1874
1988
1875 This command takes the following options:
1989 This command takes the following options:
1876
1990
1877 :param apiuser: This is filled automatically from the |authtoken|.
1991 :param apiuser: This is filled automatically from the |authtoken|.
1878 :type apiuser: AuthUser
1992 :type apiuser: AuthUser
1879 :param repoid: The repository name or repository ID.
1993 :param repoid: The repository name or repository ID.
1880 :type repoid: str or int
1994 :type repoid: str or int
1881 :param remote_uri: Optional remote URI to pass in for pull
1995 :param remote_uri: Optional remote URI to pass in for pull
1882 :type remote_uri: str
1996 :type remote_uri: str
1883
1997
1884 Example output:
1998 Example output:
1885
1999
1886 .. code-block:: bash
2000 .. code-block:: bash
1887
2001
1888 id : <id_given_in_input>
2002 id : <id_given_in_input>
1889 result : {
2003 result : {
1890 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2004 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1891 "repository": "<repository name>"
2005 "repository": "<repository name>"
1892 }
2006 }
1893 error : null
2007 error : null
1894
2008
1895 Example error output:
2009 Example error output:
1896
2010
1897 .. code-block:: bash
2011 .. code-block:: bash
1898
2012
1899 id : <id_given_in_input>
2013 id : <id_given_in_input>
1900 result : null
2014 result : null
1901 error : {
2015 error : {
1902 "Unable to push changes from `<remote_url>`"
2016 "Unable to push changes from `<remote_url>`"
1903 }
2017 }
1904
2018
1905 """
2019 """
1906
2020
1907 repo = get_repo_or_error(repoid)
2021 repo = get_repo_or_error(repoid)
1908 remote_uri = Optional.extract(remote_uri)
2022 remote_uri = Optional.extract(remote_uri)
1909 remote_uri_display = remote_uri or repo.clone_uri_hidden
2023 remote_uri_display = remote_uri or repo.clone_uri_hidden
1910 if not has_superadmin_permission(apiuser):
2024 if not has_superadmin_permission(apiuser):
1911 _perms = ('repository.admin',)
2025 _perms = ('repository.admin',)
1912 validate_repo_permissions(apiuser, repoid, repo, _perms)
2026 validate_repo_permissions(apiuser, repoid, repo, _perms)
1913
2027
1914 try:
2028 try:
1915 ScmModel().pull_changes(
2029 ScmModel().pull_changes(
1916 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2030 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1917 return {
2031 return {
1918 'msg': 'Pulled from url `%s` on repo `%s`' % (
2032 'msg': 'Pulled from url `%s` on repo `%s`' % (
1919 remote_uri_display, repo.repo_name),
2033 remote_uri_display, repo.repo_name),
1920 'repository': repo.repo_name
2034 'repository': repo.repo_name
1921 }
2035 }
1922 except Exception:
2036 except Exception:
1923 log.exception("Exception occurred while trying to "
2037 log.exception("Exception occurred while trying to "
1924 "pull changes from remote location")
2038 "pull changes from remote location")
1925 raise JSONRPCError(
2039 raise JSONRPCError(
1926 'Unable to pull changes from `%s`' % remote_uri_display
2040 'Unable to pull changes from `%s`' % remote_uri_display
1927 )
2041 )
1928
2042
1929
2043
1930 @jsonrpc_method()
2044 @jsonrpc_method()
1931 def strip(request, apiuser, repoid, revision, branch):
2045 def strip(request, apiuser, repoid, revision, branch):
1932 """
2046 """
1933 Strips the given revision from the specified repository.
2047 Strips the given revision from the specified repository.
1934
2048
1935 * This will remove the revision and all of its decendants.
2049 * This will remove the revision and all of its decendants.
1936
2050
1937 This command can only be run using an |authtoken| with admin rights to
2051 This command can only be run using an |authtoken| with admin rights to
1938 the specified repository.
2052 the specified repository.
1939
2053
1940 This command takes the following options:
2054 This command takes the following options:
1941
2055
1942 :param apiuser: This is filled automatically from the |authtoken|.
2056 :param apiuser: This is filled automatically from the |authtoken|.
1943 :type apiuser: AuthUser
2057 :type apiuser: AuthUser
1944 :param repoid: The repository name or repository ID.
2058 :param repoid: The repository name or repository ID.
1945 :type repoid: str or int
2059 :type repoid: str or int
1946 :param revision: The revision you wish to strip.
2060 :param revision: The revision you wish to strip.
1947 :type revision: str
2061 :type revision: str
1948 :param branch: The branch from which to strip the revision.
2062 :param branch: The branch from which to strip the revision.
1949 :type branch: str
2063 :type branch: str
1950
2064
1951 Example output:
2065 Example output:
1952
2066
1953 .. code-block:: bash
2067 .. code-block:: bash
1954
2068
1955 id : <id_given_in_input>
2069 id : <id_given_in_input>
1956 result : {
2070 result : {
1957 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2071 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1958 "repository": "<repository name>"
2072 "repository": "<repository name>"
1959 }
2073 }
1960 error : null
2074 error : null
1961
2075
1962 Example error output:
2076 Example error output:
1963
2077
1964 .. code-block:: bash
2078 .. code-block:: bash
1965
2079
1966 id : <id_given_in_input>
2080 id : <id_given_in_input>
1967 result : null
2081 result : null
1968 error : {
2082 error : {
1969 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2083 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1970 }
2084 }
1971
2085
1972 """
2086 """
1973
2087
1974 repo = get_repo_or_error(repoid)
2088 repo = get_repo_or_error(repoid)
1975 if not has_superadmin_permission(apiuser):
2089 if not has_superadmin_permission(apiuser):
1976 _perms = ('repository.admin',)
2090 _perms = ('repository.admin',)
1977 validate_repo_permissions(apiuser, repoid, repo, _perms)
2091 validate_repo_permissions(apiuser, repoid, repo, _perms)
1978
2092
1979 try:
2093 try:
1980 ScmModel().strip(repo, revision, branch)
2094 ScmModel().strip(repo, revision, branch)
1981 audit_logger.store_api(
2095 audit_logger.store_api(
1982 'repo.commit.strip', action_data={'commit_id': revision},
2096 'repo.commit.strip', action_data={'commit_id': revision},
1983 repo=repo,
2097 repo=repo,
1984 user=apiuser, commit=True)
2098 user=apiuser, commit=True)
1985
2099
1986 return {
2100 return {
1987 'msg': 'Stripped commit %s from repo `%s`' % (
2101 'msg': 'Stripped commit %s from repo `%s`' % (
1988 revision, repo.repo_name),
2102 revision, repo.repo_name),
1989 'repository': repo.repo_name
2103 'repository': repo.repo_name
1990 }
2104 }
1991 except Exception:
2105 except Exception:
1992 log.exception("Exception while trying to strip")
2106 log.exception("Exception while trying to strip")
1993 raise JSONRPCError(
2107 raise JSONRPCError(
1994 'Unable to strip commit %s from repo `%s`' % (
2108 'Unable to strip commit %s from repo `%s`' % (
1995 revision, repo.repo_name)
2109 revision, repo.repo_name)
1996 )
2110 )
1997
2111
1998
2112
1999 @jsonrpc_method()
2113 @jsonrpc_method()
2000 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2114 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2001 """
2115 """
2002 Returns all settings for a repository. If key is given it only returns the
2116 Returns all settings for a repository. If key is given it only returns the
2003 setting identified by the key or null.
2117 setting identified by the key or null.
2004
2118
2005 :param apiuser: This is filled automatically from the |authtoken|.
2119 :param apiuser: This is filled automatically from the |authtoken|.
2006 :type apiuser: AuthUser
2120 :type apiuser: AuthUser
2007 :param repoid: The repository name or repository id.
2121 :param repoid: The repository name or repository id.
2008 :type repoid: str or int
2122 :type repoid: str or int
2009 :param key: Key of the setting to return.
2123 :param key: Key of the setting to return.
2010 :type: key: Optional(str)
2124 :type: key: Optional(str)
2011
2125
2012 Example output:
2126 Example output:
2013
2127
2014 .. code-block:: bash
2128 .. code-block:: bash
2015
2129
2016 {
2130 {
2017 "error": null,
2131 "error": null,
2018 "id": 237,
2132 "id": 237,
2019 "result": {
2133 "result": {
2020 "extensions_largefiles": true,
2134 "extensions_largefiles": true,
2021 "extensions_evolve": true,
2135 "extensions_evolve": true,
2022 "hooks_changegroup_push_logger": true,
2136 "hooks_changegroup_push_logger": true,
2023 "hooks_changegroup_repo_size": false,
2137 "hooks_changegroup_repo_size": false,
2024 "hooks_outgoing_pull_logger": true,
2138 "hooks_outgoing_pull_logger": true,
2025 "phases_publish": "True",
2139 "phases_publish": "True",
2026 "rhodecode_hg_use_rebase_for_merging": true,
2140 "rhodecode_hg_use_rebase_for_merging": true,
2027 "rhodecode_pr_merge_enabled": true,
2141 "rhodecode_pr_merge_enabled": true,
2028 "rhodecode_use_outdated_comments": true
2142 "rhodecode_use_outdated_comments": true
2029 }
2143 }
2030 }
2144 }
2031 """
2145 """
2032
2146
2033 # Restrict access to this api method to admins only.
2147 # Restrict access to this api method to admins only.
2034 if not has_superadmin_permission(apiuser):
2148 if not has_superadmin_permission(apiuser):
2035 raise JSONRPCForbidden()
2149 raise JSONRPCForbidden()
2036
2150
2037 try:
2151 try:
2038 repo = get_repo_or_error(repoid)
2152 repo = get_repo_or_error(repoid)
2039 settings_model = VcsSettingsModel(repo=repo)
2153 settings_model = VcsSettingsModel(repo=repo)
2040 settings = settings_model.get_global_settings()
2154 settings = settings_model.get_global_settings()
2041 settings.update(settings_model.get_repo_settings())
2155 settings.update(settings_model.get_repo_settings())
2042
2156
2043 # If only a single setting is requested fetch it from all settings.
2157 # If only a single setting is requested fetch it from all settings.
2044 key = Optional.extract(key)
2158 key = Optional.extract(key)
2045 if key is not None:
2159 if key is not None:
2046 settings = settings.get(key, None)
2160 settings = settings.get(key, None)
2047 except Exception:
2161 except Exception:
2048 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2162 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2049 log.exception(msg)
2163 log.exception(msg)
2050 raise JSONRPCError(msg)
2164 raise JSONRPCError(msg)
2051
2165
2052 return settings
2166 return settings
2053
2167
2054
2168
2055 @jsonrpc_method()
2169 @jsonrpc_method()
2056 def set_repo_settings(request, apiuser, repoid, settings):
2170 def set_repo_settings(request, apiuser, repoid, settings):
2057 """
2171 """
2058 Update repository settings. Returns true on success.
2172 Update repository settings. Returns true on success.
2059
2173
2060 :param apiuser: This is filled automatically from the |authtoken|.
2174 :param apiuser: This is filled automatically from the |authtoken|.
2061 :type apiuser: AuthUser
2175 :type apiuser: AuthUser
2062 :param repoid: The repository name or repository id.
2176 :param repoid: The repository name or repository id.
2063 :type repoid: str or int
2177 :type repoid: str or int
2064 :param settings: The new settings for the repository.
2178 :param settings: The new settings for the repository.
2065 :type: settings: dict
2179 :type: settings: dict
2066
2180
2067 Example output:
2181 Example output:
2068
2182
2069 .. code-block:: bash
2183 .. code-block:: bash
2070
2184
2071 {
2185 {
2072 "error": null,
2186 "error": null,
2073 "id": 237,
2187 "id": 237,
2074 "result": true
2188 "result": true
2075 }
2189 }
2076 """
2190 """
2077 # Restrict access to this api method to admins only.
2191 # Restrict access to this api method to admins only.
2078 if not has_superadmin_permission(apiuser):
2192 if not has_superadmin_permission(apiuser):
2079 raise JSONRPCForbidden()
2193 raise JSONRPCForbidden()
2080
2194
2081 if type(settings) is not dict:
2195 if type(settings) is not dict:
2082 raise JSONRPCError('Settings have to be a JSON Object.')
2196 raise JSONRPCError('Settings have to be a JSON Object.')
2083
2197
2084 try:
2198 try:
2085 settings_model = VcsSettingsModel(repo=repoid)
2199 settings_model = VcsSettingsModel(repo=repoid)
2086
2200
2087 # Merge global, repo and incoming settings.
2201 # Merge global, repo and incoming settings.
2088 new_settings = settings_model.get_global_settings()
2202 new_settings = settings_model.get_global_settings()
2089 new_settings.update(settings_model.get_repo_settings())
2203 new_settings.update(settings_model.get_repo_settings())
2090 new_settings.update(settings)
2204 new_settings.update(settings)
2091
2205
2092 # Update the settings.
2206 # Update the settings.
2093 inherit_global_settings = new_settings.get(
2207 inherit_global_settings = new_settings.get(
2094 'inherit_global_settings', False)
2208 'inherit_global_settings', False)
2095 settings_model.create_or_update_repo_settings(
2209 settings_model.create_or_update_repo_settings(
2096 new_settings, inherit_global_settings=inherit_global_settings)
2210 new_settings, inherit_global_settings=inherit_global_settings)
2097 Session().commit()
2211 Session().commit()
2098 except Exception:
2212 except Exception:
2099 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2213 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2100 log.exception(msg)
2214 log.exception(msg)
2101 raise JSONRPCError(msg)
2215 raise JSONRPCError(msg)
2102
2216
2103 # Indicate success.
2217 # Indicate success.
2104 return True
2218 return True
2105
2219
2106
2220
2107 @jsonrpc_method()
2221 @jsonrpc_method()
2108 def maintenance(request, apiuser, repoid):
2222 def maintenance(request, apiuser, repoid):
2109 """
2223 """
2110 Triggers a maintenance on the given repository.
2224 Triggers a maintenance on the given repository.
2111
2225
2112 This command can only be run using an |authtoken| with admin
2226 This command can only be run using an |authtoken| with admin
2113 rights to the specified repository. For more information,
2227 rights to the specified repository. For more information,
2114 see :ref:`config-token-ref`.
2228 see :ref:`config-token-ref`.
2115
2229
2116 This command takes the following options:
2230 This command takes the following options:
2117
2231
2118 :param apiuser: This is filled automatically from the |authtoken|.
2232 :param apiuser: This is filled automatically from the |authtoken|.
2119 :type apiuser: AuthUser
2233 :type apiuser: AuthUser
2120 :param repoid: The repository name or repository ID.
2234 :param repoid: The repository name or repository ID.
2121 :type repoid: str or int
2235 :type repoid: str or int
2122
2236
2123 Example output:
2237 Example output:
2124
2238
2125 .. code-block:: bash
2239 .. code-block:: bash
2126
2240
2127 id : <id_given_in_input>
2241 id : <id_given_in_input>
2128 result : {
2242 result : {
2129 "msg": "executed maintenance command",
2243 "msg": "executed maintenance command",
2130 "executed_actions": [
2244 "executed_actions": [
2131 <action_message>, <action_message2>...
2245 <action_message>, <action_message2>...
2132 ],
2246 ],
2133 "repository": "<repository name>"
2247 "repository": "<repository name>"
2134 }
2248 }
2135 error : null
2249 error : null
2136
2250
2137 Example error output:
2251 Example error output:
2138
2252
2139 .. code-block:: bash
2253 .. code-block:: bash
2140
2254
2141 id : <id_given_in_input>
2255 id : <id_given_in_input>
2142 result : null
2256 result : null
2143 error : {
2257 error : {
2144 "Unable to execute maintenance on `<reponame>`"
2258 "Unable to execute maintenance on `<reponame>`"
2145 }
2259 }
2146
2260
2147 """
2261 """
2148
2262
2149 repo = get_repo_or_error(repoid)
2263 repo = get_repo_or_error(repoid)
2150 if not has_superadmin_permission(apiuser):
2264 if not has_superadmin_permission(apiuser):
2151 _perms = ('repository.admin',)
2265 _perms = ('repository.admin',)
2152 validate_repo_permissions(apiuser, repoid, repo, _perms)
2266 validate_repo_permissions(apiuser, repoid, repo, _perms)
2153
2267
2154 try:
2268 try:
2155 maintenance = repo_maintenance.RepoMaintenance()
2269 maintenance = repo_maintenance.RepoMaintenance()
2156 executed_actions = maintenance.execute(repo)
2270 executed_actions = maintenance.execute(repo)
2157
2271
2158 return {
2272 return {
2159 'msg': 'executed maintenance command',
2273 'msg': 'executed maintenance command',
2160 'executed_actions': executed_actions,
2274 'executed_actions': executed_actions,
2161 'repository': repo.repo_name
2275 'repository': repo.repo_name
2162 }
2276 }
2163 except Exception:
2277 except Exception:
2164 log.exception("Exception occurred while trying to run maintenance")
2278 log.exception("Exception occurred while trying to run maintenance")
2165 raise JSONRPCError(
2279 raise JSONRPCError(
2166 'Unable to execute maintenance on `%s`' % repo.repo_name)
2280 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,821 +1,837 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Module holding everything related to vcs nodes, with vcs2 architecture.
22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
23 """
24
24
25 import os
25 import os
26 import stat
26 import stat
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
29
29
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.lib.utils import safe_unicode, safe_str
31 from rhodecode.lib.utils import safe_unicode, safe_str
32 from rhodecode.lib.utils2 import md5
32 from rhodecode.lib.utils2 import md5
33 from rhodecode.lib.vcs import path as vcspath
33 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37
37
38 LARGEFILE_PREFIX = '.hglf'
38 LARGEFILE_PREFIX = '.hglf'
39
39
40
40
41 class NodeKind:
41 class NodeKind:
42 SUBMODULE = -1
42 SUBMODULE = -1
43 DIR = 1
43 DIR = 1
44 FILE = 2
44 FILE = 2
45 LARGEFILE = 3
45 LARGEFILE = 3
46
46
47
47
48 class NodeState:
48 class NodeState:
49 ADDED = u'added'
49 ADDED = u'added'
50 CHANGED = u'changed'
50 CHANGED = u'changed'
51 NOT_CHANGED = u'not changed'
51 NOT_CHANGED = u'not changed'
52 REMOVED = u'removed'
52 REMOVED = u'removed'
53
53
54
54
55 class NodeGeneratorBase(object):
55 class NodeGeneratorBase(object):
56 """
56 """
57 Base class for removed added and changed filenodes, it's a lazy generator
57 Base class for removed added and changed filenodes, it's a lazy generator
58 class that will create filenodes only on iteration or call
58 class that will create filenodes only on iteration or call
59
59
60 The len method doesn't need to create filenodes at all
60 The len method doesn't need to create filenodes at all
61 """
61 """
62
62
63 def __init__(self, current_paths, cs):
63 def __init__(self, current_paths, cs):
64 self.cs = cs
64 self.cs = cs
65 self.current_paths = current_paths
65 self.current_paths = current_paths
66
66
67 def __call__(self):
67 def __call__(self):
68 return [n for n in self]
68 return [n for n in self]
69
69
70 def __getslice__(self, i, j):
70 def __getslice__(self, i, j):
71 for p in self.current_paths[i:j]:
71 for p in self.current_paths[i:j]:
72 yield self.cs.get_node(p)
72 yield self.cs.get_node(p)
73
73
74 def __len__(self):
74 def __len__(self):
75 return len(self.current_paths)
75 return len(self.current_paths)
76
76
77 def __iter__(self):
77 def __iter__(self):
78 for p in self.current_paths:
78 for p in self.current_paths:
79 yield self.cs.get_node(p)
79 yield self.cs.get_node(p)
80
80
81
81
82 class AddedFileNodesGenerator(NodeGeneratorBase):
82 class AddedFileNodesGenerator(NodeGeneratorBase):
83 """
83 """
84 Class holding added files for current commit
84 Class holding added files for current commit
85 """
85 """
86
86
87
87
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 """
89 """
90 Class holding changed files for current commit
90 Class holding changed files for current commit
91 """
91 """
92
92
93
93
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 """
95 """
96 Class holding removed files for current commit
96 Class holding removed files for current commit
97 """
97 """
98 def __iter__(self):
98 def __iter__(self):
99 for p in self.current_paths:
99 for p in self.current_paths:
100 yield RemovedFileNode(path=p)
100 yield RemovedFileNode(path=p)
101
101
102 def __getslice__(self, i, j):
102 def __getslice__(self, i, j):
103 for p in self.current_paths[i:j]:
103 for p in self.current_paths[i:j]:
104 yield RemovedFileNode(path=p)
104 yield RemovedFileNode(path=p)
105
105
106
106
107 class Node(object):
107 class Node(object):
108 """
108 """
109 Simplest class representing file or directory on repository. SCM backends
109 Simplest class representing file or directory on repository. SCM backends
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 directly.
111 directly.
112
112
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 only. Moreover, every single node is identified by the ``path`` attribute,
114 only. Moreover, every single node is identified by the ``path`` attribute,
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 """
116 """
117 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
117 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
118 # security attacks could be used with this
118 # security attacks could be used with this
119 commit = None
119 commit = None
120
120
121 def __init__(self, path, kind):
121 def __init__(self, path, kind):
122 self._validate_path(path) # can throw exception if path is invalid
122 self._validate_path(path) # can throw exception if path is invalid
123 self.path = safe_str(path.rstrip('/')) # we store paths as str
123 self.path = safe_str(path.rstrip('/')) # we store paths as str
124 if path == '' and kind != NodeKind.DIR:
124 if path == '' and kind != NodeKind.DIR:
125 raise NodeError("Only DirNode and its subclasses may be "
125 raise NodeError("Only DirNode and its subclasses may be "
126 "initialized with empty path")
126 "initialized with empty path")
127 self.kind = kind
127 self.kind = kind
128
128
129 if self.is_root() and not self.is_dir():
129 if self.is_root() and not self.is_dir():
130 raise NodeError("Root node cannot be FILE kind")
130 raise NodeError("Root node cannot be FILE kind")
131
131
132 def _validate_path(self, path):
132 def _validate_path(self, path):
133 if path.startswith('/'):
133 if path.startswith('/'):
134 raise NodeError(
134 raise NodeError(
135 "Cannot initialize Node objects with slash at "
135 "Cannot initialize Node objects with slash at "
136 "the beginning as only relative paths are supported. "
136 "the beginning as only relative paths are supported. "
137 "Got %s" % (path,))
137 "Got %s" % (path,))
138
138
139 @LazyProperty
139 @LazyProperty
140 def parent(self):
140 def parent(self):
141 parent_path = self.get_parent_path()
141 parent_path = self.get_parent_path()
142 if parent_path:
142 if parent_path:
143 if self.commit:
143 if self.commit:
144 return self.commit.get_node(parent_path)
144 return self.commit.get_node(parent_path)
145 return DirNode(parent_path)
145 return DirNode(parent_path)
146 return None
146 return None
147
147
148 @LazyProperty
148 @LazyProperty
149 def unicode_path(self):
149 def unicode_path(self):
150 return safe_unicode(self.path)
150 return safe_unicode(self.path)
151
151
152 @LazyProperty
152 @LazyProperty
153 def has_rtlo(self):
153 def has_rtlo(self):
154 """Detects if a path has right-to-left-override marker"""
154 """Detects if a path has right-to-left-override marker"""
155 return self.RTLO_MARKER in self.unicode_path
155 return self.RTLO_MARKER in self.unicode_path
156
156
157 @LazyProperty
157 @LazyProperty
158 def unicode_path_safe(self):
158 def unicode_path_safe(self):
159 """
159 """
160 Special SAFE representation of path without the right-to-left-override.
160 Special SAFE representation of path without the right-to-left-override.
161 This should be only used for "showing" the file, cannot be used for any
161 This should be only used for "showing" the file, cannot be used for any
162 urls etc.
162 urls etc.
163 """
163 """
164 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
164 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
165
165
166 @LazyProperty
166 @LazyProperty
167 def dir_path(self):
167 def dir_path(self):
168 """
168 """
169 Returns name of the directory from full path of this vcs node. Empty
169 Returns name of the directory from full path of this vcs node. Empty
170 string is returned if there's no directory in the path
170 string is returned if there's no directory in the path
171 """
171 """
172 _parts = self.path.rstrip('/').rsplit('/', 1)
172 _parts = self.path.rstrip('/').rsplit('/', 1)
173 if len(_parts) == 2:
173 if len(_parts) == 2:
174 return safe_unicode(_parts[0])
174 return safe_unicode(_parts[0])
175 return u''
175 return u''
176
176
177 @LazyProperty
177 @LazyProperty
178 def name(self):
178 def name(self):
179 """
179 """
180 Returns name of the node so if its path
180 Returns name of the node so if its path
181 then only last part is returned.
181 then only last part is returned.
182 """
182 """
183 return safe_unicode(self.path.rstrip('/').split('/')[-1])
183 return safe_unicode(self.path.rstrip('/').split('/')[-1])
184
184
185 @property
185 @property
186 def kind(self):
186 def kind(self):
187 return self._kind
187 return self._kind
188
188
189 @kind.setter
189 @kind.setter
190 def kind(self, kind):
190 def kind(self, kind):
191 if hasattr(self, '_kind'):
191 if hasattr(self, '_kind'):
192 raise NodeError("Cannot change node's kind")
192 raise NodeError("Cannot change node's kind")
193 else:
193 else:
194 self._kind = kind
194 self._kind = kind
195 # Post setter check (path's trailing slash)
195 # Post setter check (path's trailing slash)
196 if self.path.endswith('/'):
196 if self.path.endswith('/'):
197 raise NodeError("Node's path cannot end with slash")
197 raise NodeError("Node's path cannot end with slash")
198
198
199 def __cmp__(self, other):
199 def __cmp__(self, other):
200 """
200 """
201 Comparator using name of the node, needed for quick list sorting.
201 Comparator using name of the node, needed for quick list sorting.
202 """
202 """
203 kind_cmp = cmp(self.kind, other.kind)
203 kind_cmp = cmp(self.kind, other.kind)
204 if kind_cmp:
204 if kind_cmp:
205 return kind_cmp
205 return kind_cmp
206 return cmp(self.name, other.name)
206 return cmp(self.name, other.name)
207
207
208 def __eq__(self, other):
208 def __eq__(self, other):
209 for attr in ['name', 'path', 'kind']:
209 for attr in ['name', 'path', 'kind']:
210 if getattr(self, attr) != getattr(other, attr):
210 if getattr(self, attr) != getattr(other, attr):
211 return False
211 return False
212 if self.is_file():
212 if self.is_file():
213 if self.content != other.content:
213 if self.content != other.content:
214 return False
214 return False
215 else:
215 else:
216 # For DirNode's check without entering each dir
216 # For DirNode's check without entering each dir
217 self_nodes_paths = list(sorted(n.path for n in self.nodes))
217 self_nodes_paths = list(sorted(n.path for n in self.nodes))
218 other_nodes_paths = list(sorted(n.path for n in self.nodes))
218 other_nodes_paths = list(sorted(n.path for n in self.nodes))
219 if self_nodes_paths != other_nodes_paths:
219 if self_nodes_paths != other_nodes_paths:
220 return False
220 return False
221 return True
221 return True
222
222
223 def __ne__(self, other):
223 def __ne__(self, other):
224 return not self.__eq__(other)
224 return not self.__eq__(other)
225
225
226 def __repr__(self):
226 def __repr__(self):
227 return '<%s %r>' % (self.__class__.__name__, self.path)
227 return '<%s %r>' % (self.__class__.__name__, self.path)
228
228
229 def __str__(self):
229 def __str__(self):
230 return self.__repr__()
230 return self.__repr__()
231
231
232 def __unicode__(self):
232 def __unicode__(self):
233 return self.name
233 return self.name
234
234
235 def get_parent_path(self):
235 def get_parent_path(self):
236 """
236 """
237 Returns node's parent path or empty string if node is root.
237 Returns node's parent path or empty string if node is root.
238 """
238 """
239 if self.is_root():
239 if self.is_root():
240 return ''
240 return ''
241 return vcspath.dirname(self.path.rstrip('/')) + '/'
241 return vcspath.dirname(self.path.rstrip('/')) + '/'
242
242
243 def is_file(self):
243 def is_file(self):
244 """
244 """
245 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
245 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
246 otherwise.
246 otherwise.
247 """
247 """
248 return self.kind == NodeKind.FILE
248 return self.kind == NodeKind.FILE
249
249
250 def is_dir(self):
250 def is_dir(self):
251 """
251 """
252 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
252 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
253 otherwise.
253 otherwise.
254 """
254 """
255 return self.kind == NodeKind.DIR
255 return self.kind == NodeKind.DIR
256
256
257 def is_root(self):
257 def is_root(self):
258 """
258 """
259 Returns ``True`` if node is a root node and ``False`` otherwise.
259 Returns ``True`` if node is a root node and ``False`` otherwise.
260 """
260 """
261 return self.kind == NodeKind.DIR and self.path == ''
261 return self.kind == NodeKind.DIR and self.path == ''
262
262
263 def is_submodule(self):
263 def is_submodule(self):
264 """
264 """
265 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
265 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
266 otherwise.
266 otherwise.
267 """
267 """
268 return self.kind == NodeKind.SUBMODULE
268 return self.kind == NodeKind.SUBMODULE
269
269
270 def is_largefile(self):
270 def is_largefile(self):
271 """
271 """
272 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
272 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
273 otherwise
273 otherwise
274 """
274 """
275 return self.kind == NodeKind.LARGEFILE
275 return self.kind == NodeKind.LARGEFILE
276
276
277 def is_link(self):
277 def is_link(self):
278 if self.commit:
278 if self.commit:
279 return self.commit.is_link(self.path)
279 return self.commit.is_link(self.path)
280 return False
280 return False
281
281
282 @LazyProperty
282 @LazyProperty
283 def added(self):
283 def added(self):
284 return self.state is NodeState.ADDED
284 return self.state is NodeState.ADDED
285
285
286 @LazyProperty
286 @LazyProperty
287 def changed(self):
287 def changed(self):
288 return self.state is NodeState.CHANGED
288 return self.state is NodeState.CHANGED
289
289
290 @LazyProperty
290 @LazyProperty
291 def not_changed(self):
291 def not_changed(self):
292 return self.state is NodeState.NOT_CHANGED
292 return self.state is NodeState.NOT_CHANGED
293
293
294 @LazyProperty
294 @LazyProperty
295 def removed(self):
295 def removed(self):
296 return self.state is NodeState.REMOVED
296 return self.state is NodeState.REMOVED
297
297
298
298
299 class FileNode(Node):
299 class FileNode(Node):
300 """
300 """
301 Class representing file nodes.
301 Class representing file nodes.
302
302
303 :attribute: path: path to the node, relative to repository's root
303 :attribute: path: path to the node, relative to repository's root
304 :attribute: content: if given arbitrary sets content of the file
304 :attribute: content: if given arbitrary sets content of the file
305 :attribute: commit: if given, first time content is accessed, callback
305 :attribute: commit: if given, first time content is accessed, callback
306 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
306 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
307 """
307 """
308 _filter_pre_load = []
308 _filter_pre_load = []
309
309
310 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
310 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
311 """
311 """
312 Only one of ``content`` and ``commit`` may be given. Passing both
312 Only one of ``content`` and ``commit`` may be given. Passing both
313 would raise ``NodeError`` exception.
313 would raise ``NodeError`` exception.
314
314
315 :param path: relative path to the node
315 :param path: relative path to the node
316 :param content: content may be passed to constructor
316 :param content: content may be passed to constructor
317 :param commit: if given, will use it to lazily fetch content
317 :param commit: if given, will use it to lazily fetch content
318 :param mode: ST_MODE (i.e. 0100644)
318 :param mode: ST_MODE (i.e. 0100644)
319 """
319 """
320 if content and commit:
320 if content and commit:
321 raise NodeError("Cannot use both content and commit")
321 raise NodeError("Cannot use both content and commit")
322 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
322 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
323 self.commit = commit
323 self.commit = commit
324 self._content = content
324 self._content = content
325 self._mode = mode or FILEMODE_DEFAULT
325 self._mode = mode or FILEMODE_DEFAULT
326
326
327 self._set_bulk_properties(pre_load)
327 self._set_bulk_properties(pre_load)
328
328
329 def _set_bulk_properties(self, pre_load):
329 def _set_bulk_properties(self, pre_load):
330 if not pre_load:
330 if not pre_load:
331 return
331 return
332 pre_load = [entry for entry in pre_load
332 pre_load = [entry for entry in pre_load
333 if entry not in self._filter_pre_load]
333 if entry not in self._filter_pre_load]
334 if not pre_load:
334 if not pre_load:
335 return
335 return
336
336
337 for attr_name in pre_load:
337 for attr_name in pre_load:
338 result = getattr(self, attr_name)
338 result = getattr(self, attr_name)
339 if callable(result):
339 if callable(result):
340 result = result()
340 result = result()
341 self.__dict__[attr_name] = result
341 self.__dict__[attr_name] = result
342
342
343 @LazyProperty
343 @LazyProperty
344 def mode(self):
344 def mode(self):
345 """
345 """
346 Returns lazily mode of the FileNode. If `commit` is not set, would
346 Returns lazily mode of the FileNode. If `commit` is not set, would
347 use value given at initialization or `FILEMODE_DEFAULT` (default).
347 use value given at initialization or `FILEMODE_DEFAULT` (default).
348 """
348 """
349 if self.commit:
349 if self.commit:
350 mode = self.commit.get_file_mode(self.path)
350 mode = self.commit.get_file_mode(self.path)
351 else:
351 else:
352 mode = self._mode
352 mode = self._mode
353 return mode
353 return mode
354
354
355 @LazyProperty
355 @LazyProperty
356 def raw_bytes(self):
356 def raw_bytes(self):
357 """
357 """
358 Returns lazily the raw bytes of the FileNode.
358 Returns lazily the raw bytes of the FileNode.
359 """
359 """
360 if self.commit:
360 if self.commit:
361 if self._content is None:
361 if self._content is None:
362 self._content = self.commit.get_file_content(self.path)
362 self._content = self.commit.get_file_content(self.path)
363 content = self._content
363 content = self._content
364 else:
364 else:
365 content = self._content
365 content = self._content
366 return content
366 return content
367
367
368 @LazyProperty
368 @LazyProperty
369 def md5(self):
369 def md5(self):
370 """
370 """
371 Returns md5 of the file node.
371 Returns md5 of the file node.
372 """
372 """
373 return md5(self.raw_bytes)
373 return md5(self.raw_bytes)
374
374
375 def metadata_uncached(self):
376 """
377 Returns md5, binary flag of the file node, without any cache usage.
378 """
379
380 if self.commit:
381 content = self.commit.get_file_content(self.path)
382 else:
383 content = self._content
384
385 is_binary = content and '\0' in content
386 size = 0
387 if content:
388 size = len(content)
389 return is_binary, md5(content), size
390
375 @LazyProperty
391 @LazyProperty
376 def content(self):
392 def content(self):
377 """
393 """
378 Returns lazily content of the FileNode. If possible, would try to
394 Returns lazily content of the FileNode. If possible, would try to
379 decode content from UTF-8.
395 decode content from UTF-8.
380 """
396 """
381 content = self.raw_bytes
397 content = self.raw_bytes
382
398
383 if self.is_binary:
399 if self.is_binary:
384 return content
400 return content
385 return safe_unicode(content)
401 return safe_unicode(content)
386
402
387 @LazyProperty
403 @LazyProperty
388 def size(self):
404 def size(self):
389 if self.commit:
405 if self.commit:
390 return self.commit.get_file_size(self.path)
406 return self.commit.get_file_size(self.path)
391 raise NodeError(
407 raise NodeError(
392 "Cannot retrieve size of the file without related "
408 "Cannot retrieve size of the file without related "
393 "commit attribute")
409 "commit attribute")
394
410
395 @LazyProperty
411 @LazyProperty
396 def message(self):
412 def message(self):
397 if self.commit:
413 if self.commit:
398 return self.last_commit.message
414 return self.last_commit.message
399 raise NodeError(
415 raise NodeError(
400 "Cannot retrieve message of the file without related "
416 "Cannot retrieve message of the file without related "
401 "commit attribute")
417 "commit attribute")
402
418
403 @LazyProperty
419 @LazyProperty
404 def last_commit(self):
420 def last_commit(self):
405 if self.commit:
421 if self.commit:
406 pre_load = ["author", "date", "message"]
422 pre_load = ["author", "date", "message"]
407 return self.commit.get_path_commit(self.path, pre_load=pre_load)
423 return self.commit.get_path_commit(self.path, pre_load=pre_load)
408 raise NodeError(
424 raise NodeError(
409 "Cannot retrieve last commit of the file without "
425 "Cannot retrieve last commit of the file without "
410 "related commit attribute")
426 "related commit attribute")
411
427
412 def get_mimetype(self):
428 def get_mimetype(self):
413 """
429 """
414 Mimetype is calculated based on the file's content. If ``_mimetype``
430 Mimetype is calculated based on the file's content. If ``_mimetype``
415 attribute is available, it will be returned (backends which store
431 attribute is available, it will be returned (backends which store
416 mimetypes or can easily recognize them, should set this private
432 mimetypes or can easily recognize them, should set this private
417 attribute to indicate that type should *NOT* be calculated).
433 attribute to indicate that type should *NOT* be calculated).
418 """
434 """
419
435
420 if hasattr(self, '_mimetype'):
436 if hasattr(self, '_mimetype'):
421 if (isinstance(self._mimetype, (tuple, list,)) and
437 if (isinstance(self._mimetype, (tuple, list,)) and
422 len(self._mimetype) == 2):
438 len(self._mimetype) == 2):
423 return self._mimetype
439 return self._mimetype
424 else:
440 else:
425 raise NodeError('given _mimetype attribute must be an 2 '
441 raise NodeError('given _mimetype attribute must be an 2 '
426 'element list or tuple')
442 'element list or tuple')
427
443
428 db = get_mimetypes_db()
444 db = get_mimetypes_db()
429 mtype, encoding = db.guess_type(self.name)
445 mtype, encoding = db.guess_type(self.name)
430
446
431 if mtype is None:
447 if mtype is None:
432 if self.is_binary:
448 if self.is_binary:
433 mtype = 'application/octet-stream'
449 mtype = 'application/octet-stream'
434 encoding = None
450 encoding = None
435 else:
451 else:
436 mtype = 'text/plain'
452 mtype = 'text/plain'
437 encoding = None
453 encoding = None
438
454
439 # try with pygments
455 # try with pygments
440 try:
456 try:
441 from pygments.lexers import get_lexer_for_filename
457 from pygments.lexers import get_lexer_for_filename
442 mt = get_lexer_for_filename(self.name).mimetypes
458 mt = get_lexer_for_filename(self.name).mimetypes
443 except Exception:
459 except Exception:
444 mt = None
460 mt = None
445
461
446 if mt:
462 if mt:
447 mtype = mt[0]
463 mtype = mt[0]
448
464
449 return mtype, encoding
465 return mtype, encoding
450
466
451 @LazyProperty
467 @LazyProperty
452 def mimetype(self):
468 def mimetype(self):
453 """
469 """
454 Wrapper around full mimetype info. It returns only type of fetched
470 Wrapper around full mimetype info. It returns only type of fetched
455 mimetype without the encoding part. use get_mimetype function to fetch
471 mimetype without the encoding part. use get_mimetype function to fetch
456 full set of (type,encoding)
472 full set of (type,encoding)
457 """
473 """
458 return self.get_mimetype()[0]
474 return self.get_mimetype()[0]
459
475
460 @LazyProperty
476 @LazyProperty
461 def mimetype_main(self):
477 def mimetype_main(self):
462 return self.mimetype.split('/')[0]
478 return self.mimetype.split('/')[0]
463
479
464 @classmethod
480 @classmethod
465 def get_lexer(cls, filename, content=None):
481 def get_lexer(cls, filename, content=None):
466 from pygments import lexers
482 from pygments import lexers
467
483
468 extension = filename.split('.')[-1]
484 extension = filename.split('.')[-1]
469 lexer = None
485 lexer = None
470
486
471 try:
487 try:
472 lexer = lexers.guess_lexer_for_filename(
488 lexer = lexers.guess_lexer_for_filename(
473 filename, content, stripnl=False)
489 filename, content, stripnl=False)
474 except lexers.ClassNotFound:
490 except lexers.ClassNotFound:
475 lexer = None
491 lexer = None
476
492
477 # try our EXTENSION_MAP
493 # try our EXTENSION_MAP
478 if not lexer:
494 if not lexer:
479 try:
495 try:
480 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
496 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
481 if lexer_class:
497 if lexer_class:
482 lexer = lexers.get_lexer_by_name(lexer_class[0])
498 lexer = lexers.get_lexer_by_name(lexer_class[0])
483 except lexers.ClassNotFound:
499 except lexers.ClassNotFound:
484 lexer = None
500 lexer = None
485
501
486 if not lexer:
502 if not lexer:
487 lexer = lexers.TextLexer(stripnl=False)
503 lexer = lexers.TextLexer(stripnl=False)
488
504
489 return lexer
505 return lexer
490
506
491 @LazyProperty
507 @LazyProperty
492 def lexer(self):
508 def lexer(self):
493 """
509 """
494 Returns pygment's lexer class. Would try to guess lexer taking file's
510 Returns pygment's lexer class. Would try to guess lexer taking file's
495 content, name and mimetype.
511 content, name and mimetype.
496 """
512 """
497 return self.get_lexer(self.name, self.content)
513 return self.get_lexer(self.name, self.content)
498
514
499 @LazyProperty
515 @LazyProperty
500 def lexer_alias(self):
516 def lexer_alias(self):
501 """
517 """
502 Returns first alias of the lexer guessed for this file.
518 Returns first alias of the lexer guessed for this file.
503 """
519 """
504 return self.lexer.aliases[0]
520 return self.lexer.aliases[0]
505
521
506 @LazyProperty
522 @LazyProperty
507 def history(self):
523 def history(self):
508 """
524 """
509 Returns a list of commit for this file in which the file was changed
525 Returns a list of commit for this file in which the file was changed
510 """
526 """
511 if self.commit is None:
527 if self.commit is None:
512 raise NodeError('Unable to get commit for this FileNode')
528 raise NodeError('Unable to get commit for this FileNode')
513 return self.commit.get_path_history(self.path)
529 return self.commit.get_path_history(self.path)
514
530
515 @LazyProperty
531 @LazyProperty
516 def annotate(self):
532 def annotate(self):
517 """
533 """
518 Returns a list of three element tuples with lineno, commit and line
534 Returns a list of three element tuples with lineno, commit and line
519 """
535 """
520 if self.commit is None:
536 if self.commit is None:
521 raise NodeError('Unable to get commit for this FileNode')
537 raise NodeError('Unable to get commit for this FileNode')
522 pre_load = ["author", "date", "message"]
538 pre_load = ["author", "date", "message"]
523 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
539 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
524
540
525 @LazyProperty
541 @LazyProperty
526 def state(self):
542 def state(self):
527 if not self.commit:
543 if not self.commit:
528 raise NodeError(
544 raise NodeError(
529 "Cannot check state of the node if it's not "
545 "Cannot check state of the node if it's not "
530 "linked with commit")
546 "linked with commit")
531 elif self.path in (node.path for node in self.commit.added):
547 elif self.path in (node.path for node in self.commit.added):
532 return NodeState.ADDED
548 return NodeState.ADDED
533 elif self.path in (node.path for node in self.commit.changed):
549 elif self.path in (node.path for node in self.commit.changed):
534 return NodeState.CHANGED
550 return NodeState.CHANGED
535 else:
551 else:
536 return NodeState.NOT_CHANGED
552 return NodeState.NOT_CHANGED
537
553
538 @LazyProperty
554 @LazyProperty
539 def is_binary(self):
555 def is_binary(self):
540 """
556 """
541 Returns True if file has binary content.
557 Returns True if file has binary content.
542 """
558 """
543 _bin = self.raw_bytes and '\0' in self.raw_bytes
559 _bin = self.raw_bytes and '\0' in self.raw_bytes
544 return _bin
560 return _bin
545
561
546 @LazyProperty
562 @LazyProperty
547 def extension(self):
563 def extension(self):
548 """Returns filenode extension"""
564 """Returns filenode extension"""
549 return self.name.split('.')[-1]
565 return self.name.split('.')[-1]
550
566
551 @property
567 @property
552 def is_executable(self):
568 def is_executable(self):
553 """
569 """
554 Returns ``True`` if file has executable flag turned on.
570 Returns ``True`` if file has executable flag turned on.
555 """
571 """
556 return bool(self.mode & stat.S_IXUSR)
572 return bool(self.mode & stat.S_IXUSR)
557
573
558 def get_largefile_node(self):
574 def get_largefile_node(self):
559 """
575 """
560 Try to return a Mercurial FileNode from this node. It does internal
576 Try to return a Mercurial FileNode from this node. It does internal
561 checks inside largefile store, if that file exist there it will
577 checks inside largefile store, if that file exist there it will
562 create special instance of LargeFileNode which can get content from
578 create special instance of LargeFileNode which can get content from
563 LF store.
579 LF store.
564 """
580 """
565 if self.commit:
581 if self.commit:
566 return self.commit.get_largefile_node(self.path)
582 return self.commit.get_largefile_node(self.path)
567
583
568 def lines(self, count_empty=False):
584 def lines(self, count_empty=False):
569 all_lines, empty_lines = 0, 0
585 all_lines, empty_lines = 0, 0
570
586
571 if not self.is_binary:
587 if not self.is_binary:
572 content = self.content
588 content = self.content
573 if count_empty:
589 if count_empty:
574 all_lines = 0
590 all_lines = 0
575 empty_lines = 0
591 empty_lines = 0
576 for line in content.splitlines(True):
592 for line in content.splitlines(True):
577 if line == '\n':
593 if line == '\n':
578 empty_lines += 1
594 empty_lines += 1
579 all_lines += 1
595 all_lines += 1
580
596
581 return all_lines, all_lines - empty_lines
597 return all_lines, all_lines - empty_lines
582 else:
598 else:
583 # fast method
599 # fast method
584 empty_lines = all_lines = content.count('\n')
600 empty_lines = all_lines = content.count('\n')
585 if all_lines == 0 and content:
601 if all_lines == 0 and content:
586 # one-line without a newline
602 # one-line without a newline
587 empty_lines = all_lines = 1
603 empty_lines = all_lines = 1
588
604
589 return all_lines, empty_lines
605 return all_lines, empty_lines
590
606
591 def __repr__(self):
607 def __repr__(self):
592 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
608 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
593 getattr(self.commit, 'short_id', ''))
609 getattr(self.commit, 'short_id', ''))
594
610
595
611
596 class RemovedFileNode(FileNode):
612 class RemovedFileNode(FileNode):
597 """
613 """
598 Dummy FileNode class - trying to access any public attribute except path,
614 Dummy FileNode class - trying to access any public attribute except path,
599 name, kind or state (or methods/attributes checking those two) would raise
615 name, kind or state (or methods/attributes checking those two) would raise
600 RemovedFileNodeError.
616 RemovedFileNodeError.
601 """
617 """
602 ALLOWED_ATTRIBUTES = [
618 ALLOWED_ATTRIBUTES = [
603 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
619 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
604 'added', 'changed', 'not_changed', 'removed'
620 'added', 'changed', 'not_changed', 'removed'
605 ]
621 ]
606
622
607 def __init__(self, path):
623 def __init__(self, path):
608 """
624 """
609 :param path: relative path to the node
625 :param path: relative path to the node
610 """
626 """
611 super(RemovedFileNode, self).__init__(path=path)
627 super(RemovedFileNode, self).__init__(path=path)
612
628
613 def __getattribute__(self, attr):
629 def __getattribute__(self, attr):
614 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
630 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
615 return super(RemovedFileNode, self).__getattribute__(attr)
631 return super(RemovedFileNode, self).__getattribute__(attr)
616 raise RemovedFileNodeError(
632 raise RemovedFileNodeError(
617 "Cannot access attribute %s on RemovedFileNode" % attr)
633 "Cannot access attribute %s on RemovedFileNode" % attr)
618
634
619 @LazyProperty
635 @LazyProperty
620 def state(self):
636 def state(self):
621 return NodeState.REMOVED
637 return NodeState.REMOVED
622
638
623
639
624 class DirNode(Node):
640 class DirNode(Node):
625 """
641 """
626 DirNode stores list of files and directories within this node.
642 DirNode stores list of files and directories within this node.
627 Nodes may be used standalone but within repository context they
643 Nodes may be used standalone but within repository context they
628 lazily fetch data within same repositorty's commit.
644 lazily fetch data within same repositorty's commit.
629 """
645 """
630
646
631 def __init__(self, path, nodes=(), commit=None):
647 def __init__(self, path, nodes=(), commit=None):
632 """
648 """
633 Only one of ``nodes`` and ``commit`` may be given. Passing both
649 Only one of ``nodes`` and ``commit`` may be given. Passing both
634 would raise ``NodeError`` exception.
650 would raise ``NodeError`` exception.
635
651
636 :param path: relative path to the node
652 :param path: relative path to the node
637 :param nodes: content may be passed to constructor
653 :param nodes: content may be passed to constructor
638 :param commit: if given, will use it to lazily fetch content
654 :param commit: if given, will use it to lazily fetch content
639 """
655 """
640 if nodes and commit:
656 if nodes and commit:
641 raise NodeError("Cannot use both nodes and commit")
657 raise NodeError("Cannot use both nodes and commit")
642 super(DirNode, self).__init__(path, NodeKind.DIR)
658 super(DirNode, self).__init__(path, NodeKind.DIR)
643 self.commit = commit
659 self.commit = commit
644 self._nodes = nodes
660 self._nodes = nodes
645
661
646 @LazyProperty
662 @LazyProperty
647 def content(self):
663 def content(self):
648 raise NodeError(
664 raise NodeError(
649 "%s represents a dir and has no `content` attribute" % self)
665 "%s represents a dir and has no `content` attribute" % self)
650
666
651 @LazyProperty
667 @LazyProperty
652 def nodes(self):
668 def nodes(self):
653 if self.commit:
669 if self.commit:
654 nodes = self.commit.get_nodes(self.path)
670 nodes = self.commit.get_nodes(self.path)
655 else:
671 else:
656 nodes = self._nodes
672 nodes = self._nodes
657 self._nodes_dict = dict((node.path, node) for node in nodes)
673 self._nodes_dict = dict((node.path, node) for node in nodes)
658 return sorted(nodes)
674 return sorted(nodes)
659
675
660 @LazyProperty
676 @LazyProperty
661 def files(self):
677 def files(self):
662 return sorted((node for node in self.nodes if node.is_file()))
678 return sorted((node for node in self.nodes if node.is_file()))
663
679
664 @LazyProperty
680 @LazyProperty
665 def dirs(self):
681 def dirs(self):
666 return sorted((node for node in self.nodes if node.is_dir()))
682 return sorted((node for node in self.nodes if node.is_dir()))
667
683
668 def __iter__(self):
684 def __iter__(self):
669 for node in self.nodes:
685 for node in self.nodes:
670 yield node
686 yield node
671
687
672 def get_node(self, path):
688 def get_node(self, path):
673 """
689 """
674 Returns node from within this particular ``DirNode``, so it is now
690 Returns node from within this particular ``DirNode``, so it is now
675 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
691 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
676 'docs'. In order to access deeper nodes one must fetch nodes between
692 'docs'. In order to access deeper nodes one must fetch nodes between
677 them first - this would work::
693 them first - this would work::
678
694
679 docs = root.get_node('docs')
695 docs = root.get_node('docs')
680 docs.get_node('api').get_node('index.rst')
696 docs.get_node('api').get_node('index.rst')
681
697
682 :param: path - relative to the current node
698 :param: path - relative to the current node
683
699
684 .. note::
700 .. note::
685 To access lazily (as in example above) node have to be initialized
701 To access lazily (as in example above) node have to be initialized
686 with related commit object - without it node is out of
702 with related commit object - without it node is out of
687 context and may know nothing about anything else than nearest
703 context and may know nothing about anything else than nearest
688 (located at same level) nodes.
704 (located at same level) nodes.
689 """
705 """
690 try:
706 try:
691 path = path.rstrip('/')
707 path = path.rstrip('/')
692 if path == '':
708 if path == '':
693 raise NodeError("Cannot retrieve node without path")
709 raise NodeError("Cannot retrieve node without path")
694 self.nodes # access nodes first in order to set _nodes_dict
710 self.nodes # access nodes first in order to set _nodes_dict
695 paths = path.split('/')
711 paths = path.split('/')
696 if len(paths) == 1:
712 if len(paths) == 1:
697 if not self.is_root():
713 if not self.is_root():
698 path = '/'.join((self.path, paths[0]))
714 path = '/'.join((self.path, paths[0]))
699 else:
715 else:
700 path = paths[0]
716 path = paths[0]
701 return self._nodes_dict[path]
717 return self._nodes_dict[path]
702 elif len(paths) > 1:
718 elif len(paths) > 1:
703 if self.commit is None:
719 if self.commit is None:
704 raise NodeError(
720 raise NodeError(
705 "Cannot access deeper nodes without commit")
721 "Cannot access deeper nodes without commit")
706 else:
722 else:
707 path1, path2 = paths[0], '/'.join(paths[1:])
723 path1, path2 = paths[0], '/'.join(paths[1:])
708 return self.get_node(path1).get_node(path2)
724 return self.get_node(path1).get_node(path2)
709 else:
725 else:
710 raise KeyError
726 raise KeyError
711 except KeyError:
727 except KeyError:
712 raise NodeError("Node does not exist at %s" % path)
728 raise NodeError("Node does not exist at %s" % path)
713
729
714 @LazyProperty
730 @LazyProperty
715 def state(self):
731 def state(self):
716 raise NodeError("Cannot access state of DirNode")
732 raise NodeError("Cannot access state of DirNode")
717
733
718 @LazyProperty
734 @LazyProperty
719 def size(self):
735 def size(self):
720 size = 0
736 size = 0
721 for root, dirs, files in self.commit.walk(self.path):
737 for root, dirs, files in self.commit.walk(self.path):
722 for f in files:
738 for f in files:
723 size += f.size
739 size += f.size
724
740
725 return size
741 return size
726
742
727 @LazyProperty
743 @LazyProperty
728 def last_commit(self):
744 def last_commit(self):
729 if self.commit:
745 if self.commit:
730 pre_load = ["author", "date", "message"]
746 pre_load = ["author", "date", "message"]
731 return self.commit.get_path_commit(self.path, pre_load=pre_load)
747 return self.commit.get_path_commit(self.path, pre_load=pre_load)
732 raise NodeError(
748 raise NodeError(
733 "Cannot retrieve last commit of the file without "
749 "Cannot retrieve last commit of the file without "
734 "related commit attribute")
750 "related commit attribute")
735
751
736 def __repr__(self):
752 def __repr__(self):
737 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
753 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
738 getattr(self.commit, 'short_id', ''))
754 getattr(self.commit, 'short_id', ''))
739
755
740
756
741 class RootNode(DirNode):
757 class RootNode(DirNode):
742 """
758 """
743 DirNode being the root node of the repository.
759 DirNode being the root node of the repository.
744 """
760 """
745
761
746 def __init__(self, nodes=(), commit=None):
762 def __init__(self, nodes=(), commit=None):
747 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
763 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
748
764
749 def __repr__(self):
765 def __repr__(self):
750 return '<%s>' % self.__class__.__name__
766 return '<%s>' % self.__class__.__name__
751
767
752
768
753 class SubModuleNode(Node):
769 class SubModuleNode(Node):
754 """
770 """
755 represents a SubModule of Git or SubRepo of Mercurial
771 represents a SubModule of Git or SubRepo of Mercurial
756 """
772 """
757 is_binary = False
773 is_binary = False
758 size = 0
774 size = 0
759
775
760 def __init__(self, name, url=None, commit=None, alias=None):
776 def __init__(self, name, url=None, commit=None, alias=None):
761 self.path = name
777 self.path = name
762 self.kind = NodeKind.SUBMODULE
778 self.kind = NodeKind.SUBMODULE
763 self.alias = alias
779 self.alias = alias
764
780
765 # we have to use EmptyCommit here since this can point to svn/git/hg
781 # we have to use EmptyCommit here since this can point to svn/git/hg
766 # submodules we cannot get from repository
782 # submodules we cannot get from repository
767 self.commit = EmptyCommit(str(commit), alias=alias)
783 self.commit = EmptyCommit(str(commit), alias=alias)
768 self.url = url or self._extract_submodule_url()
784 self.url = url or self._extract_submodule_url()
769
785
770 def __repr__(self):
786 def __repr__(self):
771 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
787 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
772 getattr(self.commit, 'short_id', ''))
788 getattr(self.commit, 'short_id', ''))
773
789
774 def _extract_submodule_url(self):
790 def _extract_submodule_url(self):
775 # TODO: find a way to parse gits submodule file and extract the
791 # TODO: find a way to parse gits submodule file and extract the
776 # linking URL
792 # linking URL
777 return self.path
793 return self.path
778
794
779 @LazyProperty
795 @LazyProperty
780 def name(self):
796 def name(self):
781 """
797 """
782 Returns name of the node so if its path
798 Returns name of the node so if its path
783 then only last part is returned.
799 then only last part is returned.
784 """
800 """
785 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
801 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
786 return u'%s @ %s' % (org, self.commit.short_id)
802 return u'%s @ %s' % (org, self.commit.short_id)
787
803
788
804
789 class LargeFileNode(FileNode):
805 class LargeFileNode(FileNode):
790
806
791 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
807 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
792 self.path = path
808 self.path = path
793 self.org_path = org_path
809 self.org_path = org_path
794 self.kind = NodeKind.LARGEFILE
810 self.kind = NodeKind.LARGEFILE
795 self.alias = alias
811 self.alias = alias
796
812
797 def _validate_path(self, path):
813 def _validate_path(self, path):
798 """
814 """
799 we override check since the LargeFileNode path is system absolute
815 we override check since the LargeFileNode path is system absolute
800 """
816 """
801 pass
817 pass
802
818
803 def __repr__(self):
819 def __repr__(self):
804 return '<%s %r>' % (self.__class__.__name__, self.path)
820 return '<%s %r>' % (self.__class__.__name__, self.path)
805
821
806 @LazyProperty
822 @LazyProperty
807 def size(self):
823 def size(self):
808 return os.stat(self.path).st_size
824 return os.stat(self.path).st_size
809
825
810 @LazyProperty
826 @LazyProperty
811 def raw_bytes(self):
827 def raw_bytes(self):
812 with open(self.path, 'rb') as f:
828 with open(self.path, 'rb') as f:
813 content = f.read()
829 content = f.read()
814 return content
830 return content
815
831
816 @LazyProperty
832 @LazyProperty
817 def name(self):
833 def name(self):
818 """
834 """
819 Overwrites name to be the org lf path
835 Overwrites name to be the org lf path
820 """
836 """
821 return self.org_path
837 return self.org_path
@@ -1,832 +1,914 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Scm model for RhodeCode
22 Scm model for RhodeCode
23 """
23 """
24
24
25 import os.path
25 import os.path
26 import traceback
26 import traceback
27 import logging
27 import logging
28 import cStringIO
28 import cStringIO
29
29
30 from sqlalchemy import func
30 from sqlalchemy import func
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.lib.vcs import get_backend
34 from rhodecode.lib.vcs import get_backend
35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 from rhodecode.lib import helpers as h, rc_cache
38 from rhodecode.lib import helpers as h, rc_cache
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
41 HasUserGroupPermissionAny)
41 HasUserGroupPermissionAny)
42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
43 from rhodecode.lib import hooks_utils
43 from rhodecode.lib import hooks_utils
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_filesystem_repos, make_db_config)
45 get_filesystem_repos, make_db_config)
46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
47 from rhodecode.lib.system_info import get_system_info
47 from rhodecode.lib.system_info import get_system_info
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 PullRequest)
51 PullRequest)
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class UserTemp(object):
58 class UserTemp(object):
59 def __init__(self, user_id):
59 def __init__(self, user_id):
60 self.user_id = user_id
60 self.user_id = user_id
61
61
62 def __repr__(self):
62 def __repr__(self):
63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64
64
65
65
66 class RepoTemp(object):
66 class RepoTemp(object):
67 def __init__(self, repo_id):
67 def __init__(self, repo_id):
68 self.repo_id = repo_id
68 self.repo_id = repo_id
69
69
70 def __repr__(self):
70 def __repr__(self):
71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72
72
73
73
74 class SimpleCachedRepoList(object):
74 class SimpleCachedRepoList(object):
75 """
75 """
76 Lighter version of of iteration of repos without the scm initialisation,
76 Lighter version of of iteration of repos without the scm initialisation,
77 and with cache usage
77 and with cache usage
78 """
78 """
79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 self.db_repo_list = db_repo_list
80 self.db_repo_list = db_repo_list
81 self.repos_path = repos_path
81 self.repos_path = repos_path
82 self.order_by = order_by
82 self.order_by = order_by
83 self.reversed = (order_by or '').startswith('-')
83 self.reversed = (order_by or '').startswith('-')
84 if not perm_set:
84 if not perm_set:
85 perm_set = ['repository.read', 'repository.write',
85 perm_set = ['repository.read', 'repository.write',
86 'repository.admin']
86 'repository.admin']
87 self.perm_set = perm_set
87 self.perm_set = perm_set
88
88
89 def __len__(self):
89 def __len__(self):
90 return len(self.db_repo_list)
90 return len(self.db_repo_list)
91
91
92 def __repr__(self):
92 def __repr__(self):
93 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
93 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
94
94
95 def __iter__(self):
95 def __iter__(self):
96 for dbr in self.db_repo_list:
96 for dbr in self.db_repo_list:
97 # check permission at this level
97 # check permission at this level
98 has_perm = HasRepoPermissionAny(*self.perm_set)(
98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 dbr.repo_name, 'SimpleCachedRepoList check')
99 dbr.repo_name, 'SimpleCachedRepoList check')
100 if not has_perm:
100 if not has_perm:
101 continue
101 continue
102
102
103 tmp_d = {
103 tmp_d = {
104 'name': dbr.repo_name,
104 'name': dbr.repo_name,
105 'dbrepo': dbr.get_dict(),
105 'dbrepo': dbr.get_dict(),
106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 }
107 }
108 yield tmp_d
108 yield tmp_d
109
109
110
110
111 class _PermCheckIterator(object):
111 class _PermCheckIterator(object):
112
112
113 def __init__(
113 def __init__(
114 self, obj_list, obj_attr, perm_set, perm_checker,
114 self, obj_list, obj_attr, perm_set, perm_checker,
115 extra_kwargs=None):
115 extra_kwargs=None):
116 """
116 """
117 Creates iterator from given list of objects, additionally
117 Creates iterator from given list of objects, additionally
118 checking permission for them from perm_set var
118 checking permission for them from perm_set var
119
119
120 :param obj_list: list of db objects
120 :param obj_list: list of db objects
121 :param obj_attr: attribute of object to pass into perm_checker
121 :param obj_attr: attribute of object to pass into perm_checker
122 :param perm_set: list of permissions to check
122 :param perm_set: list of permissions to check
123 :param perm_checker: callable to check permissions against
123 :param perm_checker: callable to check permissions against
124 """
124 """
125 self.obj_list = obj_list
125 self.obj_list = obj_list
126 self.obj_attr = obj_attr
126 self.obj_attr = obj_attr
127 self.perm_set = perm_set
127 self.perm_set = perm_set
128 self.perm_checker = perm_checker
128 self.perm_checker = perm_checker
129 self.extra_kwargs = extra_kwargs or {}
129 self.extra_kwargs = extra_kwargs or {}
130
130
131 def __len__(self):
131 def __len__(self):
132 return len(self.obj_list)
132 return len(self.obj_list)
133
133
134 def __repr__(self):
134 def __repr__(self):
135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136
136
137 def __iter__(self):
137 def __iter__(self):
138 checker = self.perm_checker(*self.perm_set)
138 checker = self.perm_checker(*self.perm_set)
139 for db_obj in self.obj_list:
139 for db_obj in self.obj_list:
140 # check permission at this level
140 # check permission at this level
141 name = getattr(db_obj, self.obj_attr, None)
141 name = getattr(db_obj, self.obj_attr, None)
142 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
142 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
143 continue
143 continue
144
144
145 yield db_obj
145 yield db_obj
146
146
147
147
148 class RepoList(_PermCheckIterator):
148 class RepoList(_PermCheckIterator):
149
149
150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 if not perm_set:
151 if not perm_set:
152 perm_set = [
152 perm_set = [
153 'repository.read', 'repository.write', 'repository.admin']
153 'repository.read', 'repository.write', 'repository.admin']
154
154
155 super(RepoList, self).__init__(
155 super(RepoList, self).__init__(
156 obj_list=db_repo_list,
156 obj_list=db_repo_list,
157 obj_attr='repo_name', perm_set=perm_set,
157 obj_attr='repo_name', perm_set=perm_set,
158 perm_checker=HasRepoPermissionAny,
158 perm_checker=HasRepoPermissionAny,
159 extra_kwargs=extra_kwargs)
159 extra_kwargs=extra_kwargs)
160
160
161
161
162 class RepoGroupList(_PermCheckIterator):
162 class RepoGroupList(_PermCheckIterator):
163
163
164 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
164 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
165 if not perm_set:
165 if not perm_set:
166 perm_set = ['group.read', 'group.write', 'group.admin']
166 perm_set = ['group.read', 'group.write', 'group.admin']
167
167
168 super(RepoGroupList, self).__init__(
168 super(RepoGroupList, self).__init__(
169 obj_list=db_repo_group_list,
169 obj_list=db_repo_group_list,
170 obj_attr='group_name', perm_set=perm_set,
170 obj_attr='group_name', perm_set=perm_set,
171 perm_checker=HasRepoGroupPermissionAny,
171 perm_checker=HasRepoGroupPermissionAny,
172 extra_kwargs=extra_kwargs)
172 extra_kwargs=extra_kwargs)
173
173
174
174
175 class UserGroupList(_PermCheckIterator):
175 class UserGroupList(_PermCheckIterator):
176
176
177 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
177 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
178 if not perm_set:
178 if not perm_set:
179 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
179 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
180
180
181 super(UserGroupList, self).__init__(
181 super(UserGroupList, self).__init__(
182 obj_list=db_user_group_list,
182 obj_list=db_user_group_list,
183 obj_attr='users_group_name', perm_set=perm_set,
183 obj_attr='users_group_name', perm_set=perm_set,
184 perm_checker=HasUserGroupPermissionAny,
184 perm_checker=HasUserGroupPermissionAny,
185 extra_kwargs=extra_kwargs)
185 extra_kwargs=extra_kwargs)
186
186
187
187
188 class ScmModel(BaseModel):
188 class ScmModel(BaseModel):
189 """
189 """
190 Generic Scm Model
190 Generic Scm Model
191 """
191 """
192
192
193 @LazyProperty
193 @LazyProperty
194 def repos_path(self):
194 def repos_path(self):
195 """
195 """
196 Gets the repositories root path from database
196 Gets the repositories root path from database
197 """
197 """
198
198
199 settings_model = VcsSettingsModel(sa=self.sa)
199 settings_model = VcsSettingsModel(sa=self.sa)
200 return settings_model.get_repos_location()
200 return settings_model.get_repos_location()
201
201
202 def repo_scan(self, repos_path=None):
202 def repo_scan(self, repos_path=None):
203 """
203 """
204 Listing of repositories in given path. This path should not be a
204 Listing of repositories in given path. This path should not be a
205 repository itself. Return a dictionary of repository objects
205 repository itself. Return a dictionary of repository objects
206
206
207 :param repos_path: path to directory containing repositories
207 :param repos_path: path to directory containing repositories
208 """
208 """
209
209
210 if repos_path is None:
210 if repos_path is None:
211 repos_path = self.repos_path
211 repos_path = self.repos_path
212
212
213 log.info('scanning for repositories in %s', repos_path)
213 log.info('scanning for repositories in %s', repos_path)
214
214
215 config = make_db_config()
215 config = make_db_config()
216 config.set('extensions', 'largefiles', '')
216 config.set('extensions', 'largefiles', '')
217 repos = {}
217 repos = {}
218
218
219 for name, path in get_filesystem_repos(repos_path, recursive=True):
219 for name, path in get_filesystem_repos(repos_path, recursive=True):
220 # name need to be decomposed and put back together using the /
220 # name need to be decomposed and put back together using the /
221 # since this is internal storage separator for rhodecode
221 # since this is internal storage separator for rhodecode
222 name = Repository.normalize_repo_name(name)
222 name = Repository.normalize_repo_name(name)
223
223
224 try:
224 try:
225 if name in repos:
225 if name in repos:
226 raise RepositoryError('Duplicate repository name %s '
226 raise RepositoryError('Duplicate repository name %s '
227 'found in %s' % (name, path))
227 'found in %s' % (name, path))
228 elif path[0] in rhodecode.BACKENDS:
228 elif path[0] in rhodecode.BACKENDS:
229 klass = get_backend(path[0])
229 klass = get_backend(path[0])
230 repos[name] = klass(path[1], config=config)
230 repos[name] = klass(path[1], config=config)
231 except OSError:
231 except OSError:
232 continue
232 continue
233 log.debug('found %s paths with repositories', len(repos))
233 log.debug('found %s paths with repositories', len(repos))
234 return repos
234 return repos
235
235
236 def get_repos(self, all_repos=None, sort_key=None):
236 def get_repos(self, all_repos=None, sort_key=None):
237 """
237 """
238 Get all repositories from db and for each repo create it's
238 Get all repositories from db and for each repo create it's
239 backend instance and fill that backed with information from database
239 backend instance and fill that backed with information from database
240
240
241 :param all_repos: list of repository names as strings
241 :param all_repos: list of repository names as strings
242 give specific repositories list, good for filtering
242 give specific repositories list, good for filtering
243
243
244 :param sort_key: initial sorting of repositories
244 :param sort_key: initial sorting of repositories
245 """
245 """
246 if all_repos is None:
246 if all_repos is None:
247 all_repos = self.sa.query(Repository)\
247 all_repos = self.sa.query(Repository)\
248 .filter(Repository.group_id == None)\
248 .filter(Repository.group_id == None)\
249 .order_by(func.lower(Repository.repo_name)).all()
249 .order_by(func.lower(Repository.repo_name)).all()
250 repo_iter = SimpleCachedRepoList(
250 repo_iter = SimpleCachedRepoList(
251 all_repos, repos_path=self.repos_path, order_by=sort_key)
251 all_repos, repos_path=self.repos_path, order_by=sort_key)
252 return repo_iter
252 return repo_iter
253
253
254 def get_repo_groups(self, all_groups=None):
254 def get_repo_groups(self, all_groups=None):
255 if all_groups is None:
255 if all_groups is None:
256 all_groups = RepoGroup.query()\
256 all_groups = RepoGroup.query()\
257 .filter(RepoGroup.group_parent_id == None).all()
257 .filter(RepoGroup.group_parent_id == None).all()
258 return [x for x in RepoGroupList(all_groups)]
258 return [x for x in RepoGroupList(all_groups)]
259
259
260 def mark_for_invalidation(self, repo_name, delete=False):
260 def mark_for_invalidation(self, repo_name, delete=False):
261 """
261 """
262 Mark caches of this repo invalid in the database. `delete` flag
262 Mark caches of this repo invalid in the database. `delete` flag
263 removes the cache entries
263 removes the cache entries
264
264
265 :param repo_name: the repo_name for which caches should be marked
265 :param repo_name: the repo_name for which caches should be marked
266 invalid, or deleted
266 invalid, or deleted
267 :param delete: delete the entry keys instead of setting bool
267 :param delete: delete the entry keys instead of setting bool
268 flag on them, and also purge caches used by the dogpile
268 flag on them, and also purge caches used by the dogpile
269 """
269 """
270 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
271
271
272 if repo:
272 if repo:
273 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
273 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
274 repo_id=repo.repo_id)
274 repo_id=repo.repo_id)
275 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
275 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
276
276
277 repo_id = repo.repo_id
277 repo_id = repo.repo_id
278 config = repo._config
278 config = repo._config
279 config.set('extensions', 'largefiles', '')
279 config.set('extensions', 'largefiles', '')
280 repo.update_commit_cache(config=config, cs_cache=None)
280 repo.update_commit_cache(config=config, cs_cache=None)
281 if delete:
281 if delete:
282 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
282 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
283 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
283 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
284
284
285 def toggle_following_repo(self, follow_repo_id, user_id):
285 def toggle_following_repo(self, follow_repo_id, user_id):
286
286
287 f = self.sa.query(UserFollowing)\
287 f = self.sa.query(UserFollowing)\
288 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
288 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
289 .filter(UserFollowing.user_id == user_id).scalar()
289 .filter(UserFollowing.user_id == user_id).scalar()
290
290
291 if f is not None:
291 if f is not None:
292 try:
292 try:
293 self.sa.delete(f)
293 self.sa.delete(f)
294 return
294 return
295 except Exception:
295 except Exception:
296 log.error(traceback.format_exc())
296 log.error(traceback.format_exc())
297 raise
297 raise
298
298
299 try:
299 try:
300 f = UserFollowing()
300 f = UserFollowing()
301 f.user_id = user_id
301 f.user_id = user_id
302 f.follows_repo_id = follow_repo_id
302 f.follows_repo_id = follow_repo_id
303 self.sa.add(f)
303 self.sa.add(f)
304 except Exception:
304 except Exception:
305 log.error(traceback.format_exc())
305 log.error(traceback.format_exc())
306 raise
306 raise
307
307
308 def toggle_following_user(self, follow_user_id, user_id):
308 def toggle_following_user(self, follow_user_id, user_id):
309 f = self.sa.query(UserFollowing)\
309 f = self.sa.query(UserFollowing)\
310 .filter(UserFollowing.follows_user_id == follow_user_id)\
310 .filter(UserFollowing.follows_user_id == follow_user_id)\
311 .filter(UserFollowing.user_id == user_id).scalar()
311 .filter(UserFollowing.user_id == user_id).scalar()
312
312
313 if f is not None:
313 if f is not None:
314 try:
314 try:
315 self.sa.delete(f)
315 self.sa.delete(f)
316 return
316 return
317 except Exception:
317 except Exception:
318 log.error(traceback.format_exc())
318 log.error(traceback.format_exc())
319 raise
319 raise
320
320
321 try:
321 try:
322 f = UserFollowing()
322 f = UserFollowing()
323 f.user_id = user_id
323 f.user_id = user_id
324 f.follows_user_id = follow_user_id
324 f.follows_user_id = follow_user_id
325 self.sa.add(f)
325 self.sa.add(f)
326 except Exception:
326 except Exception:
327 log.error(traceback.format_exc())
327 log.error(traceback.format_exc())
328 raise
328 raise
329
329
330 def is_following_repo(self, repo_name, user_id, cache=False):
330 def is_following_repo(self, repo_name, user_id, cache=False):
331 r = self.sa.query(Repository)\
331 r = self.sa.query(Repository)\
332 .filter(Repository.repo_name == repo_name).scalar()
332 .filter(Repository.repo_name == repo_name).scalar()
333
333
334 f = self.sa.query(UserFollowing)\
334 f = self.sa.query(UserFollowing)\
335 .filter(UserFollowing.follows_repository == r)\
335 .filter(UserFollowing.follows_repository == r)\
336 .filter(UserFollowing.user_id == user_id).scalar()
336 .filter(UserFollowing.user_id == user_id).scalar()
337
337
338 return f is not None
338 return f is not None
339
339
340 def is_following_user(self, username, user_id, cache=False):
340 def is_following_user(self, username, user_id, cache=False):
341 u = User.get_by_username(username)
341 u = User.get_by_username(username)
342
342
343 f = self.sa.query(UserFollowing)\
343 f = self.sa.query(UserFollowing)\
344 .filter(UserFollowing.follows_user == u)\
344 .filter(UserFollowing.follows_user == u)\
345 .filter(UserFollowing.user_id == user_id).scalar()
345 .filter(UserFollowing.user_id == user_id).scalar()
346
346
347 return f is not None
347 return f is not None
348
348
349 def get_followers(self, repo):
349 def get_followers(self, repo):
350 repo = self._get_repo(repo)
350 repo = self._get_repo(repo)
351
351
352 return self.sa.query(UserFollowing)\
352 return self.sa.query(UserFollowing)\
353 .filter(UserFollowing.follows_repository == repo).count()
353 .filter(UserFollowing.follows_repository == repo).count()
354
354
355 def get_forks(self, repo):
355 def get_forks(self, repo):
356 repo = self._get_repo(repo)
356 repo = self._get_repo(repo)
357 return self.sa.query(Repository)\
357 return self.sa.query(Repository)\
358 .filter(Repository.fork == repo).count()
358 .filter(Repository.fork == repo).count()
359
359
360 def get_pull_requests(self, repo):
360 def get_pull_requests(self, repo):
361 repo = self._get_repo(repo)
361 repo = self._get_repo(repo)
362 return self.sa.query(PullRequest)\
362 return self.sa.query(PullRequest)\
363 .filter(PullRequest.target_repo == repo)\
363 .filter(PullRequest.target_repo == repo)\
364 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
364 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
365
365
366 def mark_as_fork(self, repo, fork, user):
366 def mark_as_fork(self, repo, fork, user):
367 repo = self._get_repo(repo)
367 repo = self._get_repo(repo)
368 fork = self._get_repo(fork)
368 fork = self._get_repo(fork)
369 if fork and repo.repo_id == fork.repo_id:
369 if fork and repo.repo_id == fork.repo_id:
370 raise Exception("Cannot set repository as fork of itself")
370 raise Exception("Cannot set repository as fork of itself")
371
371
372 if fork and repo.repo_type != fork.repo_type:
372 if fork and repo.repo_type != fork.repo_type:
373 raise RepositoryError(
373 raise RepositoryError(
374 "Cannot set repository as fork of repository with other type")
374 "Cannot set repository as fork of repository with other type")
375
375
376 repo.fork = fork
376 repo.fork = fork
377 self.sa.add(repo)
377 self.sa.add(repo)
378 return repo
378 return repo
379
379
380 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
380 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
381 dbrepo = self._get_repo(repo)
381 dbrepo = self._get_repo(repo)
382 remote_uri = remote_uri or dbrepo.clone_uri
382 remote_uri = remote_uri or dbrepo.clone_uri
383 if not remote_uri:
383 if not remote_uri:
384 raise Exception("This repository doesn't have a clone uri")
384 raise Exception("This repository doesn't have a clone uri")
385
385
386 repo = dbrepo.scm_instance(cache=False)
386 repo = dbrepo.scm_instance(cache=False)
387 repo.config.clear_section('hooks')
387 repo.config.clear_section('hooks')
388
388
389 try:
389 try:
390 # NOTE(marcink): add extra validation so we skip invalid urls
390 # NOTE(marcink): add extra validation so we skip invalid urls
391 # this is due this tasks can be executed via scheduler without
391 # this is due this tasks can be executed via scheduler without
392 # proper validation of remote_uri
392 # proper validation of remote_uri
393 if validate_uri:
393 if validate_uri:
394 config = make_db_config(clear_session=False)
394 config = make_db_config(clear_session=False)
395 url_validator(remote_uri, dbrepo.repo_type, config)
395 url_validator(remote_uri, dbrepo.repo_type, config)
396 except InvalidCloneUrl:
396 except InvalidCloneUrl:
397 raise
397 raise
398
398
399 repo_name = dbrepo.repo_name
399 repo_name = dbrepo.repo_name
400 try:
400 try:
401 # TODO: we need to make sure those operations call proper hooks !
401 # TODO: we need to make sure those operations call proper hooks !
402 repo.fetch(remote_uri)
402 repo.fetch(remote_uri)
403
403
404 self.mark_for_invalidation(repo_name)
404 self.mark_for_invalidation(repo_name)
405 except Exception:
405 except Exception:
406 log.error(traceback.format_exc())
406 log.error(traceback.format_exc())
407 raise
407 raise
408
408
409 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
409 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
410 dbrepo = self._get_repo(repo)
410 dbrepo = self._get_repo(repo)
411 remote_uri = remote_uri or dbrepo.push_uri
411 remote_uri = remote_uri or dbrepo.push_uri
412 if not remote_uri:
412 if not remote_uri:
413 raise Exception("This repository doesn't have a clone uri")
413 raise Exception("This repository doesn't have a clone uri")
414
414
415 repo = dbrepo.scm_instance(cache=False)
415 repo = dbrepo.scm_instance(cache=False)
416 repo.config.clear_section('hooks')
416 repo.config.clear_section('hooks')
417
417
418 try:
418 try:
419 # NOTE(marcink): add extra validation so we skip invalid urls
419 # NOTE(marcink): add extra validation so we skip invalid urls
420 # this is due this tasks can be executed via scheduler without
420 # this is due this tasks can be executed via scheduler without
421 # proper validation of remote_uri
421 # proper validation of remote_uri
422 if validate_uri:
422 if validate_uri:
423 config = make_db_config(clear_session=False)
423 config = make_db_config(clear_session=False)
424 url_validator(remote_uri, dbrepo.repo_type, config)
424 url_validator(remote_uri, dbrepo.repo_type, config)
425 except InvalidCloneUrl:
425 except InvalidCloneUrl:
426 raise
426 raise
427
427
428 try:
428 try:
429 repo.push(remote_uri)
429 repo.push(remote_uri)
430 except Exception:
430 except Exception:
431 log.error(traceback.format_exc())
431 log.error(traceback.format_exc())
432 raise
432 raise
433
433
434 def commit_change(self, repo, repo_name, commit, user, author, message,
434 def commit_change(self, repo, repo_name, commit, user, author, message,
435 content, f_path):
435 content, f_path):
436 """
436 """
437 Commits changes
437 Commits changes
438
438
439 :param repo: SCM instance
439 :param repo: SCM instance
440
440
441 """
441 """
442 user = self._get_user(user)
442 user = self._get_user(user)
443
443
444 # decoding here will force that we have proper encoded values
444 # decoding here will force that we have proper encoded values
445 # in any other case this will throw exceptions and deny commit
445 # in any other case this will throw exceptions and deny commit
446 content = safe_str(content)
446 content = safe_str(content)
447 path = safe_str(f_path)
447 path = safe_str(f_path)
448 # message and author needs to be unicode
448 # message and author needs to be unicode
449 # proper backend should then translate that into required type
449 # proper backend should then translate that into required type
450 message = safe_unicode(message)
450 message = safe_unicode(message)
451 author = safe_unicode(author)
451 author = safe_unicode(author)
452 imc = repo.in_memory_commit
452 imc = repo.in_memory_commit
453 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
453 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
454 try:
454 try:
455 # TODO: handle pre-push action !
455 # TODO: handle pre-push action !
456 tip = imc.commit(
456 tip = imc.commit(
457 message=message, author=author, parents=[commit],
457 message=message, author=author, parents=[commit],
458 branch=commit.branch)
458 branch=commit.branch)
459 except Exception as e:
459 except Exception as e:
460 log.error(traceback.format_exc())
460 log.error(traceback.format_exc())
461 raise IMCCommitError(str(e))
461 raise IMCCommitError(str(e))
462 finally:
462 finally:
463 # always clear caches, if commit fails we want fresh object also
463 # always clear caches, if commit fails we want fresh object also
464 self.mark_for_invalidation(repo_name)
464 self.mark_for_invalidation(repo_name)
465
465
466 # We trigger the post-push action
466 # We trigger the post-push action
467 hooks_utils.trigger_post_push_hook(
467 hooks_utils.trigger_post_push_hook(
468 username=user.username, action='push_local', hook_type='post_push',
468 username=user.username, action='push_local', hook_type='post_push',
469 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
469 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
470 return tip
470 return tip
471
471
472 def _sanitize_path(self, f_path):
472 def _sanitize_path(self, f_path):
473 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
473 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
474 raise NonRelativePathError('%s is not an relative path' % f_path)
474 raise NonRelativePathError('%s is not an relative path' % f_path)
475 if f_path:
475 if f_path:
476 f_path = os.path.normpath(f_path)
476 f_path = os.path.normpath(f_path)
477 return f_path
477 return f_path
478
478
479 def get_dirnode_metadata(self, request, commit, dir_node):
479 def get_dirnode_metadata(self, request, commit, dir_node):
480 if not dir_node.is_dir():
480 if not dir_node.is_dir():
481 return []
481 return []
482
482
483 data = []
483 data = []
484 for node in dir_node:
484 for node in dir_node:
485 if not node.is_file():
485 if not node.is_file():
486 # we skip file-nodes
486 # we skip file-nodes
487 continue
487 continue
488
488
489 last_commit = node.last_commit
489 last_commit = node.last_commit
490 last_commit_date = last_commit.date
490 last_commit_date = last_commit.date
491 data.append({
491 data.append({
492 'name': node.name,
492 'name': node.name,
493 'size': h.format_byte_size_binary(node.size),
493 'size': h.format_byte_size_binary(node.size),
494 'modified_at': h.format_date(last_commit_date),
494 'modified_at': h.format_date(last_commit_date),
495 'modified_ts': last_commit_date.isoformat(),
495 'modified_ts': last_commit_date.isoformat(),
496 'revision': last_commit.revision,
496 'revision': last_commit.revision,
497 'short_id': last_commit.short_id,
497 'short_id': last_commit.short_id,
498 'message': h.escape(last_commit.message),
498 'message': h.escape(last_commit.message),
499 'author': h.escape(last_commit.author),
499 'author': h.escape(last_commit.author),
500 'user_profile': h.gravatar_with_user(
500 'user_profile': h.gravatar_with_user(
501 request, last_commit.author),
501 request, last_commit.author),
502 })
502 })
503
503
504 return data
504 return data
505
505
506 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
506 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
507 extended_info=False, content=False, max_file_bytes=None):
507 extended_info=False, content=False, max_file_bytes=None):
508 """
508 """
509 recursive walk in root dir and return a set of all path in that dir
509 recursive walk in root dir and return a set of all path in that dir
510 based on repository walk function
510 based on repository walk function
511
511
512 :param repo_name: name of repository
512 :param repo_name: name of repository
513 :param commit_id: commit id for which to list nodes
513 :param commit_id: commit id for which to list nodes
514 :param root_path: root path to list
514 :param root_path: root path to list
515 :param flat: return as a list, if False returns a dict with description
515 :param flat: return as a list, if False returns a dict with description
516 :param max_file_bytes: will not return file contents over this limit
516 :param max_file_bytes: will not return file contents over this limit
517
517
518 """
518 """
519 _files = list()
519 _files = list()
520 _dirs = list()
520 _dirs = list()
521 try:
521 try:
522 _repo = self._get_repo(repo_name)
522 _repo = self._get_repo(repo_name)
523 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
523 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
524 root_path = root_path.lstrip('/')
524 root_path = root_path.lstrip('/')
525 for __, dirs, files in commit.walk(root_path):
525 for __, dirs, files in commit.walk(root_path):
526 for f in files:
526 for f in files:
527 _content = None
527 _content = None
528 _data = f.unicode_path
528 _data = f.unicode_path
529 over_size_limit = (max_file_bytes is not None
529 over_size_limit = (max_file_bytes is not None
530 and f.size > max_file_bytes)
530 and f.size > max_file_bytes)
531
531
532 if not flat:
532 if not flat:
533 _data = {
533 _data = {
534 "name": h.escape(f.unicode_path),
534 "name": h.escape(f.unicode_path),
535 "type": "file",
535 "type": "file",
536 }
536 }
537 if extended_info:
537 if extended_info:
538 _data.update({
538 _data.update({
539 "md5": f.md5,
539 "md5": f.md5,
540 "binary": f.is_binary,
540 "binary": f.is_binary,
541 "size": f.size,
541 "size": f.size,
542 "extension": f.extension,
542 "extension": f.extension,
543 "mimetype": f.mimetype,
543 "mimetype": f.mimetype,
544 "lines": f.lines()[0]
544 "lines": f.lines()[0]
545 })
545 })
546
546
547 if content:
547 if content:
548 full_content = None
548 full_content = None
549 if not f.is_binary and not over_size_limit:
549 if not f.is_binary and not over_size_limit:
550 full_content = safe_str(f.content)
550 full_content = safe_str(f.content)
551
551
552 _data.update({
552 _data.update({
553 "content": full_content,
553 "content": full_content,
554 })
554 })
555 _files.append(_data)
555 _files.append(_data)
556 for d in dirs:
556 for d in dirs:
557 _data = d.unicode_path
557 _data = d.unicode_path
558 if not flat:
558 if not flat:
559 _data = {
559 _data = {
560 "name": h.escape(d.unicode_path),
560 "name": h.escape(d.unicode_path),
561 "type": "dir",
561 "type": "dir",
562 }
562 }
563 if extended_info:
563 if extended_info:
564 _data.update({
564 _data.update({
565 "md5": None,
565 "md5": None,
566 "binary": None,
566 "binary": None,
567 "size": None,
567 "size": None,
568 "extension": None,
568 "extension": None,
569 })
569 })
570 if content:
570 if content:
571 _data.update({
571 _data.update({
572 "content": None
572 "content": None
573 })
573 })
574 _dirs.append(_data)
574 _dirs.append(_data)
575 except RepositoryError:
575 except RepositoryError:
576 log.debug("Exception in get_nodes", exc_info=True)
576 log.exception("Exception in get_nodes")
577 raise
577 raise
578
578
579 return _dirs, _files
579 return _dirs, _files
580
580
581 def get_node(self, repo_name, commit_id, file_path,
582 extended_info=False, content=False, max_file_bytes=None):
583 """
584 retrieve single node from commit
585 """
586 try:
587
588 _repo = self._get_repo(repo_name)
589 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
590
591 file_node = commit.get_node(file_path)
592 if file_node.is_dir():
593 raise RepositoryError('The given path is a directory')
594
595 _content = None
596 f_name = file_node.unicode_path
597
598 file_data = {
599 "name": h.escape(f_name),
600 "type": "file",
601 }
602
603 if extended_info:
604 file_data.update({
605 "md5": file_node.md5,
606 "binary": file_node.is_binary,
607 "size": file_node.size,
608 "extension": file_node.extension,
609 "mimetype": file_node.mimetype,
610 "lines": file_node.lines()[0]
611 })
612
613 if content:
614 over_size_limit = (max_file_bytes is not None
615 and file_node.size > max_file_bytes)
616 full_content = None
617 if not file_node.is_binary and not over_size_limit:
618 full_content = safe_str(file_node.content)
619
620 file_data.update({
621 "content": full_content,
622 })
623
624 except RepositoryError:
625 log.exception("Exception in get_node")
626 raise
627
628 return file_data
629
630 def get_fts_data(self, repo_name, commit_id, root_path='/'):
631 """
632 Fetch node tree for usage in full text search
633 """
634
635 tree_info = list()
636
637 try:
638 _repo = self._get_repo(repo_name)
639 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
640 root_path = root_path.lstrip('/')
641 for __, dirs, files in commit.walk(root_path):
642
643 for f in files:
644 _content = None
645 _data = f_name = f.unicode_path
646 is_binary, md5, size = f.metadata_uncached()
647 _data = {
648 "name": h.escape(f_name),
649 "md5": md5,
650 "extension": f.extension,
651 "binary": is_binary,
652 "size": size
653 }
654
655 tree_info.append(_data)
656
657 except RepositoryError:
658 log.exception("Exception in get_nodes")
659 raise
660
661 return tree_info
662
581 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
663 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
582 author=None, trigger_push_hook=True):
664 author=None, trigger_push_hook=True):
583 """
665 """
584 Commits given multiple nodes into repo
666 Commits given multiple nodes into repo
585
667
586 :param user: RhodeCode User object or user_id, the commiter
668 :param user: RhodeCode User object or user_id, the commiter
587 :param repo: RhodeCode Repository object
669 :param repo: RhodeCode Repository object
588 :param message: commit message
670 :param message: commit message
589 :param nodes: mapping {filename:{'content':content},...}
671 :param nodes: mapping {filename:{'content':content},...}
590 :param parent_commit: parent commit, can be empty than it's
672 :param parent_commit: parent commit, can be empty than it's
591 initial commit
673 initial commit
592 :param author: author of commit, cna be different that commiter
674 :param author: author of commit, cna be different that commiter
593 only for git
675 only for git
594 :param trigger_push_hook: trigger push hooks
676 :param trigger_push_hook: trigger push hooks
595
677
596 :returns: new commited commit
678 :returns: new commited commit
597 """
679 """
598
680
599 user = self._get_user(user)
681 user = self._get_user(user)
600 scm_instance = repo.scm_instance(cache=False)
682 scm_instance = repo.scm_instance(cache=False)
601
683
602 processed_nodes = []
684 processed_nodes = []
603 for f_path in nodes:
685 for f_path in nodes:
604 f_path = self._sanitize_path(f_path)
686 f_path = self._sanitize_path(f_path)
605 content = nodes[f_path]['content']
687 content = nodes[f_path]['content']
606 f_path = safe_str(f_path)
688 f_path = safe_str(f_path)
607 # decoding here will force that we have proper encoded values
689 # decoding here will force that we have proper encoded values
608 # in any other case this will throw exceptions and deny commit
690 # in any other case this will throw exceptions and deny commit
609 if isinstance(content, (basestring,)):
691 if isinstance(content, (basestring,)):
610 content = safe_str(content)
692 content = safe_str(content)
611 elif isinstance(content, (file, cStringIO.OutputType,)):
693 elif isinstance(content, (file, cStringIO.OutputType,)):
612 content = content.read()
694 content = content.read()
613 else:
695 else:
614 raise Exception('Content is of unrecognized type %s' % (
696 raise Exception('Content is of unrecognized type %s' % (
615 type(content)
697 type(content)
616 ))
698 ))
617 processed_nodes.append((f_path, content))
699 processed_nodes.append((f_path, content))
618
700
619 message = safe_unicode(message)
701 message = safe_unicode(message)
620 commiter = user.full_contact
702 commiter = user.full_contact
621 author = safe_unicode(author) if author else commiter
703 author = safe_unicode(author) if author else commiter
622
704
623 imc = scm_instance.in_memory_commit
705 imc = scm_instance.in_memory_commit
624
706
625 if not parent_commit:
707 if not parent_commit:
626 parent_commit = EmptyCommit(alias=scm_instance.alias)
708 parent_commit = EmptyCommit(alias=scm_instance.alias)
627
709
628 if isinstance(parent_commit, EmptyCommit):
710 if isinstance(parent_commit, EmptyCommit):
629 # EmptyCommit means we we're editing empty repository
711 # EmptyCommit means we we're editing empty repository
630 parents = None
712 parents = None
631 else:
713 else:
632 parents = [parent_commit]
714 parents = [parent_commit]
633 # add multiple nodes
715 # add multiple nodes
634 for path, content in processed_nodes:
716 for path, content in processed_nodes:
635 imc.add(FileNode(path, content=content))
717 imc.add(FileNode(path, content=content))
636 # TODO: handle pre push scenario
718 # TODO: handle pre push scenario
637 tip = imc.commit(message=message,
719 tip = imc.commit(message=message,
638 author=author,
720 author=author,
639 parents=parents,
721 parents=parents,
640 branch=parent_commit.branch)
722 branch=parent_commit.branch)
641
723
642 self.mark_for_invalidation(repo.repo_name)
724 self.mark_for_invalidation(repo.repo_name)
643 if trigger_push_hook:
725 if trigger_push_hook:
644 hooks_utils.trigger_post_push_hook(
726 hooks_utils.trigger_post_push_hook(
645 username=user.username, action='push_local',
727 username=user.username, action='push_local',
646 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
728 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
647 hook_type='post_push',
729 hook_type='post_push',
648 commit_ids=[tip.raw_id])
730 commit_ids=[tip.raw_id])
649 return tip
731 return tip
650
732
651 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
733 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
652 author=None, trigger_push_hook=True):
734 author=None, trigger_push_hook=True):
653 user = self._get_user(user)
735 user = self._get_user(user)
654 scm_instance = repo.scm_instance(cache=False)
736 scm_instance = repo.scm_instance(cache=False)
655
737
656 message = safe_unicode(message)
738 message = safe_unicode(message)
657 commiter = user.full_contact
739 commiter = user.full_contact
658 author = safe_unicode(author) if author else commiter
740 author = safe_unicode(author) if author else commiter
659
741
660 imc = scm_instance.in_memory_commit
742 imc = scm_instance.in_memory_commit
661
743
662 if not parent_commit:
744 if not parent_commit:
663 parent_commit = EmptyCommit(alias=scm_instance.alias)
745 parent_commit = EmptyCommit(alias=scm_instance.alias)
664
746
665 if isinstance(parent_commit, EmptyCommit):
747 if isinstance(parent_commit, EmptyCommit):
666 # EmptyCommit means we we're editing empty repository
748 # EmptyCommit means we we're editing empty repository
667 parents = None
749 parents = None
668 else:
750 else:
669 parents = [parent_commit]
751 parents = [parent_commit]
670
752
671 # add multiple nodes
753 # add multiple nodes
672 for _filename, data in nodes.items():
754 for _filename, data in nodes.items():
673 # new filename, can be renamed from the old one, also sanitaze
755 # new filename, can be renamed from the old one, also sanitaze
674 # the path for any hack around relative paths like ../../ etc.
756 # the path for any hack around relative paths like ../../ etc.
675 filename = self._sanitize_path(data['filename'])
757 filename = self._sanitize_path(data['filename'])
676 old_filename = self._sanitize_path(_filename)
758 old_filename = self._sanitize_path(_filename)
677 content = data['content']
759 content = data['content']
678 file_mode = data.get('mode')
760 file_mode = data.get('mode')
679 filenode = FileNode(old_filename, content=content, mode=file_mode)
761 filenode = FileNode(old_filename, content=content, mode=file_mode)
680 op = data['op']
762 op = data['op']
681 if op == 'add':
763 if op == 'add':
682 imc.add(filenode)
764 imc.add(filenode)
683 elif op == 'del':
765 elif op == 'del':
684 imc.remove(filenode)
766 imc.remove(filenode)
685 elif op == 'mod':
767 elif op == 'mod':
686 if filename != old_filename:
768 if filename != old_filename:
687 # TODO: handle renames more efficient, needs vcs lib changes
769 # TODO: handle renames more efficient, needs vcs lib changes
688 imc.remove(filenode)
770 imc.remove(filenode)
689 imc.add(FileNode(filename, content=content, mode=file_mode))
771 imc.add(FileNode(filename, content=content, mode=file_mode))
690 else:
772 else:
691 imc.change(filenode)
773 imc.change(filenode)
692
774
693 try:
775 try:
694 # TODO: handle pre push scenario commit changes
776 # TODO: handle pre push scenario commit changes
695 tip = imc.commit(message=message,
777 tip = imc.commit(message=message,
696 author=author,
778 author=author,
697 parents=parents,
779 parents=parents,
698 branch=parent_commit.branch)
780 branch=parent_commit.branch)
699 except NodeNotChangedError:
781 except NodeNotChangedError:
700 raise
782 raise
701 except Exception as e:
783 except Exception as e:
702 log.exception("Unexpected exception during call to imc.commit")
784 log.exception("Unexpected exception during call to imc.commit")
703 raise IMCCommitError(str(e))
785 raise IMCCommitError(str(e))
704 finally:
786 finally:
705 # always clear caches, if commit fails we want fresh object also
787 # always clear caches, if commit fails we want fresh object also
706 self.mark_for_invalidation(repo.repo_name)
788 self.mark_for_invalidation(repo.repo_name)
707
789
708 if trigger_push_hook:
790 if trigger_push_hook:
709 hooks_utils.trigger_post_push_hook(
791 hooks_utils.trigger_post_push_hook(
710 username=user.username, action='push_local', hook_type='post_push',
792 username=user.username, action='push_local', hook_type='post_push',
711 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
793 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
712 commit_ids=[tip.raw_id])
794 commit_ids=[tip.raw_id])
713
795
714 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
796 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
715 author=None, trigger_push_hook=True):
797 author=None, trigger_push_hook=True):
716 """
798 """
717 Deletes given multiple nodes into `repo`
799 Deletes given multiple nodes into `repo`
718
800
719 :param user: RhodeCode User object or user_id, the committer
801 :param user: RhodeCode User object or user_id, the committer
720 :param repo: RhodeCode Repository object
802 :param repo: RhodeCode Repository object
721 :param message: commit message
803 :param message: commit message
722 :param nodes: mapping {filename:{'content':content},...}
804 :param nodes: mapping {filename:{'content':content},...}
723 :param parent_commit: parent commit, can be empty than it's initial
805 :param parent_commit: parent commit, can be empty than it's initial
724 commit
806 commit
725 :param author: author of commit, cna be different that commiter only
807 :param author: author of commit, cna be different that commiter only
726 for git
808 for git
727 :param trigger_push_hook: trigger push hooks
809 :param trigger_push_hook: trigger push hooks
728
810
729 :returns: new commit after deletion
811 :returns: new commit after deletion
730 """
812 """
731
813
732 user = self._get_user(user)
814 user = self._get_user(user)
733 scm_instance = repo.scm_instance(cache=False)
815 scm_instance = repo.scm_instance(cache=False)
734
816
735 processed_nodes = []
817 processed_nodes = []
736 for f_path in nodes:
818 for f_path in nodes:
737 f_path = self._sanitize_path(f_path)
819 f_path = self._sanitize_path(f_path)
738 # content can be empty but for compatabilty it allows same dicts
820 # content can be empty but for compatabilty it allows same dicts
739 # structure as add_nodes
821 # structure as add_nodes
740 content = nodes[f_path].get('content')
822 content = nodes[f_path].get('content')
741 processed_nodes.append((f_path, content))
823 processed_nodes.append((f_path, content))
742
824
743 message = safe_unicode(message)
825 message = safe_unicode(message)
744 commiter = user.full_contact
826 commiter = user.full_contact
745 author = safe_unicode(author) if author else commiter
827 author = safe_unicode(author) if author else commiter
746
828
747 imc = scm_instance.in_memory_commit
829 imc = scm_instance.in_memory_commit
748
830
749 if not parent_commit:
831 if not parent_commit:
750 parent_commit = EmptyCommit(alias=scm_instance.alias)
832 parent_commit = EmptyCommit(alias=scm_instance.alias)
751
833
752 if isinstance(parent_commit, EmptyCommit):
834 if isinstance(parent_commit, EmptyCommit):
753 # EmptyCommit means we we're editing empty repository
835 # EmptyCommit means we we're editing empty repository
754 parents = None
836 parents = None
755 else:
837 else:
756 parents = [parent_commit]
838 parents = [parent_commit]
757 # add multiple nodes
839 # add multiple nodes
758 for path, content in processed_nodes:
840 for path, content in processed_nodes:
759 imc.remove(FileNode(path, content=content))
841 imc.remove(FileNode(path, content=content))
760
842
761 # TODO: handle pre push scenario
843 # TODO: handle pre push scenario
762 tip = imc.commit(message=message,
844 tip = imc.commit(message=message,
763 author=author,
845 author=author,
764 parents=parents,
846 parents=parents,
765 branch=parent_commit.branch)
847 branch=parent_commit.branch)
766
848
767 self.mark_for_invalidation(repo.repo_name)
849 self.mark_for_invalidation(repo.repo_name)
768 if trigger_push_hook:
850 if trigger_push_hook:
769 hooks_utils.trigger_post_push_hook(
851 hooks_utils.trigger_post_push_hook(
770 username=user.username, action='push_local', hook_type='post_push',
852 username=user.username, action='push_local', hook_type='post_push',
771 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
853 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
772 commit_ids=[tip.raw_id])
854 commit_ids=[tip.raw_id])
773 return tip
855 return tip
774
856
775 def strip(self, repo, commit_id, branch):
857 def strip(self, repo, commit_id, branch):
776 scm_instance = repo.scm_instance(cache=False)
858 scm_instance = repo.scm_instance(cache=False)
777 scm_instance.config.clear_section('hooks')
859 scm_instance.config.clear_section('hooks')
778 scm_instance.strip(commit_id, branch)
860 scm_instance.strip(commit_id, branch)
779 self.mark_for_invalidation(repo.repo_name)
861 self.mark_for_invalidation(repo.repo_name)
780
862
781 def get_unread_journal(self):
863 def get_unread_journal(self):
782 return self.sa.query(UserLog).count()
864 return self.sa.query(UserLog).count()
783
865
784 def get_repo_landing_revs(self, translator, repo=None):
866 def get_repo_landing_revs(self, translator, repo=None):
785 """
867 """
786 Generates select option with tags branches and bookmarks (for hg only)
868 Generates select option with tags branches and bookmarks (for hg only)
787 grouped by type
869 grouped by type
788
870
789 :param repo:
871 :param repo:
790 """
872 """
791 _ = translator
873 _ = translator
792 repo = self._get_repo(repo)
874 repo = self._get_repo(repo)
793
875
794 hist_l = [
876 hist_l = [
795 ['rev:tip', _('latest tip')]
877 ['rev:tip', _('latest tip')]
796 ]
878 ]
797 choices = [
879 choices = [
798 'rev:tip'
880 'rev:tip'
799 ]
881 ]
800
882
801 if not repo:
883 if not repo:
802 return choices, hist_l
884 return choices, hist_l
803
885
804 repo = repo.scm_instance()
886 repo = repo.scm_instance()
805
887
806 branches_group = (
888 branches_group = (
807 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
889 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
808 for b in repo.branches],
890 for b in repo.branches],
809 _("Branches"))
891 _("Branches"))
810 hist_l.append(branches_group)
892 hist_l.append(branches_group)
811 choices.extend([x[0] for x in branches_group[0]])
893 choices.extend([x[0] for x in branches_group[0]])
812
894
813 if repo.alias == 'hg':
895 if repo.alias == 'hg':
814 bookmarks_group = (
896 bookmarks_group = (
815 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
897 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
816 for b in repo.bookmarks],
898 for b in repo.bookmarks],
817 _("Bookmarks"))
899 _("Bookmarks"))
818 hist_l.append(bookmarks_group)
900 hist_l.append(bookmarks_group)
819 choices.extend([x[0] for x in bookmarks_group[0]])
901 choices.extend([x[0] for x in bookmarks_group[0]])
820
902
821 tags_group = (
903 tags_group = (
822 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
904 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
823 for t in repo.tags],
905 for t in repo.tags],
824 _("Tags"))
906 _("Tags"))
825 hist_l.append(tags_group)
907 hist_l.append(tags_group)
826 choices.extend([x[0] for x in tags_group[0]])
908 choices.extend([x[0] for x in tags_group[0]])
827
909
828 return choices, hist_l
910 return choices, hist_l
829
911
830 def get_server_info(self, environ=None):
912 def get_server_info(self, environ=None):
831 server_info = get_system_info(environ)
913 server_info = get_system_info(environ)
832 return server_info
914 return server_info
General Comments 0
You need to be logged in to leave comments. Login now