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