##// END OF EJS Templates
flake8: fix E125 continuation line with same indent as next logical line
Mads Kiilerich -
r7733:f73a1103 default
parent child Browse files
Show More
@@ -1,2423 +1,2423 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.api.api
15 kallithea.controllers.api.api
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 API controller for Kallithea
18 API controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Aug 20, 2011
22 :created_on: Aug 20, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 from datetime import datetime
30 from datetime import datetime
31
31
32 from tg import request
32 from tg import request
33
33
34 from kallithea.controllers.api import JSONRPCController, JSONRPCError
34 from kallithea.controllers.api import JSONRPCController, JSONRPCError
35 from kallithea.lib.auth import (
35 from kallithea.lib.auth import (
36 AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, HasUserGroupPermissionLevel)
36 AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, HasUserGroupPermissionLevel)
37 from kallithea.lib.exceptions import DefaultUserException, UserGroupsAssignedException
37 from kallithea.lib.exceptions import DefaultUserException, UserGroupsAssignedException
38 from kallithea.lib.utils import action_logger, repo2db_mapper
38 from kallithea.lib.utils import action_logger, repo2db_mapper
39 from kallithea.lib.utils2 import OAttr, Optional
39 from kallithea.lib.utils2 import OAttr, Optional
40 from kallithea.lib.vcs.backends.base import EmptyChangeset
40 from kallithea.lib.vcs.backends.base import EmptyChangeset
41 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
41 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
42 from kallithea.model.changeset_status import ChangesetStatusModel
42 from kallithea.model.changeset_status import ChangesetStatusModel
43 from kallithea.model.comment import ChangesetCommentsModel
43 from kallithea.model.comment import ChangesetCommentsModel
44 from kallithea.model.db import ChangesetStatus, Gist, Permission, PullRequest, RepoGroup, Repository, Setting, User, UserGroup, UserIpMap
44 from kallithea.model.db import ChangesetStatus, Gist, Permission, PullRequest, RepoGroup, Repository, Setting, User, UserGroup, UserIpMap
45 from kallithea.model.gist import GistModel
45 from kallithea.model.gist import GistModel
46 from kallithea.model.meta import Session
46 from kallithea.model.meta import Session
47 from kallithea.model.pull_request import PullRequestModel
47 from kallithea.model.pull_request import PullRequestModel
48 from kallithea.model.repo import RepoModel
48 from kallithea.model.repo import RepoModel
49 from kallithea.model.repo_group import RepoGroupModel
49 from kallithea.model.repo_group import RepoGroupModel
50 from kallithea.model.scm import ScmModel, UserGroupList
50 from kallithea.model.scm import ScmModel, UserGroupList
51 from kallithea.model.user import UserModel
51 from kallithea.model.user import UserModel
52 from kallithea.model.user_group import UserGroupModel
52 from kallithea.model.user_group import UserGroupModel
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def store_update(updates, attr, name):
58 def store_update(updates, attr, name):
59 """
59 """
60 Stores param in updates dict if it's not instance of Optional
60 Stores param in updates dict if it's not instance of Optional
61 allows easy updates of passed in params
61 allows easy updates of passed in params
62 """
62 """
63 if not isinstance(attr, Optional):
63 if not isinstance(attr, Optional):
64 updates[name] = attr
64 updates[name] = attr
65
65
66
66
67 def get_user_or_error(userid):
67 def get_user_or_error(userid):
68 """
68 """
69 Get user by id or name or return JsonRPCError if not found
69 Get user by id or name or return JsonRPCError if not found
70
70
71 :param userid:
71 :param userid:
72 """
72 """
73 user = UserModel().get_user(userid)
73 user = UserModel().get_user(userid)
74 if user is None:
74 if user is None:
75 raise JSONRPCError("user `%s` does not exist" % (userid,))
75 raise JSONRPCError("user `%s` does not exist" % (userid,))
76 return user
76 return user
77
77
78
78
79 def get_repo_or_error(repoid):
79 def get_repo_or_error(repoid):
80 """
80 """
81 Get repo by id or name or return JsonRPCError if not found
81 Get repo by id or name or return JsonRPCError if not found
82
82
83 :param repoid:
83 :param repoid:
84 """
84 """
85 repo = RepoModel().get_repo(repoid)
85 repo = RepoModel().get_repo(repoid)
86 if repo is None:
86 if repo is None:
87 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
87 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
88 return repo
88 return repo
89
89
90
90
91 def get_repo_group_or_error(repogroupid):
91 def get_repo_group_or_error(repogroupid):
92 """
92 """
93 Get repo group by id or name or return JsonRPCError if not found
93 Get repo group by id or name or return JsonRPCError if not found
94
94
95 :param repogroupid:
95 :param repogroupid:
96 """
96 """
97 repo_group = RepoGroup.guess_instance(repogroupid)
97 repo_group = RepoGroup.guess_instance(repogroupid)
98 if repo_group is None:
98 if repo_group is None:
99 raise JSONRPCError(
99 raise JSONRPCError(
100 'repository group `%s` does not exist' % (repogroupid,))
100 'repository group `%s` does not exist' % (repogroupid,))
101 return repo_group
101 return repo_group
102
102
103
103
104 def get_user_group_or_error(usergroupid):
104 def get_user_group_or_error(usergroupid):
105 """
105 """
106 Get user group by id or name or return JsonRPCError if not found
106 Get user group by id or name or return JsonRPCError if not found
107
107
108 :param usergroupid:
108 :param usergroupid:
109 """
109 """
110 user_group = UserGroupModel().get_group(usergroupid)
110 user_group = UserGroupModel().get_group(usergroupid)
111 if user_group is None:
111 if user_group is None:
112 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
112 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
113 return user_group
113 return user_group
114
114
115
115
116 def get_perm_or_error(permid, prefix=None):
116 def get_perm_or_error(permid, prefix=None):
117 """
117 """
118 Get permission by id or name or return JsonRPCError if not found
118 Get permission by id or name or return JsonRPCError if not found
119
119
120 :param permid:
120 :param permid:
121 """
121 """
122 perm = Permission.get_by_key(permid)
122 perm = Permission.get_by_key(permid)
123 if perm is None:
123 if perm is None:
124 raise JSONRPCError('permission `%s` does not exist' % (permid,))
124 raise JSONRPCError('permission `%s` does not exist' % (permid,))
125 if prefix:
125 if prefix:
126 if not perm.permission_name.startswith(prefix):
126 if not perm.permission_name.startswith(prefix):
127 raise JSONRPCError('permission `%s` is invalid, '
127 raise JSONRPCError('permission `%s` is invalid, '
128 'should start with %s' % (permid, prefix))
128 'should start with %s' % (permid, prefix))
129 return perm
129 return perm
130
130
131
131
132 def get_gist_or_error(gistid):
132 def get_gist_or_error(gistid):
133 """
133 """
134 Get gist by id or gist_access_id or return JsonRPCError if not found
134 Get gist by id or gist_access_id or return JsonRPCError if not found
135
135
136 :param gistid:
136 :param gistid:
137 """
137 """
138 gist = GistModel().get_gist(gistid)
138 gist = GistModel().get_gist(gistid)
139 if gist is None:
139 if gist is None:
140 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
140 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
141 return gist
141 return gist
142
142
143
143
144 class ApiController(JSONRPCController):
144 class ApiController(JSONRPCController):
145 """
145 """
146 API Controller
146 API Controller
147
147
148 The authenticated user can be found as request.authuser.
148 The authenticated user can be found as request.authuser.
149
149
150 Example function::
150 Example function::
151
151
152 def func(arg1, arg2,...):
152 def func(arg1, arg2,...):
153 pass
153 pass
154
154
155 Each function should also **raise** JSONRPCError for any
155 Each function should also **raise** JSONRPCError for any
156 errors that happens.
156 errors that happens.
157 """
157 """
158
158
159 @HasPermissionAnyDecorator('hg.admin')
159 @HasPermissionAnyDecorator('hg.admin')
160 def test(self, args):
160 def test(self, args):
161 return args
161 return args
162
162
163 @HasPermissionAnyDecorator('hg.admin')
163 @HasPermissionAnyDecorator('hg.admin')
164 def pull(self, repoid, clone_uri=Optional(None)):
164 def pull(self, repoid, clone_uri=Optional(None)):
165 """
165 """
166 Triggers a pull from remote location on given repo. Can be used to
166 Triggers a pull from remote location on given repo. Can be used to
167 automatically keep remote repos up to date. This command can be executed
167 automatically keep remote repos up to date. This command can be executed
168 only using api_key belonging to user with admin rights
168 only using api_key belonging to user with admin rights
169
169
170 :param repoid: repository name or repository id
170 :param repoid: repository name or repository id
171 :type repoid: str or int
171 :type repoid: str or int
172 :param clone_uri: repository URI to pull from (optional)
172 :param clone_uri: repository URI to pull from (optional)
173 :type clone_uri: str
173 :type clone_uri: str
174
174
175 OUTPUT::
175 OUTPUT::
176
176
177 id : <id_given_in_input>
177 id : <id_given_in_input>
178 result : {
178 result : {
179 "msg": "Pulled from `<repository name>`"
179 "msg": "Pulled from `<repository name>`"
180 "repository": "<repository name>"
180 "repository": "<repository name>"
181 }
181 }
182 error : null
182 error : null
183
183
184 ERROR OUTPUT::
184 ERROR OUTPUT::
185
185
186 id : <id_given_in_input>
186 id : <id_given_in_input>
187 result : null
187 result : null
188 error : {
188 error : {
189 "Unable to pull changes from `<reponame>`"
189 "Unable to pull changes from `<reponame>`"
190 }
190 }
191
191
192 """
192 """
193
193
194 repo = get_repo_or_error(repoid)
194 repo = get_repo_or_error(repoid)
195
195
196 try:
196 try:
197 ScmModel().pull_changes(repo.repo_name,
197 ScmModel().pull_changes(repo.repo_name,
198 request.authuser.username,
198 request.authuser.username,
199 request.ip_addr,
199 request.ip_addr,
200 clone_uri=Optional.extract(clone_uri))
200 clone_uri=Optional.extract(clone_uri))
201 return dict(
201 return dict(
202 msg='Pulled from `%s`' % repo.repo_name,
202 msg='Pulled from `%s`' % repo.repo_name,
203 repository=repo.repo_name
203 repository=repo.repo_name
204 )
204 )
205 except Exception:
205 except Exception:
206 log.error(traceback.format_exc())
206 log.error(traceback.format_exc())
207 raise JSONRPCError(
207 raise JSONRPCError(
208 'Unable to pull changes from `%s`' % repo.repo_name
208 'Unable to pull changes from `%s`' % repo.repo_name
209 )
209 )
210
210
211 @HasPermissionAnyDecorator('hg.admin')
211 @HasPermissionAnyDecorator('hg.admin')
212 def rescan_repos(self, remove_obsolete=Optional(False)):
212 def rescan_repos(self, remove_obsolete=Optional(False)):
213 """
213 """
214 Triggers rescan repositories action. If remove_obsolete is set
214 Triggers rescan repositories action. If remove_obsolete is set
215 than also delete repos that are in database but not in the filesystem.
215 than also delete repos that are in database but not in the filesystem.
216 aka "clean zombies". This command can be executed only using api_key
216 aka "clean zombies". This command can be executed only using api_key
217 belonging to user with admin rights.
217 belonging to user with admin rights.
218
218
219 :param remove_obsolete: deletes repositories from
219 :param remove_obsolete: deletes repositories from
220 database that are not found on the filesystem
220 database that are not found on the filesystem
221 :type remove_obsolete: Optional(bool)
221 :type remove_obsolete: Optional(bool)
222
222
223 OUTPUT::
223 OUTPUT::
224
224
225 id : <id_given_in_input>
225 id : <id_given_in_input>
226 result : {
226 result : {
227 'added': [<added repository name>,...]
227 'added': [<added repository name>,...]
228 'removed': [<removed repository name>,...]
228 'removed': [<removed repository name>,...]
229 }
229 }
230 error : null
230 error : null
231
231
232 ERROR OUTPUT::
232 ERROR OUTPUT::
233
233
234 id : <id_given_in_input>
234 id : <id_given_in_input>
235 result : null
235 result : null
236 error : {
236 error : {
237 'Error occurred during rescan repositories action'
237 'Error occurred during rescan repositories action'
238 }
238 }
239
239
240 """
240 """
241
241
242 try:
242 try:
243 rm_obsolete = Optional.extract(remove_obsolete)
243 rm_obsolete = Optional.extract(remove_obsolete)
244 added, removed = repo2db_mapper(ScmModel().repo_scan(),
244 added, removed = repo2db_mapper(ScmModel().repo_scan(),
245 remove_obsolete=rm_obsolete)
245 remove_obsolete=rm_obsolete)
246 return {'added': added, 'removed': removed}
246 return {'added': added, 'removed': removed}
247 except Exception:
247 except Exception:
248 log.error(traceback.format_exc())
248 log.error(traceback.format_exc())
249 raise JSONRPCError(
249 raise JSONRPCError(
250 'Error occurred during rescan repositories action'
250 'Error occurred during rescan repositories action'
251 )
251 )
252
252
253 def invalidate_cache(self, repoid):
253 def invalidate_cache(self, repoid):
254 """
254 """
255 Invalidate cache for repository.
255 Invalidate cache for repository.
256 This command can be executed only using api_key belonging to user with admin
256 This command can be executed only using api_key belonging to user with admin
257 rights or regular user that have write or admin or write access to repository.
257 rights or regular user that have write or admin or write access to repository.
258
258
259 :param repoid: repository name or repository id
259 :param repoid: repository name or repository id
260 :type repoid: str or int
260 :type repoid: str or int
261
261
262 OUTPUT::
262 OUTPUT::
263
263
264 id : <id_given_in_input>
264 id : <id_given_in_input>
265 result : {
265 result : {
266 'msg': Cache for repository `<repository name>` was invalidated,
266 'msg': Cache for repository `<repository name>` was invalidated,
267 'repository': <repository name>
267 'repository': <repository name>
268 }
268 }
269 error : null
269 error : null
270
270
271 ERROR OUTPUT::
271 ERROR OUTPUT::
272
272
273 id : <id_given_in_input>
273 id : <id_given_in_input>
274 result : null
274 result : null
275 error : {
275 error : {
276 'Error occurred during cache invalidation action'
276 'Error occurred during cache invalidation action'
277 }
277 }
278
278
279 """
279 """
280 repo = get_repo_or_error(repoid)
280 repo = get_repo_or_error(repoid)
281 if not HasPermissionAny('hg.admin')():
281 if not HasPermissionAny('hg.admin')():
282 if not HasRepoPermissionLevel('write')(repo.repo_name):
282 if not HasRepoPermissionLevel('write')(repo.repo_name):
283 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
283 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
284
284
285 try:
285 try:
286 ScmModel().mark_for_invalidation(repo.repo_name)
286 ScmModel().mark_for_invalidation(repo.repo_name)
287 return dict(
287 return dict(
288 msg='Cache for repository `%s` was invalidated' % (repoid,),
288 msg='Cache for repository `%s` was invalidated' % (repoid,),
289 repository=repo.repo_name
289 repository=repo.repo_name
290 )
290 )
291 except Exception:
291 except Exception:
292 log.error(traceback.format_exc())
292 log.error(traceback.format_exc())
293 raise JSONRPCError(
293 raise JSONRPCError(
294 'Error occurred during cache invalidation action'
294 'Error occurred during cache invalidation action'
295 )
295 )
296
296
297 @HasPermissionAnyDecorator('hg.admin')
297 @HasPermissionAnyDecorator('hg.admin')
298 def get_ip(self, userid=Optional(OAttr('apiuser'))):
298 def get_ip(self, userid=Optional(OAttr('apiuser'))):
299 """
299 """
300 Shows IP address as seen from Kallithea server, together with all
300 Shows IP address as seen from Kallithea server, together with all
301 defined IP addresses for given user. If userid is not passed data is
301 defined IP addresses for given user. If userid is not passed data is
302 returned for user who's calling this function.
302 returned for user who's calling this function.
303 This command can be executed only using api_key belonging to user with
303 This command can be executed only using api_key belonging to user with
304 admin rights.
304 admin rights.
305
305
306 :param userid: username to show ips for
306 :param userid: username to show ips for
307 :type userid: Optional(str or int)
307 :type userid: Optional(str or int)
308
308
309 OUTPUT::
309 OUTPUT::
310
310
311 id : <id_given_in_input>
311 id : <id_given_in_input>
312 result : {
312 result : {
313 "server_ip_addr": "<ip_from_clien>",
313 "server_ip_addr": "<ip_from_clien>",
314 "user_ips": [
314 "user_ips": [
315 {
315 {
316 "ip_addr": "<ip_with_mask>",
316 "ip_addr": "<ip_with_mask>",
317 "ip_range": ["<start_ip>", "<end_ip>"],
317 "ip_range": ["<start_ip>", "<end_ip>"],
318 },
318 },
319 ...
319 ...
320 ]
320 ]
321 }
321 }
322
322
323 """
323 """
324 if isinstance(userid, Optional):
324 if isinstance(userid, Optional):
325 userid = request.authuser.user_id
325 userid = request.authuser.user_id
326 user = get_user_or_error(userid)
326 user = get_user_or_error(userid)
327 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
327 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
328 return dict(
328 return dict(
329 server_ip_addr=request.ip_addr,
329 server_ip_addr=request.ip_addr,
330 user_ips=ips
330 user_ips=ips
331 )
331 )
332
332
333 # alias for old
333 # alias for old
334 show_ip = get_ip
334 show_ip = get_ip
335
335
336 @HasPermissionAnyDecorator('hg.admin')
336 @HasPermissionAnyDecorator('hg.admin')
337 def get_server_info(self):
337 def get_server_info(self):
338 """
338 """
339 return server info, including Kallithea version and installed packages
339 return server info, including Kallithea version and installed packages
340
340
341
341
342 OUTPUT::
342 OUTPUT::
343
343
344 id : <id_given_in_input>
344 id : <id_given_in_input>
345 result : {
345 result : {
346 'modules': [<module name>,...]
346 'modules': [<module name>,...]
347 'py_version': <python version>,
347 'py_version': <python version>,
348 'platform': <platform type>,
348 'platform': <platform type>,
349 'kallithea_version': <kallithea version>
349 'kallithea_version': <kallithea version>
350 }
350 }
351 error : null
351 error : null
352 """
352 """
353 return Setting.get_server_info()
353 return Setting.get_server_info()
354
354
355 def get_user(self, userid=Optional(OAttr('apiuser'))):
355 def get_user(self, userid=Optional(OAttr('apiuser'))):
356 """
356 """
357 Gets a user by username or user_id, Returns empty result if user is
357 Gets a user by username or user_id, Returns empty result if user is
358 not found. If userid param is skipped it is set to id of user who is
358 not found. If userid param is skipped it is set to id of user who is
359 calling this method. This command can be executed only using api_key
359 calling this method. This command can be executed only using api_key
360 belonging to user with admin rights, or regular users that cannot
360 belonging to user with admin rights, or regular users that cannot
361 specify different userid than theirs
361 specify different userid than theirs
362
362
363 :param userid: user to get data for
363 :param userid: user to get data for
364 :type userid: Optional(str or int)
364 :type userid: Optional(str or int)
365
365
366 OUTPUT::
366 OUTPUT::
367
367
368 id : <id_given_in_input>
368 id : <id_given_in_input>
369 result: None if user does not exist or
369 result: None if user does not exist or
370 {
370 {
371 "user_id" : "<user_id>",
371 "user_id" : "<user_id>",
372 "api_key" : "<api_key>",
372 "api_key" : "<api_key>",
373 "api_keys": "[<list of all API keys including additional ones>]"
373 "api_keys": "[<list of all API keys including additional ones>]"
374 "username" : "<username>",
374 "username" : "<username>",
375 "firstname": "<firstname>",
375 "firstname": "<firstname>",
376 "lastname" : "<lastname>",
376 "lastname" : "<lastname>",
377 "email" : "<email>",
377 "email" : "<email>",
378 "emails": "[<list of all emails including additional ones>]",
378 "emails": "[<list of all emails including additional ones>]",
379 "ip_addresses": "[<ip_address_for_user>,...]",
379 "ip_addresses": "[<ip_address_for_user>,...]",
380 "active" : "<bool: user active>",
380 "active" : "<bool: user active>",
381 "admin" :Β  "<bool: user is admin>",
381 "admin" :Β  "<bool: user is admin>",
382 "extern_name" : "<extern_name>",
382 "extern_name" : "<extern_name>",
383 "extern_type" : "<extern type>
383 "extern_type" : "<extern type>
384 "last_login": "<last_login>",
384 "last_login": "<last_login>",
385 "permissions": {
385 "permissions": {
386 "global": ["hg.create.repository",
386 "global": ["hg.create.repository",
387 "repository.read",
387 "repository.read",
388 "hg.register.manual_activate"],
388 "hg.register.manual_activate"],
389 "repositories": {"repo1": "repository.none"},
389 "repositories": {"repo1": "repository.none"},
390 "repositories_groups": {"Group1": "group.read"}
390 "repositories_groups": {"Group1": "group.read"}
391 },
391 },
392 }
392 }
393
393
394 error: null
394 error: null
395
395
396 """
396 """
397 if not HasPermissionAny('hg.admin')():
397 if not HasPermissionAny('hg.admin')():
398 # make sure normal user does not pass someone else userid,
398 # make sure normal user does not pass someone else userid,
399 # he is not allowed to do that
399 # he is not allowed to do that
400 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
400 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
401 raise JSONRPCError(
401 raise JSONRPCError(
402 'userid is not the same as your user'
402 'userid is not the same as your user'
403 )
403 )
404
404
405 if isinstance(userid, Optional):
405 if isinstance(userid, Optional):
406 userid = request.authuser.user_id
406 userid = request.authuser.user_id
407
407
408 user = get_user_or_error(userid)
408 user = get_user_or_error(userid)
409 data = user.get_api_data()
409 data = user.get_api_data()
410 data['permissions'] = AuthUser(user_id=user.user_id).permissions
410 data['permissions'] = AuthUser(user_id=user.user_id).permissions
411 return data
411 return data
412
412
413 @HasPermissionAnyDecorator('hg.admin')
413 @HasPermissionAnyDecorator('hg.admin')
414 def get_users(self):
414 def get_users(self):
415 """
415 """
416 Lists all existing users. This command can be executed only using api_key
416 Lists all existing users. This command can be executed only using api_key
417 belonging to user with admin rights.
417 belonging to user with admin rights.
418
418
419
419
420 OUTPUT::
420 OUTPUT::
421
421
422 id : <id_given_in_input>
422 id : <id_given_in_input>
423 result: [<user_object>, ...]
423 result: [<user_object>, ...]
424 error: null
424 error: null
425 """
425 """
426
426
427 return [
427 return [
428 user.get_api_data()
428 user.get_api_data()
429 for user in User.query()
429 for user in User.query()
430 .order_by(User.username)
430 .order_by(User.username)
431 .filter_by(is_default_user=False)
431 .filter_by(is_default_user=False)
432 ]
432 ]
433
433
434 @HasPermissionAnyDecorator('hg.admin')
434 @HasPermissionAnyDecorator('hg.admin')
435 def create_user(self, username, email, password=Optional(''),
435 def create_user(self, username, email, password=Optional(''),
436 firstname=Optional(u''), lastname=Optional(u''),
436 firstname=Optional(u''), lastname=Optional(u''),
437 active=Optional(True), admin=Optional(False),
437 active=Optional(True), admin=Optional(False),
438 extern_type=Optional(User.DEFAULT_AUTH_TYPE),
438 extern_type=Optional(User.DEFAULT_AUTH_TYPE),
439 extern_name=Optional('')):
439 extern_name=Optional('')):
440 """
440 """
441 Creates new user. Returns new user object. This command can
441 Creates new user. Returns new user object. This command can
442 be executed only using api_key belonging to user with admin rights.
442 be executed only using api_key belonging to user with admin rights.
443
443
444 :param username: new username
444 :param username: new username
445 :type username: str or int
445 :type username: str or int
446 :param email: email
446 :param email: email
447 :type email: str
447 :type email: str
448 :param password: password
448 :param password: password
449 :type password: Optional(str)
449 :type password: Optional(str)
450 :param firstname: firstname
450 :param firstname: firstname
451 :type firstname: Optional(str)
451 :type firstname: Optional(str)
452 :param lastname: lastname
452 :param lastname: lastname
453 :type lastname: Optional(str)
453 :type lastname: Optional(str)
454 :param active: active
454 :param active: active
455 :type active: Optional(bool)
455 :type active: Optional(bool)
456 :param admin: admin
456 :param admin: admin
457 :type admin: Optional(bool)
457 :type admin: Optional(bool)
458 :param extern_name: name of extern
458 :param extern_name: name of extern
459 :type extern_name: Optional(str)
459 :type extern_name: Optional(str)
460 :param extern_type: extern_type
460 :param extern_type: extern_type
461 :type extern_type: Optional(str)
461 :type extern_type: Optional(str)
462
462
463
463
464 OUTPUT::
464 OUTPUT::
465
465
466 id : <id_given_in_input>
466 id : <id_given_in_input>
467 result: {
467 result: {
468 "msg" : "created new user `<username>`",
468 "msg" : "created new user `<username>`",
469 "user": <user_obj>
469 "user": <user_obj>
470 }
470 }
471 error: null
471 error: null
472
472
473 ERROR OUTPUT::
473 ERROR OUTPUT::
474
474
475 id : <id_given_in_input>
475 id : <id_given_in_input>
476 result : null
476 result : null
477 error : {
477 error : {
478 "user `<username>` already exist"
478 "user `<username>` already exist"
479 or
479 or
480 "email `<email>` already exist"
480 "email `<email>` already exist"
481 or
481 or
482 "failed to create user `<username>`"
482 "failed to create user `<username>`"
483 }
483 }
484
484
485 """
485 """
486
486
487 if User.get_by_username(username):
487 if User.get_by_username(username):
488 raise JSONRPCError("user `%s` already exist" % (username,))
488 raise JSONRPCError("user `%s` already exist" % (username,))
489
489
490 if User.get_by_email(email):
490 if User.get_by_email(email):
491 raise JSONRPCError("email `%s` already exist" % (email,))
491 raise JSONRPCError("email `%s` already exist" % (email,))
492
492
493 try:
493 try:
494 user = UserModel().create_or_update(
494 user = UserModel().create_or_update(
495 username=Optional.extract(username),
495 username=Optional.extract(username),
496 password=Optional.extract(password),
496 password=Optional.extract(password),
497 email=Optional.extract(email),
497 email=Optional.extract(email),
498 firstname=Optional.extract(firstname),
498 firstname=Optional.extract(firstname),
499 lastname=Optional.extract(lastname),
499 lastname=Optional.extract(lastname),
500 active=Optional.extract(active),
500 active=Optional.extract(active),
501 admin=Optional.extract(admin),
501 admin=Optional.extract(admin),
502 extern_type=Optional.extract(extern_type),
502 extern_type=Optional.extract(extern_type),
503 extern_name=Optional.extract(extern_name)
503 extern_name=Optional.extract(extern_name)
504 )
504 )
505 Session().commit()
505 Session().commit()
506 return dict(
506 return dict(
507 msg='created new user `%s`' % username,
507 msg='created new user `%s`' % username,
508 user=user.get_api_data()
508 user=user.get_api_data()
509 )
509 )
510 except Exception:
510 except Exception:
511 log.error(traceback.format_exc())
511 log.error(traceback.format_exc())
512 raise JSONRPCError('failed to create user `%s`' % (username,))
512 raise JSONRPCError('failed to create user `%s`' % (username,))
513
513
514 @HasPermissionAnyDecorator('hg.admin')
514 @HasPermissionAnyDecorator('hg.admin')
515 def update_user(self, userid, username=Optional(None),
515 def update_user(self, userid, username=Optional(None),
516 email=Optional(None), password=Optional(None),
516 email=Optional(None), password=Optional(None),
517 firstname=Optional(None), lastname=Optional(None),
517 firstname=Optional(None), lastname=Optional(None),
518 active=Optional(None), admin=Optional(None),
518 active=Optional(None), admin=Optional(None),
519 extern_type=Optional(None), extern_name=Optional(None)):
519 extern_type=Optional(None), extern_name=Optional(None)):
520 """
520 """
521 updates given user if such user exists. This command can
521 updates given user if such user exists. This command can
522 be executed only using api_key belonging to user with admin rights.
522 be executed only using api_key belonging to user with admin rights.
523
523
524 :param userid: userid to update
524 :param userid: userid to update
525 :type userid: str or int
525 :type userid: str or int
526 :param username: new username
526 :param username: new username
527 :type username: str or int
527 :type username: str or int
528 :param email: email
528 :param email: email
529 :type email: str
529 :type email: str
530 :param password: password
530 :param password: password
531 :type password: Optional(str)
531 :type password: Optional(str)
532 :param firstname: firstname
532 :param firstname: firstname
533 :type firstname: Optional(str)
533 :type firstname: Optional(str)
534 :param lastname: lastname
534 :param lastname: lastname
535 :type lastname: Optional(str)
535 :type lastname: Optional(str)
536 :param active: active
536 :param active: active
537 :type active: Optional(bool)
537 :type active: Optional(bool)
538 :param admin: admin
538 :param admin: admin
539 :type admin: Optional(bool)
539 :type admin: Optional(bool)
540 :param extern_name:
540 :param extern_name:
541 :type extern_name: Optional(str)
541 :type extern_name: Optional(str)
542 :param extern_type:
542 :param extern_type:
543 :type extern_type: Optional(str)
543 :type extern_type: Optional(str)
544
544
545
545
546 OUTPUT::
546 OUTPUT::
547
547
548 id : <id_given_in_input>
548 id : <id_given_in_input>
549 result: {
549 result: {
550 "msg" : "updated user ID:<userid> <username>",
550 "msg" : "updated user ID:<userid> <username>",
551 "user": <user_object>,
551 "user": <user_object>,
552 }
552 }
553 error: null
553 error: null
554
554
555 ERROR OUTPUT::
555 ERROR OUTPUT::
556
556
557 id : <id_given_in_input>
557 id : <id_given_in_input>
558 result : null
558 result : null
559 error : {
559 error : {
560 "failed to update user `<username>`"
560 "failed to update user `<username>`"
561 }
561 }
562
562
563 """
563 """
564
564
565 user = get_user_or_error(userid)
565 user = get_user_or_error(userid)
566
566
567 # only non optional arguments will be stored in updates
567 # only non optional arguments will be stored in updates
568 updates = {}
568 updates = {}
569
569
570 try:
570 try:
571
571
572 store_update(updates, username, 'username')
572 store_update(updates, username, 'username')
573 store_update(updates, password, 'password')
573 store_update(updates, password, 'password')
574 store_update(updates, email, 'email')
574 store_update(updates, email, 'email')
575 store_update(updates, firstname, 'name')
575 store_update(updates, firstname, 'name')
576 store_update(updates, lastname, 'lastname')
576 store_update(updates, lastname, 'lastname')
577 store_update(updates, active, 'active')
577 store_update(updates, active, 'active')
578 store_update(updates, admin, 'admin')
578 store_update(updates, admin, 'admin')
579 store_update(updates, extern_name, 'extern_name')
579 store_update(updates, extern_name, 'extern_name')
580 store_update(updates, extern_type, 'extern_type')
580 store_update(updates, extern_type, 'extern_type')
581
581
582 user = UserModel().update_user(user, **updates)
582 user = UserModel().update_user(user, **updates)
583 Session().commit()
583 Session().commit()
584 return dict(
584 return dict(
585 msg='updated user ID:%s %s' % (user.user_id, user.username),
585 msg='updated user ID:%s %s' % (user.user_id, user.username),
586 user=user.get_api_data()
586 user=user.get_api_data()
587 )
587 )
588 except DefaultUserException:
588 except DefaultUserException:
589 log.error(traceback.format_exc())
589 log.error(traceback.format_exc())
590 raise JSONRPCError('editing default user is forbidden')
590 raise JSONRPCError('editing default user is forbidden')
591 except Exception:
591 except Exception:
592 log.error(traceback.format_exc())
592 log.error(traceback.format_exc())
593 raise JSONRPCError('failed to update user `%s`' % (userid,))
593 raise JSONRPCError('failed to update user `%s`' % (userid,))
594
594
595 @HasPermissionAnyDecorator('hg.admin')
595 @HasPermissionAnyDecorator('hg.admin')
596 def delete_user(self, userid):
596 def delete_user(self, userid):
597 """
597 """
598 deletes given user if such user exists. This command can
598 deletes given user if such user exists. This command can
599 be executed only using api_key belonging to user with admin rights.
599 be executed only using api_key belonging to user with admin rights.
600
600
601 :param userid: user to delete
601 :param userid: user to delete
602 :type userid: str or int
602 :type userid: str or int
603
603
604 OUTPUT::
604 OUTPUT::
605
605
606 id : <id_given_in_input>
606 id : <id_given_in_input>
607 result: {
607 result: {
608 "msg" : "deleted user ID:<userid> <username>",
608 "msg" : "deleted user ID:<userid> <username>",
609 "user": null
609 "user": null
610 }
610 }
611 error: null
611 error: null
612
612
613 ERROR OUTPUT::
613 ERROR OUTPUT::
614
614
615 id : <id_given_in_input>
615 id : <id_given_in_input>
616 result : null
616 result : null
617 error : {
617 error : {
618 "failed to delete user ID:<userid> <username>"
618 "failed to delete user ID:<userid> <username>"
619 }
619 }
620
620
621 """
621 """
622 user = get_user_or_error(userid)
622 user = get_user_or_error(userid)
623
623
624 try:
624 try:
625 UserModel().delete(userid)
625 UserModel().delete(userid)
626 Session().commit()
626 Session().commit()
627 return dict(
627 return dict(
628 msg='deleted user ID:%s %s' % (user.user_id, user.username),
628 msg='deleted user ID:%s %s' % (user.user_id, user.username),
629 user=None
629 user=None
630 )
630 )
631 except Exception:
631 except Exception:
632
632
633 log.error(traceback.format_exc())
633 log.error(traceback.format_exc())
634 raise JSONRPCError('failed to delete user ID:%s %s'
634 raise JSONRPCError('failed to delete user ID:%s %s'
635 % (user.user_id, user.username))
635 % (user.user_id, user.username))
636
636
637 # permission check inside
637 # permission check inside
638 def get_user_group(self, usergroupid):
638 def get_user_group(self, usergroupid):
639 """
639 """
640 Gets an existing user group. This command can be executed only using api_key
640 Gets an existing user group. This command can be executed only using api_key
641 belonging to user with admin rights or user who has at least
641 belonging to user with admin rights or user who has at least
642 read access to user group.
642 read access to user group.
643
643
644 :param usergroupid: id of user_group to edit
644 :param usergroupid: id of user_group to edit
645 :type usergroupid: str or int
645 :type usergroupid: str or int
646
646
647 OUTPUT::
647 OUTPUT::
648
648
649 id : <id_given_in_input>
649 id : <id_given_in_input>
650 result : None if group not exist
650 result : None if group not exist
651 {
651 {
652 "users_group_id" : "<id>",
652 "users_group_id" : "<id>",
653 "group_name" : "<groupname>",
653 "group_name" : "<groupname>",
654 "active": "<bool>",
654 "active": "<bool>",
655 "members" : [<user_obj>,...]
655 "members" : [<user_obj>,...]
656 }
656 }
657 error : null
657 error : null
658
658
659 """
659 """
660 user_group = get_user_group_or_error(usergroupid)
660 user_group = get_user_group_or_error(usergroupid)
661 if not HasPermissionAny('hg.admin')():
661 if not HasPermissionAny('hg.admin')():
662 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
662 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
663 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
663 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
664
664
665 data = user_group.get_api_data()
665 data = user_group.get_api_data()
666 return data
666 return data
667
667
668 # permission check inside
668 # permission check inside
669 def get_user_groups(self):
669 def get_user_groups(self):
670 """
670 """
671 Lists all existing user groups. This command can be executed only using
671 Lists all existing user groups. This command can be executed only using
672 api_key belonging to user with admin rights or user who has at least
672 api_key belonging to user with admin rights or user who has at least
673 read access to user group.
673 read access to user group.
674
674
675
675
676 OUTPUT::
676 OUTPUT::
677
677
678 id : <id_given_in_input>
678 id : <id_given_in_input>
679 result : [<user_group_obj>,...]
679 result : [<user_group_obj>,...]
680 error : null
680 error : null
681 """
681 """
682
682
683 return [
683 return [
684 user_group.get_api_data()
684 user_group.get_api_data()
685 for user_group in UserGroupList(UserGroup.query().all(), perm_level='read')
685 for user_group in UserGroupList(UserGroup.query().all(), perm_level='read')
686 ]
686 ]
687
687
688 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
688 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
689 def create_user_group(self, group_name, description=Optional(u''),
689 def create_user_group(self, group_name, description=Optional(u''),
690 owner=Optional(OAttr('apiuser')), active=Optional(True)):
690 owner=Optional(OAttr('apiuser')), active=Optional(True)):
691 """
691 """
692 Creates new user group. This command can be executed only using api_key
692 Creates new user group. This command can be executed only using api_key
693 belonging to user with admin rights or an user who has create user group
693 belonging to user with admin rights or an user who has create user group
694 permission
694 permission
695
695
696 :param group_name: name of new user group
696 :param group_name: name of new user group
697 :type group_name: str
697 :type group_name: str
698 :param description: group description
698 :param description: group description
699 :type description: str
699 :type description: str
700 :param owner: owner of group. If not passed apiuser is the owner
700 :param owner: owner of group. If not passed apiuser is the owner
701 :type owner: Optional(str or int)
701 :type owner: Optional(str or int)
702 :param active: group is active
702 :param active: group is active
703 :type active: Optional(bool)
703 :type active: Optional(bool)
704
704
705 OUTPUT::
705 OUTPUT::
706
706
707 id : <id_given_in_input>
707 id : <id_given_in_input>
708 result: {
708 result: {
709 "msg": "created new user group `<groupname>`",
709 "msg": "created new user group `<groupname>`",
710 "user_group": <user_group_object>
710 "user_group": <user_group_object>
711 }
711 }
712 error: null
712 error: null
713
713
714 ERROR OUTPUT::
714 ERROR OUTPUT::
715
715
716 id : <id_given_in_input>
716 id : <id_given_in_input>
717 result : null
717 result : null
718 error : {
718 error : {
719 "user group `<group name>` already exist"
719 "user group `<group name>` already exist"
720 or
720 or
721 "failed to create group `<group name>`"
721 "failed to create group `<group name>`"
722 }
722 }
723
723
724 """
724 """
725
725
726 if UserGroupModel().get_by_name(group_name):
726 if UserGroupModel().get_by_name(group_name):
727 raise JSONRPCError("user group `%s` already exist" % (group_name,))
727 raise JSONRPCError("user group `%s` already exist" % (group_name,))
728
728
729 try:
729 try:
730 if isinstance(owner, Optional):
730 if isinstance(owner, Optional):
731 owner = request.authuser.user_id
731 owner = request.authuser.user_id
732
732
733 owner = get_user_or_error(owner)
733 owner = get_user_or_error(owner)
734 active = Optional.extract(active)
734 active = Optional.extract(active)
735 description = Optional.extract(description)
735 description = Optional.extract(description)
736 ug = UserGroupModel().create(name=group_name, description=description,
736 ug = UserGroupModel().create(name=group_name, description=description,
737 owner=owner, active=active)
737 owner=owner, active=active)
738 Session().commit()
738 Session().commit()
739 return dict(
739 return dict(
740 msg='created new user group `%s`' % group_name,
740 msg='created new user group `%s`' % group_name,
741 user_group=ug.get_api_data()
741 user_group=ug.get_api_data()
742 )
742 )
743 except Exception:
743 except Exception:
744 log.error(traceback.format_exc())
744 log.error(traceback.format_exc())
745 raise JSONRPCError('failed to create group `%s`' % (group_name,))
745 raise JSONRPCError('failed to create group `%s`' % (group_name,))
746
746
747 # permission check inside
747 # permission check inside
748 def update_user_group(self, usergroupid, group_name=Optional(''),
748 def update_user_group(self, usergroupid, group_name=Optional(''),
749 description=Optional(''), owner=Optional(None),
749 description=Optional(''), owner=Optional(None),
750 active=Optional(True)):
750 active=Optional(True)):
751 """
751 """
752 Updates given usergroup. This command can be executed only using api_key
752 Updates given usergroup. This command can be executed only using api_key
753 belonging to user with admin rights or an admin of given user group
753 belonging to user with admin rights or an admin of given user group
754
754
755 :param usergroupid: id of user group to update
755 :param usergroupid: id of user group to update
756 :type usergroupid: str or int
756 :type usergroupid: str or int
757 :param group_name: name of new user group
757 :param group_name: name of new user group
758 :type group_name: str
758 :type group_name: str
759 :param description: group description
759 :param description: group description
760 :type description: str
760 :type description: str
761 :param owner: owner of group.
761 :param owner: owner of group.
762 :type owner: Optional(str or int)
762 :type owner: Optional(str or int)
763 :param active: group is active
763 :param active: group is active
764 :type active: Optional(bool)
764 :type active: Optional(bool)
765
765
766 OUTPUT::
766 OUTPUT::
767
767
768 id : <id_given_in_input>
768 id : <id_given_in_input>
769 result : {
769 result : {
770 "msg": 'updated user group ID:<user group id> <user group name>',
770 "msg": 'updated user group ID:<user group id> <user group name>',
771 "user_group": <user_group_object>
771 "user_group": <user_group_object>
772 }
772 }
773 error : null
773 error : null
774
774
775 ERROR OUTPUT::
775 ERROR OUTPUT::
776
776
777 id : <id_given_in_input>
777 id : <id_given_in_input>
778 result : null
778 result : null
779 error : {
779 error : {
780 "failed to update user group `<user group name>`"
780 "failed to update user group `<user group name>`"
781 }
781 }
782
782
783 """
783 """
784 user_group = get_user_group_or_error(usergroupid)
784 user_group = get_user_group_or_error(usergroupid)
785 if not HasPermissionAny('hg.admin')():
785 if not HasPermissionAny('hg.admin')():
786 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
786 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
787 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
787 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
788
788
789 if not isinstance(owner, Optional):
789 if not isinstance(owner, Optional):
790 owner = get_user_or_error(owner)
790 owner = get_user_or_error(owner)
791
791
792 updates = {}
792 updates = {}
793 store_update(updates, group_name, 'users_group_name')
793 store_update(updates, group_name, 'users_group_name')
794 store_update(updates, description, 'user_group_description')
794 store_update(updates, description, 'user_group_description')
795 store_update(updates, owner, 'owner')
795 store_update(updates, owner, 'owner')
796 store_update(updates, active, 'users_group_active')
796 store_update(updates, active, 'users_group_active')
797 try:
797 try:
798 UserGroupModel().update(user_group, updates)
798 UserGroupModel().update(user_group, updates)
799 Session().commit()
799 Session().commit()
800 return dict(
800 return dict(
801 msg='updated user group ID:%s %s' % (user_group.users_group_id,
801 msg='updated user group ID:%s %s' % (user_group.users_group_id,
802 user_group.users_group_name),
802 user_group.users_group_name),
803 user_group=user_group.get_api_data()
803 user_group=user_group.get_api_data()
804 )
804 )
805 except Exception:
805 except Exception:
806 log.error(traceback.format_exc())
806 log.error(traceback.format_exc())
807 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
807 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
808
808
809 # permission check inside
809 # permission check inside
810 def delete_user_group(self, usergroupid):
810 def delete_user_group(self, usergroupid):
811 """
811 """
812 Delete given user group by user group id or name.
812 Delete given user group by user group id or name.
813 This command can be executed only using api_key
813 This command can be executed only using api_key
814 belonging to user with admin rights or an admin of given user group
814 belonging to user with admin rights or an admin of given user group
815
815
816 :param usergroupid:
816 :param usergroupid:
817 :type usergroupid: int
817 :type usergroupid: int
818
818
819 OUTPUT::
819 OUTPUT::
820
820
821 id : <id_given_in_input>
821 id : <id_given_in_input>
822 result : {
822 result : {
823 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
823 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
824 }
824 }
825 error : null
825 error : null
826
826
827 ERROR OUTPUT::
827 ERROR OUTPUT::
828
828
829 id : <id_given_in_input>
829 id : <id_given_in_input>
830 result : null
830 result : null
831 error : {
831 error : {
832 "failed to delete user group ID:<user_group_id> <user_group_name>"
832 "failed to delete user group ID:<user_group_id> <user_group_name>"
833 or
833 or
834 "RepoGroup assigned to <repo_groups_list>"
834 "RepoGroup assigned to <repo_groups_list>"
835 }
835 }
836
836
837 """
837 """
838 user_group = get_user_group_or_error(usergroupid)
838 user_group = get_user_group_or_error(usergroupid)
839 if not HasPermissionAny('hg.admin')():
839 if not HasPermissionAny('hg.admin')():
840 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
840 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
841 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
841 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
842
842
843 try:
843 try:
844 UserGroupModel().delete(user_group)
844 UserGroupModel().delete(user_group)
845 Session().commit()
845 Session().commit()
846 return dict(
846 return dict(
847 msg='deleted user group ID:%s %s' %
847 msg='deleted user group ID:%s %s' %
848 (user_group.users_group_id, user_group.users_group_name),
848 (user_group.users_group_id, user_group.users_group_name),
849 user_group=None
849 user_group=None
850 )
850 )
851 except UserGroupsAssignedException as e:
851 except UserGroupsAssignedException as e:
852 log.error(traceback.format_exc())
852 log.error(traceback.format_exc())
853 raise JSONRPCError(str(e))
853 raise JSONRPCError(str(e))
854 except Exception:
854 except Exception:
855 log.error(traceback.format_exc())
855 log.error(traceback.format_exc())
856 raise JSONRPCError('failed to delete user group ID:%s %s' %
856 raise JSONRPCError('failed to delete user group ID:%s %s' %
857 (user_group.users_group_id,
857 (user_group.users_group_id,
858 user_group.users_group_name)
858 user_group.users_group_name)
859 )
859 )
860
860
861 # permission check inside
861 # permission check inside
862 def add_user_to_user_group(self, usergroupid, userid):
862 def add_user_to_user_group(self, usergroupid, userid):
863 """
863 """
864 Adds a user to a user group. If user exists in that group success will be
864 Adds a user to a user group. If user exists in that group success will be
865 `false`. This command can be executed only using api_key
865 `false`. This command can be executed only using api_key
866 belonging to user with admin rights or an admin of given user group
866 belonging to user with admin rights or an admin of given user group
867
867
868 :param usergroupid:
868 :param usergroupid:
869 :type usergroupid: int
869 :type usergroupid: int
870 :param userid:
870 :param userid:
871 :type userid: int
871 :type userid: int
872
872
873 OUTPUT::
873 OUTPUT::
874
874
875 id : <id_given_in_input>
875 id : <id_given_in_input>
876 result : {
876 result : {
877 "success": True|False # depends on if member is in group
877 "success": True|False # depends on if member is in group
878 "msg": "added member `<username>` to user group `<groupname>` |
878 "msg": "added member `<username>` to user group `<groupname>` |
879 User is already in that group"
879 User is already in that group"
880
880
881 }
881 }
882 error : null
882 error : null
883
883
884 ERROR OUTPUT::
884 ERROR OUTPUT::
885
885
886 id : <id_given_in_input>
886 id : <id_given_in_input>
887 result : null
887 result : null
888 error : {
888 error : {
889 "failed to add member to user group `<user_group_name>`"
889 "failed to add member to user group `<user_group_name>`"
890 }
890 }
891
891
892 """
892 """
893 user = get_user_or_error(userid)
893 user = get_user_or_error(userid)
894 user_group = get_user_group_or_error(usergroupid)
894 user_group = get_user_group_or_error(usergroupid)
895 if not HasPermissionAny('hg.admin')():
895 if not HasPermissionAny('hg.admin')():
896 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
896 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
897 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
897 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
898
898
899 try:
899 try:
900 ugm = UserGroupModel().add_user_to_group(user_group, user)
900 ugm = UserGroupModel().add_user_to_group(user_group, user)
901 success = True if ugm is not True else False
901 success = True if ugm is not True else False
902 msg = 'added member `%s` to user group `%s`' % (
902 msg = 'added member `%s` to user group `%s`' % (
903 user.username, user_group.users_group_name
903 user.username, user_group.users_group_name
904 )
904 )
905 msg = msg if success else 'User is already in that group'
905 msg = msg if success else 'User is already in that group'
906 Session().commit()
906 Session().commit()
907
907
908 return dict(
908 return dict(
909 success=success,
909 success=success,
910 msg=msg
910 msg=msg
911 )
911 )
912 except Exception:
912 except Exception:
913 log.error(traceback.format_exc())
913 log.error(traceback.format_exc())
914 raise JSONRPCError(
914 raise JSONRPCError(
915 'failed to add member to user group `%s`' % (
915 'failed to add member to user group `%s`' % (
916 user_group.users_group_name,
916 user_group.users_group_name,
917 )
917 )
918 )
918 )
919
919
920 # permission check inside
920 # permission check inside
921 def remove_user_from_user_group(self, usergroupid, userid):
921 def remove_user_from_user_group(self, usergroupid, userid):
922 """
922 """
923 Removes a user from a user group. If user is not in given group success will
923 Removes a user from a user group. If user is not in given group success will
924 be `false`. This command can be executed only
924 be `false`. This command can be executed only
925 using api_key belonging to user with admin rights or an admin of given user group
925 using api_key belonging to user with admin rights or an admin of given user group
926
926
927 :param usergroupid:
927 :param usergroupid:
928 :param userid:
928 :param userid:
929
929
930
930
931 OUTPUT::
931 OUTPUT::
932
932
933 id : <id_given_in_input>
933 id : <id_given_in_input>
934 result: {
934 result: {
935 "success": True|False, # depends on if member is in group
935 "success": True|False, # depends on if member is in group
936 "msg": "removed member <username> from user group <groupname> |
936 "msg": "removed member <username> from user group <groupname> |
937 User wasn't in group"
937 User wasn't in group"
938 }
938 }
939 error: null
939 error: null
940
940
941 """
941 """
942 user = get_user_or_error(userid)
942 user = get_user_or_error(userid)
943 user_group = get_user_group_or_error(usergroupid)
943 user_group = get_user_group_or_error(usergroupid)
944 if not HasPermissionAny('hg.admin')():
944 if not HasPermissionAny('hg.admin')():
945 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
945 if not HasUserGroupPermissionLevel('admin')(user_group.users_group_name):
946 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
946 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
947
947
948 try:
948 try:
949 success = UserGroupModel().remove_user_from_group(user_group, user)
949 success = UserGroupModel().remove_user_from_group(user_group, user)
950 msg = 'removed member `%s` from user group `%s`' % (
950 msg = 'removed member `%s` from user group `%s`' % (
951 user.username, user_group.users_group_name
951 user.username, user_group.users_group_name
952 )
952 )
953 msg = msg if success else "User wasn't in group"
953 msg = msg if success else "User wasn't in group"
954 Session().commit()
954 Session().commit()
955 return dict(success=success, msg=msg)
955 return dict(success=success, msg=msg)
956 except Exception:
956 except Exception:
957 log.error(traceback.format_exc())
957 log.error(traceback.format_exc())
958 raise JSONRPCError(
958 raise JSONRPCError(
959 'failed to remove member from user group `%s`' % (
959 'failed to remove member from user group `%s`' % (
960 user_group.users_group_name,
960 user_group.users_group_name,
961 )
961 )
962 )
962 )
963
963
964 # permission check inside
964 # permission check inside
965 def get_repo(self, repoid,
965 def get_repo(self, repoid,
966 with_revision_names=Optional(False),
966 with_revision_names=Optional(False),
967 with_pullrequests=Optional(False)):
967 with_pullrequests=Optional(False)):
968 """
968 """
969 Gets an existing repository by it's name or repository_id. Members will return
969 Gets an existing repository by it's name or repository_id. Members will return
970 either users_group or user associated to that repository. This command can be
970 either users_group or user associated to that repository. This command can be
971 executed only using api_key belonging to user with admin
971 executed only using api_key belonging to user with admin
972 rights or regular user that have at least read access to repository.
972 rights or regular user that have at least read access to repository.
973
973
974 :param repoid: repository name or repository id
974 :param repoid: repository name or repository id
975 :type repoid: str or int
975 :type repoid: str or int
976
976
977 OUTPUT::
977 OUTPUT::
978
978
979 id : <id_given_in_input>
979 id : <id_given_in_input>
980 result : {
980 result : {
981 {
981 {
982 "repo_id" : "<repo_id>",
982 "repo_id" : "<repo_id>",
983 "repo_name" : "<reponame>"
983 "repo_name" : "<reponame>"
984 "repo_type" : "<repo_type>",
984 "repo_type" : "<repo_type>",
985 "clone_uri" : "<clone_uri>",
985 "clone_uri" : "<clone_uri>",
986 "enable_downloads": "<bool>",
986 "enable_downloads": "<bool>",
987 "enable_statistics": "<bool>",
987 "enable_statistics": "<bool>",
988 "private": "<bool>",
988 "private": "<bool>",
989 "created_on" : "<date_time_created>",
989 "created_on" : "<date_time_created>",
990 "description" : "<description>",
990 "description" : "<description>",
991 "landing_rev": "<landing_rev>",
991 "landing_rev": "<landing_rev>",
992 "last_changeset": {
992 "last_changeset": {
993 "author": "<full_author>",
993 "author": "<full_author>",
994 "date": "<date_time_of_commit>",
994 "date": "<date_time_of_commit>",
995 "message": "<commit_message>",
995 "message": "<commit_message>",
996 "raw_id": "<raw_id>",
996 "raw_id": "<raw_id>",
997 "revision": "<numeric_revision>",
997 "revision": "<numeric_revision>",
998 "short_id": "<short_id>"
998 "short_id": "<short_id>"
999 }
999 }
1000 "owner": "<repo_owner>",
1000 "owner": "<repo_owner>",
1001 "fork_of": "<name_of_fork_parent>",
1001 "fork_of": "<name_of_fork_parent>",
1002 "members" : [
1002 "members" : [
1003 {
1003 {
1004 "name": "<username>",
1004 "name": "<username>",
1005 "type" : "user",
1005 "type" : "user",
1006 "permission" : "repository.(read|write|admin)"
1006 "permission" : "repository.(read|write|admin)"
1007 },
1007 },
1008 …
1008 …
1009 {
1009 {
1010 "name": "<usergroup name>",
1010 "name": "<usergroup name>",
1011 "type" : "user_group",
1011 "type" : "user_group",
1012 "permission" : "usergroup.(read|write|admin)"
1012 "permission" : "usergroup.(read|write|admin)"
1013 },
1013 },
1014 …
1014 …
1015 ]
1015 ]
1016 "followers": [<user_obj>, ...],
1016 "followers": [<user_obj>, ...],
1017 <if with_revision_names == True>
1017 <if with_revision_names == True>
1018 "tags": {
1018 "tags": {
1019 "<tagname>": "<raw_id>",
1019 "<tagname>": "<raw_id>",
1020 ...
1020 ...
1021 },
1021 },
1022 "branches": {
1022 "branches": {
1023 "<branchname>": "<raw_id>",
1023 "<branchname>": "<raw_id>",
1024 ...
1024 ...
1025 },
1025 },
1026 "bookmarks": {
1026 "bookmarks": {
1027 "<bookmarkname>": "<raw_id>",
1027 "<bookmarkname>": "<raw_id>",
1028 ...
1028 ...
1029 },
1029 },
1030 }
1030 }
1031 }
1031 }
1032 error : null
1032 error : null
1033
1033
1034 """
1034 """
1035 repo = get_repo_or_error(repoid)
1035 repo = get_repo_or_error(repoid)
1036
1036
1037 if not HasPermissionAny('hg.admin')():
1037 if not HasPermissionAny('hg.admin')():
1038 if not HasRepoPermissionLevel('read')(repo.repo_name):
1038 if not HasRepoPermissionLevel('read')(repo.repo_name):
1039 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1039 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1040
1040
1041 members = []
1041 members = []
1042 for user in repo.repo_to_perm:
1042 for user in repo.repo_to_perm:
1043 perm = user.permission.permission_name
1043 perm = user.permission.permission_name
1044 user = user.user
1044 user = user.user
1045 user_data = {
1045 user_data = {
1046 'name': user.username,
1046 'name': user.username,
1047 'type': "user",
1047 'type': "user",
1048 'permission': perm
1048 'permission': perm
1049 }
1049 }
1050 members.append(user_data)
1050 members.append(user_data)
1051
1051
1052 for user_group in repo.users_group_to_perm:
1052 for user_group in repo.users_group_to_perm:
1053 perm = user_group.permission.permission_name
1053 perm = user_group.permission.permission_name
1054 user_group = user_group.users_group
1054 user_group = user_group.users_group
1055 user_group_data = {
1055 user_group_data = {
1056 'name': user_group.users_group_name,
1056 'name': user_group.users_group_name,
1057 'type': "user_group",
1057 'type': "user_group",
1058 'permission': perm
1058 'permission': perm
1059 }
1059 }
1060 members.append(user_group_data)
1060 members.append(user_group_data)
1061
1061
1062 followers = [
1062 followers = [
1063 uf.user.get_api_data()
1063 uf.user.get_api_data()
1064 for uf in repo.followers
1064 for uf in repo.followers
1065 ]
1065 ]
1066
1066
1067 data = repo.get_api_data(with_revision_names=Optional.extract(with_revision_names),
1067 data = repo.get_api_data(with_revision_names=Optional.extract(with_revision_names),
1068 with_pullrequests=Optional.extract(with_pullrequests))
1068 with_pullrequests=Optional.extract(with_pullrequests))
1069 data['members'] = members
1069 data['members'] = members
1070 data['followers'] = followers
1070 data['followers'] = followers
1071 return data
1071 return data
1072
1072
1073 # permission check inside
1073 # permission check inside
1074 def get_repos(self):
1074 def get_repos(self):
1075 """
1075 """
1076 Lists all existing repositories. This command can be executed only using
1076 Lists all existing repositories. This command can be executed only using
1077 api_key belonging to user with admin rights or regular user that have
1077 api_key belonging to user with admin rights or regular user that have
1078 admin, write or read access to repository.
1078 admin, write or read access to repository.
1079
1079
1080
1080
1081 OUTPUT::
1081 OUTPUT::
1082
1082
1083 id : <id_given_in_input>
1083 id : <id_given_in_input>
1084 result: [
1084 result: [
1085 {
1085 {
1086 "repo_id" : "<repo_id>",
1086 "repo_id" : "<repo_id>",
1087 "repo_name" : "<reponame>"
1087 "repo_name" : "<reponame>"
1088 "repo_type" : "<repo_type>",
1088 "repo_type" : "<repo_type>",
1089 "clone_uri" : "<clone_uri>",
1089 "clone_uri" : "<clone_uri>",
1090 "private": : "<bool>",
1090 "private": : "<bool>",
1091 "created_on" : "<datetimecreated>",
1091 "created_on" : "<datetimecreated>",
1092 "description" : "<description>",
1092 "description" : "<description>",
1093 "landing_rev": "<landing_rev>",
1093 "landing_rev": "<landing_rev>",
1094 "owner": "<repo_owner>",
1094 "owner": "<repo_owner>",
1095 "fork_of": "<name_of_fork_parent>",
1095 "fork_of": "<name_of_fork_parent>",
1096 "enable_downloads": "<bool>",
1096 "enable_downloads": "<bool>",
1097 "enable_statistics": "<bool>",
1097 "enable_statistics": "<bool>",
1098 },
1098 },
1099 …
1099 …
1100 ]
1100 ]
1101 error: null
1101 error: null
1102 """
1102 """
1103 if not HasPermissionAny('hg.admin')():
1103 if not HasPermissionAny('hg.admin')():
1104 repos = RepoModel().get_all_user_repos(user=request.authuser.user_id)
1104 repos = RepoModel().get_all_user_repos(user=request.authuser.user_id)
1105 else:
1105 else:
1106 repos = Repository.query()
1106 repos = Repository.query()
1107
1107
1108 return [
1108 return [
1109 repo.get_api_data()
1109 repo.get_api_data()
1110 for repo in repos
1110 for repo in repos
1111 ]
1111 ]
1112
1112
1113 # permission check inside
1113 # permission check inside
1114 def get_repo_nodes(self, repoid, revision, root_path,
1114 def get_repo_nodes(self, repoid, revision, root_path,
1115 ret_type=Optional('all')):
1115 ret_type=Optional('all')):
1116 """
1116 """
1117 returns a list of nodes and it's children in a flat list for a given path
1117 returns a list of nodes and it's children in a flat list for a given path
1118 at given revision. It's possible to specify ret_type to show only `files` or
1118 at given revision. It's possible to specify ret_type to show only `files` or
1119 `dirs`. This command can be executed only using api_key belonging to
1119 `dirs`. This command can be executed only using api_key belonging to
1120 user with admin rights or regular user that have at least read access to repository.
1120 user with admin rights or regular user that have at least read access to repository.
1121
1121
1122 :param repoid: repository name or repository id
1122 :param repoid: repository name or repository id
1123 :type repoid: str or int
1123 :type repoid: str or int
1124 :param revision: revision for which listing should be done
1124 :param revision: revision for which listing should be done
1125 :type revision: str
1125 :type revision: str
1126 :param root_path: path from which start displaying
1126 :param root_path: path from which start displaying
1127 :type root_path: str
1127 :type root_path: str
1128 :param ret_type: return type 'all|files|dirs' nodes
1128 :param ret_type: return type 'all|files|dirs' nodes
1129 :type ret_type: Optional(str)
1129 :type ret_type: Optional(str)
1130
1130
1131
1131
1132 OUTPUT::
1132 OUTPUT::
1133
1133
1134 id : <id_given_in_input>
1134 id : <id_given_in_input>
1135 result: [
1135 result: [
1136 {
1136 {
1137 "name" : "<name>"
1137 "name" : "<name>"
1138 "type" : "<type>",
1138 "type" : "<type>",
1139 },
1139 },
1140 …
1140 …
1141 ]
1141 ]
1142 error: null
1142 error: null
1143 """
1143 """
1144 repo = get_repo_or_error(repoid)
1144 repo = get_repo_or_error(repoid)
1145
1145
1146 if not HasPermissionAny('hg.admin')():
1146 if not HasPermissionAny('hg.admin')():
1147 if not HasRepoPermissionLevel('read')(repo.repo_name):
1147 if not HasRepoPermissionLevel('read')(repo.repo_name):
1148 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1148 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1149
1149
1150 ret_type = Optional.extract(ret_type)
1150 ret_type = Optional.extract(ret_type)
1151 _map = {}
1151 _map = {}
1152 try:
1152 try:
1153 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1153 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1154 flat=False)
1154 flat=False)
1155 _map = {
1155 _map = {
1156 'all': _d + _f,
1156 'all': _d + _f,
1157 'files': _f,
1157 'files': _f,
1158 'dirs': _d,
1158 'dirs': _d,
1159 }
1159 }
1160 return _map[ret_type]
1160 return _map[ret_type]
1161 except KeyError:
1161 except KeyError:
1162 raise JSONRPCError('ret_type must be one of %s'
1162 raise JSONRPCError('ret_type must be one of %s'
1163 % (','.join(_map.keys())))
1163 % (','.join(_map.keys())))
1164 except Exception:
1164 except Exception:
1165 log.error(traceback.format_exc())
1165 log.error(traceback.format_exc())
1166 raise JSONRPCError(
1166 raise JSONRPCError(
1167 'failed to get repo: `%s` nodes' % repo.repo_name
1167 'failed to get repo: `%s` nodes' % repo.repo_name
1168 )
1168 )
1169
1169
1170 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1170 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1171 def create_repo(self, repo_name, owner=Optional(OAttr('apiuser')),
1171 def create_repo(self, repo_name, owner=Optional(OAttr('apiuser')),
1172 repo_type=Optional('hg'), description=Optional(''),
1172 repo_type=Optional('hg'), description=Optional(''),
1173 private=Optional(False), clone_uri=Optional(None),
1173 private=Optional(False), clone_uri=Optional(None),
1174 landing_rev=Optional('rev:tip'),
1174 landing_rev=Optional('rev:tip'),
1175 enable_statistics=Optional(False),
1175 enable_statistics=Optional(False),
1176 enable_downloads=Optional(False),
1176 enable_downloads=Optional(False),
1177 copy_permissions=Optional(False)):
1177 copy_permissions=Optional(False)):
1178 """
1178 """
1179 Creates a repository. The repository name contains the full path, but the
1179 Creates a repository. The repository name contains the full path, but the
1180 parent repository group must exist. For example "foo/bar/baz" require the groups
1180 parent repository group must exist. For example "foo/bar/baz" require the groups
1181 "foo" and "bar" (with "foo" as parent), and create "baz" repository with
1181 "foo" and "bar" (with "foo" as parent), and create "baz" repository with
1182 "bar" as group. This command can be executed only using api_key
1182 "bar" as group. This command can be executed only using api_key
1183 belonging to user with admin rights or regular user that have create
1183 belonging to user with admin rights or regular user that have create
1184 repository permission. Regular users cannot specify owner parameter
1184 repository permission. Regular users cannot specify owner parameter
1185
1185
1186 :param repo_name: repository name
1186 :param repo_name: repository name
1187 :type repo_name: str
1187 :type repo_name: str
1188 :param owner: user_id or username
1188 :param owner: user_id or username
1189 :type owner: Optional(str)
1189 :type owner: Optional(str)
1190 :param repo_type: 'hg' or 'git'
1190 :param repo_type: 'hg' or 'git'
1191 :type repo_type: Optional(str)
1191 :type repo_type: Optional(str)
1192 :param description: repository description
1192 :param description: repository description
1193 :type description: Optional(str)
1193 :type description: Optional(str)
1194 :param private:
1194 :param private:
1195 :type private: bool
1195 :type private: bool
1196 :param clone_uri:
1196 :param clone_uri:
1197 :type clone_uri: str
1197 :type clone_uri: str
1198 :param landing_rev: <rev_type>:<rev>
1198 :param landing_rev: <rev_type>:<rev>
1199 :type landing_rev: str
1199 :type landing_rev: str
1200 :param enable_downloads:
1200 :param enable_downloads:
1201 :type enable_downloads: bool
1201 :type enable_downloads: bool
1202 :param enable_statistics:
1202 :param enable_statistics:
1203 :type enable_statistics: bool
1203 :type enable_statistics: bool
1204 :param copy_permissions: Copy permission from group that repository is
1204 :param copy_permissions: Copy permission from group that repository is
1205 being created.
1205 being created.
1206 :type copy_permissions: bool
1206 :type copy_permissions: bool
1207
1207
1208 OUTPUT::
1208 OUTPUT::
1209
1209
1210 id : <id_given_in_input>
1210 id : <id_given_in_input>
1211 result: {
1211 result: {
1212 "msg": "Created new repository `<reponame>`",
1212 "msg": "Created new repository `<reponame>`",
1213 "success": true,
1213 "success": true,
1214 "task": "<celery task id or None if done sync>"
1214 "task": "<celery task id or None if done sync>"
1215 }
1215 }
1216 error: null
1216 error: null
1217
1217
1218 ERROR OUTPUT::
1218 ERROR OUTPUT::
1219
1219
1220 id : <id_given_in_input>
1220 id : <id_given_in_input>
1221 result : null
1221 result : null
1222 error : {
1222 error : {
1223 'failed to create repository `<repo_name>`
1223 'failed to create repository `<repo_name>`
1224 }
1224 }
1225
1225
1226 """
1226 """
1227 if not HasPermissionAny('hg.admin')():
1227 if not HasPermissionAny('hg.admin')():
1228 if not isinstance(owner, Optional):
1228 if not isinstance(owner, Optional):
1229 # forbid setting owner for non-admins
1229 # forbid setting owner for non-admins
1230 raise JSONRPCError(
1230 raise JSONRPCError(
1231 'Only Kallithea admin can specify `owner` param'
1231 'Only Kallithea admin can specify `owner` param'
1232 )
1232 )
1233 if isinstance(owner, Optional):
1233 if isinstance(owner, Optional):
1234 owner = request.authuser.user_id
1234 owner = request.authuser.user_id
1235
1235
1236 owner = get_user_or_error(owner)
1236 owner = get_user_or_error(owner)
1237
1237
1238 if RepoModel().get_by_repo_name(repo_name):
1238 if RepoModel().get_by_repo_name(repo_name):
1239 raise JSONRPCError("repo `%s` already exist" % repo_name)
1239 raise JSONRPCError("repo `%s` already exist" % repo_name)
1240
1240
1241 defs = Setting.get_default_repo_settings(strip_prefix=True)
1241 defs = Setting.get_default_repo_settings(strip_prefix=True)
1242 if isinstance(private, Optional):
1242 if isinstance(private, Optional):
1243 private = defs.get('repo_private') or Optional.extract(private)
1243 private = defs.get('repo_private') or Optional.extract(private)
1244 if isinstance(repo_type, Optional):
1244 if isinstance(repo_type, Optional):
1245 repo_type = defs.get('repo_type')
1245 repo_type = defs.get('repo_type')
1246 if isinstance(enable_statistics, Optional):
1246 if isinstance(enable_statistics, Optional):
1247 enable_statistics = defs.get('repo_enable_statistics')
1247 enable_statistics = defs.get('repo_enable_statistics')
1248 if isinstance(enable_downloads, Optional):
1248 if isinstance(enable_downloads, Optional):
1249 enable_downloads = defs.get('repo_enable_downloads')
1249 enable_downloads = defs.get('repo_enable_downloads')
1250
1250
1251 clone_uri = Optional.extract(clone_uri)
1251 clone_uri = Optional.extract(clone_uri)
1252 description = Optional.extract(description)
1252 description = Optional.extract(description)
1253 landing_rev = Optional.extract(landing_rev)
1253 landing_rev = Optional.extract(landing_rev)
1254 copy_permissions = Optional.extract(copy_permissions)
1254 copy_permissions = Optional.extract(copy_permissions)
1255
1255
1256 try:
1256 try:
1257 repo_name_parts = repo_name.split('/')
1257 repo_name_parts = repo_name.split('/')
1258 repo_group = None
1258 repo_group = None
1259 if len(repo_name_parts) > 1:
1259 if len(repo_name_parts) > 1:
1260 group_name = '/'.join(repo_name_parts[:-1])
1260 group_name = '/'.join(repo_name_parts[:-1])
1261 repo_group = RepoGroup.get_by_group_name(group_name)
1261 repo_group = RepoGroup.get_by_group_name(group_name)
1262 if repo_group is None:
1262 if repo_group is None:
1263 raise JSONRPCError("repo group `%s` not found" % group_name)
1263 raise JSONRPCError("repo group `%s` not found" % group_name)
1264 data = dict(
1264 data = dict(
1265 repo_name=repo_name_parts[-1],
1265 repo_name=repo_name_parts[-1],
1266 repo_name_full=repo_name,
1266 repo_name_full=repo_name,
1267 repo_type=repo_type,
1267 repo_type=repo_type,
1268 repo_description=description,
1268 repo_description=description,
1269 owner=owner,
1269 owner=owner,
1270 repo_private=private,
1270 repo_private=private,
1271 clone_uri=clone_uri,
1271 clone_uri=clone_uri,
1272 repo_group=repo_group,
1272 repo_group=repo_group,
1273 repo_landing_rev=landing_rev,
1273 repo_landing_rev=landing_rev,
1274 enable_statistics=enable_statistics,
1274 enable_statistics=enable_statistics,
1275 enable_downloads=enable_downloads,
1275 enable_downloads=enable_downloads,
1276 repo_copy_permissions=copy_permissions,
1276 repo_copy_permissions=copy_permissions,
1277 )
1277 )
1278
1278
1279 task = RepoModel().create(form_data=data, cur_user=owner)
1279 task = RepoModel().create(form_data=data, cur_user=owner)
1280 task_id = task.task_id
1280 task_id = task.task_id
1281 # no commit, it's done in RepoModel, or async via celery
1281 # no commit, it's done in RepoModel, or async via celery
1282 return dict(
1282 return dict(
1283 msg="Created new repository `%s`" % (repo_name,),
1283 msg="Created new repository `%s`" % (repo_name,),
1284 success=True, # cannot return the repo data here since fork
1284 success=True, # cannot return the repo data here since fork
1285 # can be done async
1285 # can be done async
1286 task=task_id
1286 task=task_id
1287 )
1287 )
1288 except Exception:
1288 except Exception:
1289 log.error(traceback.format_exc())
1289 log.error(traceback.format_exc())
1290 raise JSONRPCError(
1290 raise JSONRPCError(
1291 'failed to create repository `%s`' % (repo_name,))
1291 'failed to create repository `%s`' % (repo_name,))
1292
1292
1293 # permission check inside
1293 # permission check inside
1294 def update_repo(self, repoid, name=Optional(None),
1294 def update_repo(self, repoid, name=Optional(None),
1295 owner=Optional(OAttr('apiuser')),
1295 owner=Optional(OAttr('apiuser')),
1296 group=Optional(None),
1296 group=Optional(None),
1297 description=Optional(''), private=Optional(False),
1297 description=Optional(''), private=Optional(False),
1298 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1298 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1299 enable_statistics=Optional(False),
1299 enable_statistics=Optional(False),
1300 enable_downloads=Optional(False)):
1300 enable_downloads=Optional(False)):
1301
1301
1302 """
1302 """
1303 Updates repo
1303 Updates repo
1304
1304
1305 :param repoid: repository name or repository id
1305 :param repoid: repository name or repository id
1306 :type repoid: str or int
1306 :type repoid: str or int
1307 :param name:
1307 :param name:
1308 :param owner:
1308 :param owner:
1309 :param group:
1309 :param group:
1310 :param description:
1310 :param description:
1311 :param private:
1311 :param private:
1312 :param clone_uri:
1312 :param clone_uri:
1313 :param landing_rev:
1313 :param landing_rev:
1314 :param enable_statistics:
1314 :param enable_statistics:
1315 :param enable_downloads:
1315 :param enable_downloads:
1316 """
1316 """
1317 repo = get_repo_or_error(repoid)
1317 repo = get_repo_or_error(repoid)
1318 if not HasPermissionAny('hg.admin')():
1318 if not HasPermissionAny('hg.admin')():
1319 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1319 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1320 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1320 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1321
1321
1322 if (name != repo.repo_name and
1322 if (name != repo.repo_name and
1323 not HasPermissionAny('hg.create.repository')()
1323 not HasPermissionAny('hg.create.repository')()
1324 ):
1324 ):
1325 raise JSONRPCError('no permission to create (or move) repositories')
1325 raise JSONRPCError('no permission to create (or move) repositories')
1326
1326
1327 if not isinstance(owner, Optional):
1327 if not isinstance(owner, Optional):
1328 # forbid setting owner for non-admins
1328 # forbid setting owner for non-admins
1329 raise JSONRPCError(
1329 raise JSONRPCError(
1330 'Only Kallithea admin can specify `owner` param'
1330 'Only Kallithea admin can specify `owner` param'
1331 )
1331 )
1332
1332
1333 updates = {}
1333 updates = {}
1334 repo_group = group
1334 repo_group = group
1335 if not isinstance(repo_group, Optional):
1335 if not isinstance(repo_group, Optional):
1336 repo_group = get_repo_group_or_error(repo_group)
1336 repo_group = get_repo_group_or_error(repo_group)
1337 repo_group = repo_group.group_id
1337 repo_group = repo_group.group_id
1338 try:
1338 try:
1339 store_update(updates, name, 'repo_name')
1339 store_update(updates, name, 'repo_name')
1340 store_update(updates, repo_group, 'repo_group')
1340 store_update(updates, repo_group, 'repo_group')
1341 store_update(updates, owner, 'owner')
1341 store_update(updates, owner, 'owner')
1342 store_update(updates, description, 'repo_description')
1342 store_update(updates, description, 'repo_description')
1343 store_update(updates, private, 'repo_private')
1343 store_update(updates, private, 'repo_private')
1344 store_update(updates, clone_uri, 'clone_uri')
1344 store_update(updates, clone_uri, 'clone_uri')
1345 store_update(updates, landing_rev, 'repo_landing_rev')
1345 store_update(updates, landing_rev, 'repo_landing_rev')
1346 store_update(updates, enable_statistics, 'repo_enable_statistics')
1346 store_update(updates, enable_statistics, 'repo_enable_statistics')
1347 store_update(updates, enable_downloads, 'repo_enable_downloads')
1347 store_update(updates, enable_downloads, 'repo_enable_downloads')
1348
1348
1349 RepoModel().update(repo, **updates)
1349 RepoModel().update(repo, **updates)
1350 Session().commit()
1350 Session().commit()
1351 return dict(
1351 return dict(
1352 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1352 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1353 repository=repo.get_api_data()
1353 repository=repo.get_api_data()
1354 )
1354 )
1355 except Exception:
1355 except Exception:
1356 log.error(traceback.format_exc())
1356 log.error(traceback.format_exc())
1357 raise JSONRPCError('failed to update repo `%s`' % repoid)
1357 raise JSONRPCError('failed to update repo `%s`' % repoid)
1358
1358
1359 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1359 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1360 def fork_repo(self, repoid, fork_name,
1360 def fork_repo(self, repoid, fork_name,
1361 owner=Optional(OAttr('apiuser')),
1361 owner=Optional(OAttr('apiuser')),
1362 description=Optional(''), copy_permissions=Optional(False),
1362 description=Optional(''), copy_permissions=Optional(False),
1363 private=Optional(False), landing_rev=Optional('rev:tip')):
1363 private=Optional(False), landing_rev=Optional('rev:tip')):
1364 """
1364 """
1365 Creates a fork of given repo. In case of using celery this will
1365 Creates a fork of given repo. In case of using celery this will
1366 immediately return success message, while fork is going to be created
1366 immediately return success message, while fork is going to be created
1367 asynchronous. This command can be executed only using api_key belonging to
1367 asynchronous. This command can be executed only using api_key belonging to
1368 user with admin rights or regular user that have fork permission, and at least
1368 user with admin rights or regular user that have fork permission, and at least
1369 read access to forking repository. Regular users cannot specify owner parameter.
1369 read access to forking repository. Regular users cannot specify owner parameter.
1370
1370
1371 :param repoid: repository name or repository id
1371 :param repoid: repository name or repository id
1372 :type repoid: str or int
1372 :type repoid: str or int
1373 :param fork_name:
1373 :param fork_name:
1374 :param owner:
1374 :param owner:
1375 :param description:
1375 :param description:
1376 :param copy_permissions:
1376 :param copy_permissions:
1377 :param private:
1377 :param private:
1378 :param landing_rev:
1378 :param landing_rev:
1379
1379
1380 INPUT::
1380 INPUT::
1381
1381
1382 id : <id_for_response>
1382 id : <id_for_response>
1383 api_key : "<api_key>"
1383 api_key : "<api_key>"
1384 args: {
1384 args: {
1385 "repoid" : "<reponame or repo_id>",
1385 "repoid" : "<reponame or repo_id>",
1386 "fork_name": "<forkname>",
1386 "fork_name": "<forkname>",
1387 "owner": "<username or user_id = Optional(=apiuser)>",
1387 "owner": "<username or user_id = Optional(=apiuser)>",
1388 "description": "<description>",
1388 "description": "<description>",
1389 "copy_permissions": "<bool>",
1389 "copy_permissions": "<bool>",
1390 "private": "<bool>",
1390 "private": "<bool>",
1391 "landing_rev": "<landing_rev>"
1391 "landing_rev": "<landing_rev>"
1392 }
1392 }
1393
1393
1394 OUTPUT::
1394 OUTPUT::
1395
1395
1396 id : <id_given_in_input>
1396 id : <id_given_in_input>
1397 result: {
1397 result: {
1398 "msg": "Created fork of `<reponame>` as `<forkname>`",
1398 "msg": "Created fork of `<reponame>` as `<forkname>`",
1399 "success": true,
1399 "success": true,
1400 "task": "<celery task id or None if done sync>"
1400 "task": "<celery task id or None if done sync>"
1401 }
1401 }
1402 error: null
1402 error: null
1403
1403
1404 """
1404 """
1405 repo = get_repo_or_error(repoid)
1405 repo = get_repo_or_error(repoid)
1406 repo_name = repo.repo_name
1406 repo_name = repo.repo_name
1407
1407
1408 _repo = RepoModel().get_by_repo_name(fork_name)
1408 _repo = RepoModel().get_by_repo_name(fork_name)
1409 if _repo:
1409 if _repo:
1410 type_ = 'fork' if _repo.fork else 'repo'
1410 type_ = 'fork' if _repo.fork else 'repo'
1411 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1411 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1412
1412
1413 if HasPermissionAny('hg.admin')():
1413 if HasPermissionAny('hg.admin')():
1414 pass
1414 pass
1415 elif HasRepoPermissionLevel('read')(repo.repo_name):
1415 elif HasRepoPermissionLevel('read')(repo.repo_name):
1416 if not isinstance(owner, Optional):
1416 if not isinstance(owner, Optional):
1417 # forbid setting owner for non-admins
1417 # forbid setting owner for non-admins
1418 raise JSONRPCError(
1418 raise JSONRPCError(
1419 'Only Kallithea admin can specify `owner` param'
1419 'Only Kallithea admin can specify `owner` param'
1420 )
1420 )
1421
1421
1422 if not HasPermissionAny('hg.create.repository')():
1422 if not HasPermissionAny('hg.create.repository')():
1423 raise JSONRPCError('no permission to create repositories')
1423 raise JSONRPCError('no permission to create repositories')
1424 else:
1424 else:
1425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1425 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1426
1426
1427 if isinstance(owner, Optional):
1427 if isinstance(owner, Optional):
1428 owner = request.authuser.user_id
1428 owner = request.authuser.user_id
1429
1429
1430 owner = get_user_or_error(owner)
1430 owner = get_user_or_error(owner)
1431
1431
1432 try:
1432 try:
1433 fork_name_parts = fork_name.split('/')
1433 fork_name_parts = fork_name.split('/')
1434 repo_group = None
1434 repo_group = None
1435 if len(fork_name_parts) > 1:
1435 if len(fork_name_parts) > 1:
1436 group_name = '/'.join(fork_name_parts[:-1])
1436 group_name = '/'.join(fork_name_parts[:-1])
1437 repo_group = RepoGroup.get_by_group_name(group_name)
1437 repo_group = RepoGroup.get_by_group_name(group_name)
1438 if repo_group is None:
1438 if repo_group is None:
1439 raise JSONRPCError("repo group `%s` not found" % group_name)
1439 raise JSONRPCError("repo group `%s` not found" % group_name)
1440
1440
1441 form_data = dict(
1441 form_data = dict(
1442 repo_name=fork_name_parts[-1],
1442 repo_name=fork_name_parts[-1],
1443 repo_name_full=fork_name,
1443 repo_name_full=fork_name,
1444 repo_group=repo_group,
1444 repo_group=repo_group,
1445 repo_type=repo.repo_type,
1445 repo_type=repo.repo_type,
1446 description=Optional.extract(description),
1446 description=Optional.extract(description),
1447 private=Optional.extract(private),
1447 private=Optional.extract(private),
1448 copy_permissions=Optional.extract(copy_permissions),
1448 copy_permissions=Optional.extract(copy_permissions),
1449 landing_rev=Optional.extract(landing_rev),
1449 landing_rev=Optional.extract(landing_rev),
1450 update_after_clone=False,
1450 update_after_clone=False,
1451 fork_parent_id=repo.repo_id,
1451 fork_parent_id=repo.repo_id,
1452 )
1452 )
1453 task = RepoModel().create_fork(form_data, cur_user=owner)
1453 task = RepoModel().create_fork(form_data, cur_user=owner)
1454 # no commit, it's done in RepoModel, or async via celery
1454 # no commit, it's done in RepoModel, or async via celery
1455 task_id = task.task_id
1455 task_id = task.task_id
1456 return dict(
1456 return dict(
1457 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1457 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1458 fork_name),
1458 fork_name),
1459 success=True, # cannot return the repo data here since fork
1459 success=True, # cannot return the repo data here since fork
1460 # can be done async
1460 # can be done async
1461 task=task_id
1461 task=task_id
1462 )
1462 )
1463 except Exception:
1463 except Exception:
1464 log.error(traceback.format_exc())
1464 log.error(traceback.format_exc())
1465 raise JSONRPCError(
1465 raise JSONRPCError(
1466 'failed to fork repository `%s` as `%s`' % (repo_name,
1466 'failed to fork repository `%s` as `%s`' % (repo_name,
1467 fork_name)
1467 fork_name)
1468 )
1468 )
1469
1469
1470 # permission check inside
1470 # permission check inside
1471 def delete_repo(self, repoid, forks=Optional('')):
1471 def delete_repo(self, repoid, forks=Optional('')):
1472 """
1472 """
1473 Deletes a repository. This command can be executed only using api_key belonging
1473 Deletes a repository. This command can be executed only using api_key belonging
1474 to user with admin rights or regular user that have admin access to repository.
1474 to user with admin rights or regular user that have admin access to repository.
1475 When `forks` param is set it's possible to detach or delete forks of deleting
1475 When `forks` param is set it's possible to detach or delete forks of deleting
1476 repository
1476 repository
1477
1477
1478 :param repoid: repository name or repository id
1478 :param repoid: repository name or repository id
1479 :type repoid: str or int
1479 :type repoid: str or int
1480 :param forks: `detach` or `delete`, what do do with attached forks for repo
1480 :param forks: `detach` or `delete`, what do do with attached forks for repo
1481 :type forks: Optional(str)
1481 :type forks: Optional(str)
1482
1482
1483 OUTPUT::
1483 OUTPUT::
1484
1484
1485 id : <id_given_in_input>
1485 id : <id_given_in_input>
1486 result: {
1486 result: {
1487 "msg": "Deleted repository `<reponame>`",
1487 "msg": "Deleted repository `<reponame>`",
1488 "success": true
1488 "success": true
1489 }
1489 }
1490 error: null
1490 error: null
1491
1491
1492 """
1492 """
1493 repo = get_repo_or_error(repoid)
1493 repo = get_repo_or_error(repoid)
1494
1494
1495 if not HasPermissionAny('hg.admin')():
1495 if not HasPermissionAny('hg.admin')():
1496 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1496 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1497 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1497 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1498
1498
1499 try:
1499 try:
1500 handle_forks = Optional.extract(forks)
1500 handle_forks = Optional.extract(forks)
1501 _forks_msg = ''
1501 _forks_msg = ''
1502 _forks = [f for f in repo.forks]
1502 _forks = [f for f in repo.forks]
1503 if handle_forks == 'detach':
1503 if handle_forks == 'detach':
1504 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1504 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1505 elif handle_forks == 'delete':
1505 elif handle_forks == 'delete':
1506 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1506 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1507 elif _forks:
1507 elif _forks:
1508 raise JSONRPCError(
1508 raise JSONRPCError(
1509 'Cannot delete `%s` it still contains attached forks' %
1509 'Cannot delete `%s` it still contains attached forks' %
1510 (repo.repo_name,)
1510 (repo.repo_name,)
1511 )
1511 )
1512
1512
1513 RepoModel().delete(repo, forks=forks)
1513 RepoModel().delete(repo, forks=forks)
1514 Session().commit()
1514 Session().commit()
1515 return dict(
1515 return dict(
1516 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1516 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1517 success=True
1517 success=True
1518 )
1518 )
1519 except Exception:
1519 except Exception:
1520 log.error(traceback.format_exc())
1520 log.error(traceback.format_exc())
1521 raise JSONRPCError(
1521 raise JSONRPCError(
1522 'failed to delete repository `%s`' % (repo.repo_name,)
1522 'failed to delete repository `%s`' % (repo.repo_name,)
1523 )
1523 )
1524
1524
1525 @HasPermissionAnyDecorator('hg.admin')
1525 @HasPermissionAnyDecorator('hg.admin')
1526 def grant_user_permission(self, repoid, userid, perm):
1526 def grant_user_permission(self, repoid, userid, perm):
1527 """
1527 """
1528 Grant permission for user on given repository, or update existing one
1528 Grant permission for user on given repository, or update existing one
1529 if found. This command can be executed only using api_key belonging to user
1529 if found. This command can be executed only using api_key belonging to user
1530 with admin rights.
1530 with admin rights.
1531
1531
1532 :param repoid: repository name or repository id
1532 :param repoid: repository name or repository id
1533 :type repoid: str or int
1533 :type repoid: str or int
1534 :param userid:
1534 :param userid:
1535 :param perm: (repository.(none|read|write|admin))
1535 :param perm: (repository.(none|read|write|admin))
1536 :type perm: str
1536 :type perm: str
1537
1537
1538 OUTPUT::
1538 OUTPUT::
1539
1539
1540 id : <id_given_in_input>
1540 id : <id_given_in_input>
1541 result: {
1541 result: {
1542 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1542 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1543 "success": true
1543 "success": true
1544 }
1544 }
1545 error: null
1545 error: null
1546 """
1546 """
1547 repo = get_repo_or_error(repoid)
1547 repo = get_repo_or_error(repoid)
1548 user = get_user_or_error(userid)
1548 user = get_user_or_error(userid)
1549 perm = get_perm_or_error(perm)
1549 perm = get_perm_or_error(perm)
1550
1550
1551 try:
1551 try:
1552
1552
1553 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1553 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1554
1554
1555 Session().commit()
1555 Session().commit()
1556 return dict(
1556 return dict(
1557 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1557 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1558 perm.permission_name, user.username, repo.repo_name
1558 perm.permission_name, user.username, repo.repo_name
1559 ),
1559 ),
1560 success=True
1560 success=True
1561 )
1561 )
1562 except Exception:
1562 except Exception:
1563 log.error(traceback.format_exc())
1563 log.error(traceback.format_exc())
1564 raise JSONRPCError(
1564 raise JSONRPCError(
1565 'failed to edit permission for user: `%s` in repo: `%s`' % (
1565 'failed to edit permission for user: `%s` in repo: `%s`' % (
1566 userid, repoid
1566 userid, repoid
1567 )
1567 )
1568 )
1568 )
1569
1569
1570 @HasPermissionAnyDecorator('hg.admin')
1570 @HasPermissionAnyDecorator('hg.admin')
1571 def revoke_user_permission(self, repoid, userid):
1571 def revoke_user_permission(self, repoid, userid):
1572 """
1572 """
1573 Revoke permission for user on given repository. This command can be executed
1573 Revoke permission for user on given repository. This command can be executed
1574 only using api_key belonging to user with admin rights.
1574 only using api_key belonging to user with admin rights.
1575
1575
1576 :param repoid: repository name or repository id
1576 :param repoid: repository name or repository id
1577 :type repoid: str or int
1577 :type repoid: str or int
1578 :param userid:
1578 :param userid:
1579
1579
1580 OUTPUT::
1580 OUTPUT::
1581
1581
1582 id : <id_given_in_input>
1582 id : <id_given_in_input>
1583 result: {
1583 result: {
1584 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1584 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1585 "success": true
1585 "success": true
1586 }
1586 }
1587 error: null
1587 error: null
1588
1588
1589 """
1589 """
1590
1590
1591 repo = get_repo_or_error(repoid)
1591 repo = get_repo_or_error(repoid)
1592 user = get_user_or_error(userid)
1592 user = get_user_or_error(userid)
1593 try:
1593 try:
1594 RepoModel().revoke_user_permission(repo=repo, user=user)
1594 RepoModel().revoke_user_permission(repo=repo, user=user)
1595 Session().commit()
1595 Session().commit()
1596 return dict(
1596 return dict(
1597 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1597 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1598 user.username, repo.repo_name
1598 user.username, repo.repo_name
1599 ),
1599 ),
1600 success=True
1600 success=True
1601 )
1601 )
1602 except Exception:
1602 except Exception:
1603 log.error(traceback.format_exc())
1603 log.error(traceback.format_exc())
1604 raise JSONRPCError(
1604 raise JSONRPCError(
1605 'failed to edit permission for user: `%s` in repo: `%s`' % (
1605 'failed to edit permission for user: `%s` in repo: `%s`' % (
1606 userid, repoid
1606 userid, repoid
1607 )
1607 )
1608 )
1608 )
1609
1609
1610 # permission check inside
1610 # permission check inside
1611 def grant_user_group_permission(self, repoid, usergroupid, perm):
1611 def grant_user_group_permission(self, repoid, usergroupid, perm):
1612 """
1612 """
1613 Grant permission for user group on given repository, or update
1613 Grant permission for user group on given repository, or update
1614 existing one if found. This command can be executed only using
1614 existing one if found. This command can be executed only using
1615 api_key belonging to user with admin rights.
1615 api_key belonging to user with admin rights.
1616
1616
1617 :param repoid: repository name or repository id
1617 :param repoid: repository name or repository id
1618 :type repoid: str or int
1618 :type repoid: str or int
1619 :param usergroupid: id of usergroup
1619 :param usergroupid: id of usergroup
1620 :type usergroupid: str or int
1620 :type usergroupid: str or int
1621 :param perm: (repository.(none|read|write|admin))
1621 :param perm: (repository.(none|read|write|admin))
1622 :type perm: str
1622 :type perm: str
1623
1623
1624 OUTPUT::
1624 OUTPUT::
1625
1625
1626 id : <id_given_in_input>
1626 id : <id_given_in_input>
1627 result : {
1627 result : {
1628 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1628 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1629 "success": true
1629 "success": true
1630
1630
1631 }
1631 }
1632 error : null
1632 error : null
1633
1633
1634 ERROR OUTPUT::
1634 ERROR OUTPUT::
1635
1635
1636 id : <id_given_in_input>
1636 id : <id_given_in_input>
1637 result : null
1637 result : null
1638 error : {
1638 error : {
1639 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1639 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1640 }
1640 }
1641
1641
1642 """
1642 """
1643 repo = get_repo_or_error(repoid)
1643 repo = get_repo_or_error(repoid)
1644 perm = get_perm_or_error(perm)
1644 perm = get_perm_or_error(perm)
1645 user_group = get_user_group_or_error(usergroupid)
1645 user_group = get_user_group_or_error(usergroupid)
1646 if not HasPermissionAny('hg.admin')():
1646 if not HasPermissionAny('hg.admin')():
1647 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1647 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1648 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1648 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1649
1649
1650 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
1650 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
1651 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1651 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1652
1652
1653 try:
1653 try:
1654 RepoModel().grant_user_group_permission(
1654 RepoModel().grant_user_group_permission(
1655 repo=repo, group_name=user_group, perm=perm)
1655 repo=repo, group_name=user_group, perm=perm)
1656
1656
1657 Session().commit()
1657 Session().commit()
1658 return dict(
1658 return dict(
1659 msg='Granted perm: `%s` for user group: `%s` in '
1659 msg='Granted perm: `%s` for user group: `%s` in '
1660 'repo: `%s`' % (
1660 'repo: `%s`' % (
1661 perm.permission_name, user_group.users_group_name,
1661 perm.permission_name, user_group.users_group_name,
1662 repo.repo_name
1662 repo.repo_name
1663 ),
1663 ),
1664 success=True
1664 success=True
1665 )
1665 )
1666 except Exception:
1666 except Exception:
1667 log.error(traceback.format_exc())
1667 log.error(traceback.format_exc())
1668 raise JSONRPCError(
1668 raise JSONRPCError(
1669 'failed to edit permission for user group: `%s` in '
1669 'failed to edit permission for user group: `%s` in '
1670 'repo: `%s`' % (
1670 'repo: `%s`' % (
1671 usergroupid, repo.repo_name
1671 usergroupid, repo.repo_name
1672 )
1672 )
1673 )
1673 )
1674
1674
1675 # permission check inside
1675 # permission check inside
1676 def revoke_user_group_permission(self, repoid, usergroupid):
1676 def revoke_user_group_permission(self, repoid, usergroupid):
1677 """
1677 """
1678 Revoke permission for user group on given repository. This command can be
1678 Revoke permission for user group on given repository. This command can be
1679 executed only using api_key belonging to user with admin rights.
1679 executed only using api_key belonging to user with admin rights.
1680
1680
1681 :param repoid: repository name or repository id
1681 :param repoid: repository name or repository id
1682 :type repoid: str or int
1682 :type repoid: str or int
1683 :param usergroupid:
1683 :param usergroupid:
1684
1684
1685 OUTPUT::
1685 OUTPUT::
1686
1686
1687 id : <id_given_in_input>
1687 id : <id_given_in_input>
1688 result: {
1688 result: {
1689 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1689 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1690 "success": true
1690 "success": true
1691 }
1691 }
1692 error: null
1692 error: null
1693 """
1693 """
1694 repo = get_repo_or_error(repoid)
1694 repo = get_repo_or_error(repoid)
1695 user_group = get_user_group_or_error(usergroupid)
1695 user_group = get_user_group_or_error(usergroupid)
1696 if not HasPermissionAny('hg.admin')():
1696 if not HasPermissionAny('hg.admin')():
1697 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1697 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1698 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1698 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1699
1699
1700 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
1700 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
1701 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1701 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1702
1702
1703 try:
1703 try:
1704 RepoModel().revoke_user_group_permission(
1704 RepoModel().revoke_user_group_permission(
1705 repo=repo, group_name=user_group)
1705 repo=repo, group_name=user_group)
1706
1706
1707 Session().commit()
1707 Session().commit()
1708 return dict(
1708 return dict(
1709 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1709 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1710 user_group.users_group_name, repo.repo_name
1710 user_group.users_group_name, repo.repo_name
1711 ),
1711 ),
1712 success=True
1712 success=True
1713 )
1713 )
1714 except Exception:
1714 except Exception:
1715 log.error(traceback.format_exc())
1715 log.error(traceback.format_exc())
1716 raise JSONRPCError(
1716 raise JSONRPCError(
1717 'failed to edit permission for user group: `%s` in '
1717 'failed to edit permission for user group: `%s` in '
1718 'repo: `%s`' % (
1718 'repo: `%s`' % (
1719 user_group.users_group_name, repo.repo_name
1719 user_group.users_group_name, repo.repo_name
1720 )
1720 )
1721 )
1721 )
1722
1722
1723 @HasPermissionAnyDecorator('hg.admin')
1723 @HasPermissionAnyDecorator('hg.admin')
1724 def get_repo_group(self, repogroupid):
1724 def get_repo_group(self, repogroupid):
1725 """
1725 """
1726 Returns given repo group together with permissions, and repositories
1726 Returns given repo group together with permissions, and repositories
1727 inside the group
1727 inside the group
1728
1728
1729 :param repogroupid: id/name of repository group
1729 :param repogroupid: id/name of repository group
1730 :type repogroupid: str or int
1730 :type repogroupid: str or int
1731 """
1731 """
1732 repo_group = get_repo_group_or_error(repogroupid)
1732 repo_group = get_repo_group_or_error(repogroupid)
1733
1733
1734 members = []
1734 members = []
1735 for user in repo_group.repo_group_to_perm:
1735 for user in repo_group.repo_group_to_perm:
1736 perm = user.permission.permission_name
1736 perm = user.permission.permission_name
1737 user = user.user
1737 user = user.user
1738 user_data = {
1738 user_data = {
1739 'name': user.username,
1739 'name': user.username,
1740 'type': "user",
1740 'type': "user",
1741 'permission': perm
1741 'permission': perm
1742 }
1742 }
1743 members.append(user_data)
1743 members.append(user_data)
1744
1744
1745 for user_group in repo_group.users_group_to_perm:
1745 for user_group in repo_group.users_group_to_perm:
1746 perm = user_group.permission.permission_name
1746 perm = user_group.permission.permission_name
1747 user_group = user_group.users_group
1747 user_group = user_group.users_group
1748 user_group_data = {
1748 user_group_data = {
1749 'name': user_group.users_group_name,
1749 'name': user_group.users_group_name,
1750 'type': "user_group",
1750 'type': "user_group",
1751 'permission': perm
1751 'permission': perm
1752 }
1752 }
1753 members.append(user_group_data)
1753 members.append(user_group_data)
1754
1754
1755 data = repo_group.get_api_data()
1755 data = repo_group.get_api_data()
1756 data["members"] = members
1756 data["members"] = members
1757 return data
1757 return data
1758
1758
1759 @HasPermissionAnyDecorator('hg.admin')
1759 @HasPermissionAnyDecorator('hg.admin')
1760 def get_repo_groups(self):
1760 def get_repo_groups(self):
1761 """
1761 """
1762 Returns all repository groups
1762 Returns all repository groups
1763
1763
1764 """
1764 """
1765 return [
1765 return [
1766 repo_group.get_api_data()
1766 repo_group.get_api_data()
1767 for repo_group in RepoGroup.query()
1767 for repo_group in RepoGroup.query()
1768 ]
1768 ]
1769
1769
1770 @HasPermissionAnyDecorator('hg.admin')
1770 @HasPermissionAnyDecorator('hg.admin')
1771 def create_repo_group(self, group_name, description=Optional(''),
1771 def create_repo_group(self, group_name, description=Optional(''),
1772 owner=Optional(OAttr('apiuser')),
1772 owner=Optional(OAttr('apiuser')),
1773 parent=Optional(None),
1773 parent=Optional(None),
1774 copy_permissions=Optional(False)):
1774 copy_permissions=Optional(False)):
1775 """
1775 """
1776 Creates a repository group. This command can be executed only using
1776 Creates a repository group. This command can be executed only using
1777 api_key belonging to user with admin rights.
1777 api_key belonging to user with admin rights.
1778
1778
1779 :param group_name:
1779 :param group_name:
1780 :type group_name:
1780 :type group_name:
1781 :param description:
1781 :param description:
1782 :type description:
1782 :type description:
1783 :param owner:
1783 :param owner:
1784 :type owner:
1784 :type owner:
1785 :param parent:
1785 :param parent:
1786 :type parent:
1786 :type parent:
1787 :param copy_permissions:
1787 :param copy_permissions:
1788 :type copy_permissions:
1788 :type copy_permissions:
1789
1789
1790 OUTPUT::
1790 OUTPUT::
1791
1791
1792 id : <id_given_in_input>
1792 id : <id_given_in_input>
1793 result : {
1793 result : {
1794 "msg": "created new repo group `<repo_group_name>`"
1794 "msg": "created new repo group `<repo_group_name>`"
1795 "repo_group": <repogroup_object>
1795 "repo_group": <repogroup_object>
1796 }
1796 }
1797 error : null
1797 error : null
1798
1798
1799 ERROR OUTPUT::
1799 ERROR OUTPUT::
1800
1800
1801 id : <id_given_in_input>
1801 id : <id_given_in_input>
1802 result : null
1802 result : null
1803 error : {
1803 error : {
1804 failed to create repo group `<repogroupid>`
1804 failed to create repo group `<repogroupid>`
1805 }
1805 }
1806
1806
1807 """
1807 """
1808 if RepoGroup.get_by_group_name(group_name):
1808 if RepoGroup.get_by_group_name(group_name):
1809 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
1809 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
1810
1810
1811 if isinstance(owner, Optional):
1811 if isinstance(owner, Optional):
1812 owner = request.authuser.user_id
1812 owner = request.authuser.user_id
1813 group_description = Optional.extract(description)
1813 group_description = Optional.extract(description)
1814 parent_group = Optional.extract(parent)
1814 parent_group = Optional.extract(parent)
1815 if not isinstance(parent, Optional):
1815 if not isinstance(parent, Optional):
1816 parent_group = get_repo_group_or_error(parent_group)
1816 parent_group = get_repo_group_or_error(parent_group)
1817
1817
1818 copy_permissions = Optional.extract(copy_permissions)
1818 copy_permissions = Optional.extract(copy_permissions)
1819 try:
1819 try:
1820 repo_group = RepoGroupModel().create(
1820 repo_group = RepoGroupModel().create(
1821 group_name=group_name,
1821 group_name=group_name,
1822 group_description=group_description,
1822 group_description=group_description,
1823 owner=owner,
1823 owner=owner,
1824 parent=parent_group,
1824 parent=parent_group,
1825 copy_permissions=copy_permissions
1825 copy_permissions=copy_permissions
1826 )
1826 )
1827 Session().commit()
1827 Session().commit()
1828 return dict(
1828 return dict(
1829 msg='created new repo group `%s`' % group_name,
1829 msg='created new repo group `%s`' % group_name,
1830 repo_group=repo_group.get_api_data()
1830 repo_group=repo_group.get_api_data()
1831 )
1831 )
1832 except Exception:
1832 except Exception:
1833
1833
1834 log.error(traceback.format_exc())
1834 log.error(traceback.format_exc())
1835 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
1835 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
1836
1836
1837 @HasPermissionAnyDecorator('hg.admin')
1837 @HasPermissionAnyDecorator('hg.admin')
1838 def update_repo_group(self, repogroupid, group_name=Optional(''),
1838 def update_repo_group(self, repogroupid, group_name=Optional(''),
1839 description=Optional(''),
1839 description=Optional(''),
1840 owner=Optional(OAttr('apiuser')),
1840 owner=Optional(OAttr('apiuser')),
1841 parent=Optional(None)):
1841 parent=Optional(None)):
1842 repo_group = get_repo_group_or_error(repogroupid)
1842 repo_group = get_repo_group_or_error(repogroupid)
1843
1843
1844 updates = {}
1844 updates = {}
1845 try:
1845 try:
1846 store_update(updates, group_name, 'group_name')
1846 store_update(updates, group_name, 'group_name')
1847 store_update(updates, description, 'group_description')
1847 store_update(updates, description, 'group_description')
1848 store_update(updates, owner, 'owner')
1848 store_update(updates, owner, 'owner')
1849 store_update(updates, parent, 'parent_group')
1849 store_update(updates, parent, 'parent_group')
1850 repo_group = RepoGroupModel().update(repo_group, updates)
1850 repo_group = RepoGroupModel().update(repo_group, updates)
1851 Session().commit()
1851 Session().commit()
1852 return dict(
1852 return dict(
1853 msg='updated repository group ID:%s %s' % (repo_group.group_id,
1853 msg='updated repository group ID:%s %s' % (repo_group.group_id,
1854 repo_group.group_name),
1854 repo_group.group_name),
1855 repo_group=repo_group.get_api_data()
1855 repo_group=repo_group.get_api_data()
1856 )
1856 )
1857 except Exception:
1857 except Exception:
1858 log.error(traceback.format_exc())
1858 log.error(traceback.format_exc())
1859 raise JSONRPCError('failed to update repository group `%s`'
1859 raise JSONRPCError('failed to update repository group `%s`'
1860 % (repogroupid,))
1860 % (repogroupid,))
1861
1861
1862 @HasPermissionAnyDecorator('hg.admin')
1862 @HasPermissionAnyDecorator('hg.admin')
1863 def delete_repo_group(self, repogroupid):
1863 def delete_repo_group(self, repogroupid):
1864 """
1864 """
1865
1865
1866 :param repogroupid: name or id of repository group
1866 :param repogroupid: name or id of repository group
1867 :type repogroupid: str or int
1867 :type repogroupid: str or int
1868
1868
1869 OUTPUT::
1869 OUTPUT::
1870
1870
1871 id : <id_given_in_input>
1871 id : <id_given_in_input>
1872 result : {
1872 result : {
1873 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
1873 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
1874 'repo_group': null
1874 'repo_group': null
1875 }
1875 }
1876 error : null
1876 error : null
1877
1877
1878 ERROR OUTPUT::
1878 ERROR OUTPUT::
1879
1879
1880 id : <id_given_in_input>
1880 id : <id_given_in_input>
1881 result : null
1881 result : null
1882 error : {
1882 error : {
1883 "failed to delete repo group ID:<repogroupid> <repogroupname>"
1883 "failed to delete repo group ID:<repogroupid> <repogroupname>"
1884 }
1884 }
1885
1885
1886 """
1886 """
1887 repo_group = get_repo_group_or_error(repogroupid)
1887 repo_group = get_repo_group_or_error(repogroupid)
1888
1888
1889 try:
1889 try:
1890 RepoGroupModel().delete(repo_group)
1890 RepoGroupModel().delete(repo_group)
1891 Session().commit()
1891 Session().commit()
1892 return dict(
1892 return dict(
1893 msg='deleted repo group ID:%s %s' %
1893 msg='deleted repo group ID:%s %s' %
1894 (repo_group.group_id, repo_group.group_name),
1894 (repo_group.group_id, repo_group.group_name),
1895 repo_group=None
1895 repo_group=None
1896 )
1896 )
1897 except Exception:
1897 except Exception:
1898 log.error(traceback.format_exc())
1898 log.error(traceback.format_exc())
1899 raise JSONRPCError('failed to delete repo group ID:%s %s' %
1899 raise JSONRPCError('failed to delete repo group ID:%s %s' %
1900 (repo_group.group_id, repo_group.group_name)
1900 (repo_group.group_id, repo_group.group_name)
1901 )
1901 )
1902
1902
1903 # permission check inside
1903 # permission check inside
1904 def grant_user_permission_to_repo_group(self, repogroupid, userid,
1904 def grant_user_permission_to_repo_group(self, repogroupid, userid,
1905 perm, apply_to_children=Optional('none')):
1905 perm, apply_to_children=Optional('none')):
1906 """
1906 """
1907 Grant permission for user on given repository group, or update existing
1907 Grant permission for user on given repository group, or update existing
1908 one if found. This command can be executed only using api_key belonging
1908 one if found. This command can be executed only using api_key belonging
1909 to user with admin rights, or user who has admin right to given repository
1909 to user with admin rights, or user who has admin right to given repository
1910 group.
1910 group.
1911
1911
1912 :param repogroupid: name or id of repository group
1912 :param repogroupid: name or id of repository group
1913 :type repogroupid: str or int
1913 :type repogroupid: str or int
1914 :param userid:
1914 :param userid:
1915 :param perm: (group.(none|read|write|admin))
1915 :param perm: (group.(none|read|write|admin))
1916 :type perm: str
1916 :type perm: str
1917 :param apply_to_children: 'none', 'repos', 'groups', 'all'
1917 :param apply_to_children: 'none', 'repos', 'groups', 'all'
1918 :type apply_to_children: str
1918 :type apply_to_children: str
1919
1919
1920 OUTPUT::
1920 OUTPUT::
1921
1921
1922 id : <id_given_in_input>
1922 id : <id_given_in_input>
1923 result: {
1923 result: {
1924 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
1924 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
1925 "success": true
1925 "success": true
1926 }
1926 }
1927 error: null
1927 error: null
1928
1928
1929 ERROR OUTPUT::
1929 ERROR OUTPUT::
1930
1930
1931 id : <id_given_in_input>
1931 id : <id_given_in_input>
1932 result : null
1932 result : null
1933 error : {
1933 error : {
1934 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
1934 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
1935 }
1935 }
1936
1936
1937 """
1937 """
1938
1938
1939 repo_group = get_repo_group_or_error(repogroupid)
1939 repo_group = get_repo_group_or_error(repogroupid)
1940
1940
1941 if not HasPermissionAny('hg.admin')():
1941 if not HasPermissionAny('hg.admin')():
1942 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
1942 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
1943 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
1943 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
1944
1944
1945 user = get_user_or_error(userid)
1945 user = get_user_or_error(userid)
1946 perm = get_perm_or_error(perm, prefix='group.')
1946 perm = get_perm_or_error(perm, prefix='group.')
1947 apply_to_children = Optional.extract(apply_to_children)
1947 apply_to_children = Optional.extract(apply_to_children)
1948
1948
1949 try:
1949 try:
1950 RepoGroupModel().add_permission(repo_group=repo_group,
1950 RepoGroupModel().add_permission(repo_group=repo_group,
1951 obj=user,
1951 obj=user,
1952 obj_type="user",
1952 obj_type="user",
1953 perm=perm,
1953 perm=perm,
1954 recursive=apply_to_children)
1954 recursive=apply_to_children)
1955 Session().commit()
1955 Session().commit()
1956 return dict(
1956 return dict(
1957 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1957 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1958 perm.permission_name, apply_to_children, user.username, repo_group.name
1958 perm.permission_name, apply_to_children, user.username, repo_group.name
1959 ),
1959 ),
1960 success=True
1960 success=True
1961 )
1961 )
1962 except Exception:
1962 except Exception:
1963 log.error(traceback.format_exc())
1963 log.error(traceback.format_exc())
1964 raise JSONRPCError(
1964 raise JSONRPCError(
1965 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1965 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1966 userid, repo_group.name))
1966 userid, repo_group.name))
1967
1967
1968 # permission check inside
1968 # permission check inside
1969 def revoke_user_permission_from_repo_group(self, repogroupid, userid,
1969 def revoke_user_permission_from_repo_group(self, repogroupid, userid,
1970 apply_to_children=Optional('none')):
1970 apply_to_children=Optional('none')):
1971 """
1971 """
1972 Revoke permission for user on given repository group. This command can
1972 Revoke permission for user on given repository group. This command can
1973 be executed only using api_key belonging to user with admin rights, or
1973 be executed only using api_key belonging to user with admin rights, or
1974 user who has admin right to given repository group.
1974 user who has admin right to given repository group.
1975
1975
1976 :param repogroupid: name or id of repository group
1976 :param repogroupid: name or id of repository group
1977 :type repogroupid: str or int
1977 :type repogroupid: str or int
1978 :param userid:
1978 :param userid:
1979 :type userid:
1979 :type userid:
1980 :param apply_to_children: 'none', 'repos', 'groups', 'all'
1980 :param apply_to_children: 'none', 'repos', 'groups', 'all'
1981 :type apply_to_children: str
1981 :type apply_to_children: str
1982
1982
1983 OUTPUT::
1983 OUTPUT::
1984
1984
1985 id : <id_given_in_input>
1985 id : <id_given_in_input>
1986 result: {
1986 result: {
1987 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
1987 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
1988 "success": true
1988 "success": true
1989 }
1989 }
1990 error: null
1990 error: null
1991
1991
1992 ERROR OUTPUT::
1992 ERROR OUTPUT::
1993
1993
1994 id : <id_given_in_input>
1994 id : <id_given_in_input>
1995 result : null
1995 result : null
1996 error : {
1996 error : {
1997 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
1997 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
1998 }
1998 }
1999
1999
2000 """
2000 """
2001
2001
2002 repo_group = get_repo_group_or_error(repogroupid)
2002 repo_group = get_repo_group_or_error(repogroupid)
2003
2003
2004 if not HasPermissionAny('hg.admin')():
2004 if not HasPermissionAny('hg.admin')():
2005 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2005 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2006 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2006 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2007
2007
2008 user = get_user_or_error(userid)
2008 user = get_user_or_error(userid)
2009 apply_to_children = Optional.extract(apply_to_children)
2009 apply_to_children = Optional.extract(apply_to_children)
2010
2010
2011 try:
2011 try:
2012 RepoGroupModel().delete_permission(repo_group=repo_group,
2012 RepoGroupModel().delete_permission(repo_group=repo_group,
2013 obj=user,
2013 obj=user,
2014 obj_type="user",
2014 obj_type="user",
2015 recursive=apply_to_children)
2015 recursive=apply_to_children)
2016
2016
2017 Session().commit()
2017 Session().commit()
2018 return dict(
2018 return dict(
2019 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2019 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2020 apply_to_children, user.username, repo_group.name
2020 apply_to_children, user.username, repo_group.name
2021 ),
2021 ),
2022 success=True
2022 success=True
2023 )
2023 )
2024 except Exception:
2024 except Exception:
2025 log.error(traceback.format_exc())
2025 log.error(traceback.format_exc())
2026 raise JSONRPCError(
2026 raise JSONRPCError(
2027 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2027 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2028 userid, repo_group.name))
2028 userid, repo_group.name))
2029
2029
2030 # permission check inside
2030 # permission check inside
2031 def grant_user_group_permission_to_repo_group(
2031 def grant_user_group_permission_to_repo_group(
2032 self, repogroupid, usergroupid, perm,
2032 self, repogroupid, usergroupid, perm,
2033 apply_to_children=Optional('none')):
2033 apply_to_children=Optional('none')):
2034 """
2034 """
2035 Grant permission for user group on given repository group, or update
2035 Grant permission for user group on given repository group, or update
2036 existing one if found. This command can be executed only using
2036 existing one if found. This command can be executed only using
2037 api_key belonging to user with admin rights, or user who has admin
2037 api_key belonging to user with admin rights, or user who has admin
2038 right to given repository group.
2038 right to given repository group.
2039
2039
2040 :param repogroupid: name or id of repository group
2040 :param repogroupid: name or id of repository group
2041 :type repogroupid: str or int
2041 :type repogroupid: str or int
2042 :param usergroupid: id of usergroup
2042 :param usergroupid: id of usergroup
2043 :type usergroupid: str or int
2043 :type usergroupid: str or int
2044 :param perm: (group.(none|read|write|admin))
2044 :param perm: (group.(none|read|write|admin))
2045 :type perm: str
2045 :type perm: str
2046 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2046 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2047 :type apply_to_children: str
2047 :type apply_to_children: str
2048
2048
2049 OUTPUT::
2049 OUTPUT::
2050
2050
2051 id : <id_given_in_input>
2051 id : <id_given_in_input>
2052 result : {
2052 result : {
2053 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2053 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2054 "success": true
2054 "success": true
2055
2055
2056 }
2056 }
2057 error : null
2057 error : null
2058
2058
2059 ERROR OUTPUT::
2059 ERROR OUTPUT::
2060
2060
2061 id : <id_given_in_input>
2061 id : <id_given_in_input>
2062 result : null
2062 result : null
2063 error : {
2063 error : {
2064 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2064 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2065 }
2065 }
2066
2066
2067 """
2067 """
2068 repo_group = get_repo_group_or_error(repogroupid)
2068 repo_group = get_repo_group_or_error(repogroupid)
2069 perm = get_perm_or_error(perm, prefix='group.')
2069 perm = get_perm_or_error(perm, prefix='group.')
2070 user_group = get_user_group_or_error(usergroupid)
2070 user_group = get_user_group_or_error(usergroupid)
2071 if not HasPermissionAny('hg.admin')():
2071 if not HasPermissionAny('hg.admin')():
2072 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2072 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2073 raise JSONRPCError(
2073 raise JSONRPCError(
2074 'repository group `%s` does not exist' % (repogroupid,))
2074 'repository group `%s` does not exist' % (repogroupid,))
2075
2075
2076 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
2076 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
2077 raise JSONRPCError(
2077 raise JSONRPCError(
2078 'user group `%s` does not exist' % (usergroupid,))
2078 'user group `%s` does not exist' % (usergroupid,))
2079
2079
2080 apply_to_children = Optional.extract(apply_to_children)
2080 apply_to_children = Optional.extract(apply_to_children)
2081
2081
2082 try:
2082 try:
2083 RepoGroupModel().add_permission(repo_group=repo_group,
2083 RepoGroupModel().add_permission(repo_group=repo_group,
2084 obj=user_group,
2084 obj=user_group,
2085 obj_type="user_group",
2085 obj_type="user_group",
2086 perm=perm,
2086 perm=perm,
2087 recursive=apply_to_children)
2087 recursive=apply_to_children)
2088 Session().commit()
2088 Session().commit()
2089 return dict(
2089 return dict(
2090 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2090 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2091 perm.permission_name, apply_to_children,
2091 perm.permission_name, apply_to_children,
2092 user_group.users_group_name, repo_group.name
2092 user_group.users_group_name, repo_group.name
2093 ),
2093 ),
2094 success=True
2094 success=True
2095 )
2095 )
2096 except Exception:
2096 except Exception:
2097 log.error(traceback.format_exc())
2097 log.error(traceback.format_exc())
2098 raise JSONRPCError(
2098 raise JSONRPCError(
2099 'failed to edit permission for user group: `%s` in '
2099 'failed to edit permission for user group: `%s` in '
2100 'repo group: `%s`' % (
2100 'repo group: `%s`' % (
2101 usergroupid, repo_group.name
2101 usergroupid, repo_group.name
2102 )
2102 )
2103 )
2103 )
2104
2104
2105 # permission check inside
2105 # permission check inside
2106 def revoke_user_group_permission_from_repo_group(
2106 def revoke_user_group_permission_from_repo_group(
2107 self, repogroupid, usergroupid,
2107 self, repogroupid, usergroupid,
2108 apply_to_children=Optional('none')):
2108 apply_to_children=Optional('none')):
2109 """
2109 """
2110 Revoke permission for user group on given repository. This command can be
2110 Revoke permission for user group on given repository. This command can be
2111 executed only using api_key belonging to user with admin rights, or
2111 executed only using api_key belonging to user with admin rights, or
2112 user who has admin right to given repository group.
2112 user who has admin right to given repository group.
2113
2113
2114 :param repogroupid: name or id of repository group
2114 :param repogroupid: name or id of repository group
2115 :type repogroupid: str or int
2115 :type repogroupid: str or int
2116 :param usergroupid:
2116 :param usergroupid:
2117 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2117 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2118 :type apply_to_children: str
2118 :type apply_to_children: str
2119
2119
2120 OUTPUT::
2120 OUTPUT::
2121
2121
2122 id : <id_given_in_input>
2122 id : <id_given_in_input>
2123 result: {
2123 result: {
2124 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2124 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2125 "success": true
2125 "success": true
2126 }
2126 }
2127 error: null
2127 error: null
2128
2128
2129 ERROR OUTPUT::
2129 ERROR OUTPUT::
2130
2130
2131 id : <id_given_in_input>
2131 id : <id_given_in_input>
2132 result : null
2132 result : null
2133 error : {
2133 error : {
2134 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2134 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2135 }
2135 }
2136
2136
2137
2137
2138 """
2138 """
2139 repo_group = get_repo_group_or_error(repogroupid)
2139 repo_group = get_repo_group_or_error(repogroupid)
2140 user_group = get_user_group_or_error(usergroupid)
2140 user_group = get_user_group_or_error(usergroupid)
2141 if not HasPermissionAny('hg.admin')():
2141 if not HasPermissionAny('hg.admin')():
2142 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2142 if not HasRepoGroupPermissionLevel('admin')(repo_group.group_name):
2143 raise JSONRPCError(
2143 raise JSONRPCError(
2144 'repository group `%s` does not exist' % (repogroupid,))
2144 'repository group `%s` does not exist' % (repogroupid,))
2145
2145
2146 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
2146 if not HasUserGroupPermissionLevel('read')(user_group.users_group_name):
2147 raise JSONRPCError(
2147 raise JSONRPCError(
2148 'user group `%s` does not exist' % (usergroupid,))
2148 'user group `%s` does not exist' % (usergroupid,))
2149
2149
2150 apply_to_children = Optional.extract(apply_to_children)
2150 apply_to_children = Optional.extract(apply_to_children)
2151
2151
2152 try:
2152 try:
2153 RepoGroupModel().delete_permission(repo_group=repo_group,
2153 RepoGroupModel().delete_permission(repo_group=repo_group,
2154 obj=user_group,
2154 obj=user_group,
2155 obj_type="user_group",
2155 obj_type="user_group",
2156 recursive=apply_to_children)
2156 recursive=apply_to_children)
2157 Session().commit()
2157 Session().commit()
2158 return dict(
2158 return dict(
2159 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2159 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2160 apply_to_children, user_group.users_group_name, repo_group.name
2160 apply_to_children, user_group.users_group_name, repo_group.name
2161 ),
2161 ),
2162 success=True
2162 success=True
2163 )
2163 )
2164 except Exception:
2164 except Exception:
2165 log.error(traceback.format_exc())
2165 log.error(traceback.format_exc())
2166 raise JSONRPCError(
2166 raise JSONRPCError(
2167 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2167 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2168 user_group.users_group_name, repo_group.name
2168 user_group.users_group_name, repo_group.name
2169 )
2169 )
2170 )
2170 )
2171
2171
2172 def get_gist(self, gistid):
2172 def get_gist(self, gistid):
2173 """
2173 """
2174 Get given gist by id
2174 Get given gist by id
2175
2175
2176 :param gistid: id of private or public gist
2176 :param gistid: id of private or public gist
2177 :type gistid: str
2177 :type gistid: str
2178 """
2178 """
2179 gist = get_gist_or_error(gistid)
2179 gist = get_gist_or_error(gistid)
2180 if not HasPermissionAny('hg.admin')():
2180 if not HasPermissionAny('hg.admin')():
2181 if gist.owner_id != request.authuser.user_id:
2181 if gist.owner_id != request.authuser.user_id:
2182 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2182 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2183 return gist.get_api_data()
2183 return gist.get_api_data()
2184
2184
2185 def get_gists(self, userid=Optional(OAttr('apiuser'))):
2185 def get_gists(self, userid=Optional(OAttr('apiuser'))):
2186 """
2186 """
2187 Get all gists for given user. If userid is empty returned gists
2187 Get all gists for given user. If userid is empty returned gists
2188 are for user who called the api
2188 are for user who called the api
2189
2189
2190 :param userid: user to get gists for
2190 :param userid: user to get gists for
2191 :type userid: Optional(str or int)
2191 :type userid: Optional(str or int)
2192 """
2192 """
2193 if not HasPermissionAny('hg.admin')():
2193 if not HasPermissionAny('hg.admin')():
2194 # make sure normal user does not pass someone else userid,
2194 # make sure normal user does not pass someone else userid,
2195 # he is not allowed to do that
2195 # he is not allowed to do that
2196 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
2196 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
2197 raise JSONRPCError(
2197 raise JSONRPCError(
2198 'userid is not the same as your user'
2198 'userid is not the same as your user'
2199 )
2199 )
2200
2200
2201 if isinstance(userid, Optional):
2201 if isinstance(userid, Optional):
2202 user_id = request.authuser.user_id
2202 user_id = request.authuser.user_id
2203 else:
2203 else:
2204 user_id = get_user_or_error(userid).user_id
2204 user_id = get_user_or_error(userid).user_id
2205
2205
2206 return [
2206 return [
2207 gist.get_api_data()
2207 gist.get_api_data()
2208 for gist in Gist().query()
2208 for gist in Gist().query()
2209 .filter_by(is_expired=False)
2209 .filter_by(is_expired=False)
2210 .filter(Gist.owner_id == user_id)
2210 .filter(Gist.owner_id == user_id)
2211 .order_by(Gist.created_on.desc())
2211 .order_by(Gist.created_on.desc())
2212 ]
2212 ]
2213
2213
2214 def create_gist(self, files, owner=Optional(OAttr('apiuser')),
2214 def create_gist(self, files, owner=Optional(OAttr('apiuser')),
2215 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2215 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2216 description=Optional('')):
2216 description=Optional('')):
2217
2217
2218 """
2218 """
2219 Creates new Gist
2219 Creates new Gist
2220
2220
2221 :param files: files to be added to gist
2221 :param files: files to be added to gist
2222 {'filename': {'content':'...', 'lexer': null},
2222 {'filename': {'content':'...', 'lexer': null},
2223 'filename2': {'content':'...', 'lexer': null}}
2223 'filename2': {'content':'...', 'lexer': null}}
2224 :type files: dict
2224 :type files: dict
2225 :param owner: gist owner, defaults to api method caller
2225 :param owner: gist owner, defaults to api method caller
2226 :type owner: Optional(str or int)
2226 :type owner: Optional(str or int)
2227 :param gist_type: type of gist 'public' or 'private'
2227 :param gist_type: type of gist 'public' or 'private'
2228 :type gist_type: Optional(str)
2228 :type gist_type: Optional(str)
2229 :param lifetime: time in minutes of gist lifetime
2229 :param lifetime: time in minutes of gist lifetime
2230 :type lifetime: Optional(int)
2230 :type lifetime: Optional(int)
2231 :param description: gist description
2231 :param description: gist description
2232 :type description: Optional(str)
2232 :type description: Optional(str)
2233
2233
2234 OUTPUT::
2234 OUTPUT::
2235
2235
2236 id : <id_given_in_input>
2236 id : <id_given_in_input>
2237 result : {
2237 result : {
2238 "msg": "created new gist",
2238 "msg": "created new gist",
2239 "gist": {}
2239 "gist": {}
2240 }
2240 }
2241 error : null
2241 error : null
2242
2242
2243 ERROR OUTPUT::
2243 ERROR OUTPUT::
2244
2244
2245 id : <id_given_in_input>
2245 id : <id_given_in_input>
2246 result : null
2246 result : null
2247 error : {
2247 error : {
2248 "failed to create gist"
2248 "failed to create gist"
2249 }
2249 }
2250
2250
2251 """
2251 """
2252 try:
2252 try:
2253 if isinstance(owner, Optional):
2253 if isinstance(owner, Optional):
2254 owner = request.authuser.user_id
2254 owner = request.authuser.user_id
2255
2255
2256 owner = get_user_or_error(owner)
2256 owner = get_user_or_error(owner)
2257 description = Optional.extract(description)
2257 description = Optional.extract(description)
2258 gist_type = Optional.extract(gist_type)
2258 gist_type = Optional.extract(gist_type)
2259 lifetime = Optional.extract(lifetime)
2259 lifetime = Optional.extract(lifetime)
2260
2260
2261 gist = GistModel().create(description=description,
2261 gist = GistModel().create(description=description,
2262 owner=owner,
2262 owner=owner,
2263 ip_addr=request.ip_addr,
2263 ip_addr=request.ip_addr,
2264 gist_mapping=files,
2264 gist_mapping=files,
2265 gist_type=gist_type,
2265 gist_type=gist_type,
2266 lifetime=lifetime)
2266 lifetime=lifetime)
2267 Session().commit()
2267 Session().commit()
2268 return dict(
2268 return dict(
2269 msg='created new gist',
2269 msg='created new gist',
2270 gist=gist.get_api_data()
2270 gist=gist.get_api_data()
2271 )
2271 )
2272 except Exception:
2272 except Exception:
2273 log.error(traceback.format_exc())
2273 log.error(traceback.format_exc())
2274 raise JSONRPCError('failed to create gist')
2274 raise JSONRPCError('failed to create gist')
2275
2275
2276 # def update_gist(self, gistid, files, owner=Optional(OAttr('apiuser')),
2276 # def update_gist(self, gistid, files, owner=Optional(OAttr('apiuser')),
2277 # gist_type=Optional(Gist.GIST_PUBLIC),
2277 # gist_type=Optional(Gist.GIST_PUBLIC),
2278 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2278 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2279 # gist = get_gist_or_error(gistid)
2279 # gist = get_gist_or_error(gistid)
2280 # updates = {}
2280 # updates = {}
2281
2281
2282 # permission check inside
2282 # permission check inside
2283 def delete_gist(self, gistid):
2283 def delete_gist(self, gistid):
2284 """
2284 """
2285 Deletes existing gist
2285 Deletes existing gist
2286
2286
2287 :param gistid: id of gist to delete
2287 :param gistid: id of gist to delete
2288 :type gistid: str
2288 :type gistid: str
2289
2289
2290 OUTPUT::
2290 OUTPUT::
2291
2291
2292 id : <id_given_in_input>
2292 id : <id_given_in_input>
2293 result : {
2293 result : {
2294 "deleted gist ID: <gist_id>",
2294 "deleted gist ID: <gist_id>",
2295 "gist": null
2295 "gist": null
2296 }
2296 }
2297 error : null
2297 error : null
2298
2298
2299 ERROR OUTPUT::
2299 ERROR OUTPUT::
2300
2300
2301 id : <id_given_in_input>
2301 id : <id_given_in_input>
2302 result : null
2302 result : null
2303 error : {
2303 error : {
2304 "failed to delete gist ID:<gist_id>"
2304 "failed to delete gist ID:<gist_id>"
2305 }
2305 }
2306
2306
2307 """
2307 """
2308 gist = get_gist_or_error(gistid)
2308 gist = get_gist_or_error(gistid)
2309 if not HasPermissionAny('hg.admin')():
2309 if not HasPermissionAny('hg.admin')():
2310 if gist.owner_id != request.authuser.user_id:
2310 if gist.owner_id != request.authuser.user_id:
2311 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2311 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2312
2312
2313 try:
2313 try:
2314 GistModel().delete(gist)
2314 GistModel().delete(gist)
2315 Session().commit()
2315 Session().commit()
2316 return dict(
2316 return dict(
2317 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2317 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2318 gist=None
2318 gist=None
2319 )
2319 )
2320 except Exception:
2320 except Exception:
2321 log.error(traceback.format_exc())
2321 log.error(traceback.format_exc())
2322 raise JSONRPCError('failed to delete gist ID:%s'
2322 raise JSONRPCError('failed to delete gist ID:%s'
2323 % (gist.gist_access_id,))
2323 % (gist.gist_access_id,))
2324
2324
2325 # permission check inside
2325 # permission check inside
2326 def get_changesets(self, repoid, start=None, end=None, start_date=None,
2326 def get_changesets(self, repoid, start=None, end=None, start_date=None,
2327 end_date=None, branch_name=None, reverse=False, with_file_list=False, max_revisions=None):
2327 end_date=None, branch_name=None, reverse=False, with_file_list=False, max_revisions=None):
2328 repo = get_repo_or_error(repoid)
2328 repo = get_repo_or_error(repoid)
2329 if not HasRepoPermissionLevel('read')(repo.repo_name):
2329 if not HasRepoPermissionLevel('read')(repo.repo_name):
2330 raise JSONRPCError('Access denied to repo %s' % repo.repo_name)
2330 raise JSONRPCError('Access denied to repo %s' % repo.repo_name)
2331
2331
2332 format = "%Y-%m-%dT%H:%M:%S"
2332 format = "%Y-%m-%dT%H:%M:%S"
2333 try:
2333 try:
2334 return [e.__json__(with_file_list) for e in
2334 return [e.__json__(with_file_list) for e in
2335 repo.scm_instance.get_changesets(start,
2335 repo.scm_instance.get_changesets(start,
2336 end,
2336 end,
2337 datetime.strptime(start_date, format) if start_date else None,
2337 datetime.strptime(start_date, format) if start_date else None,
2338 datetime.strptime(end_date, format) if end_date else None,
2338 datetime.strptime(end_date, format) if end_date else None,
2339 branch_name,
2339 branch_name,
2340 reverse, max_revisions)]
2340 reverse, max_revisions)]
2341 except EmptyRepositoryError as e:
2341 except EmptyRepositoryError as e:
2342 raise JSONRPCError(e.message)
2342 raise JSONRPCError(e.message)
2343
2343
2344 # permission check inside
2344 # permission check inside
2345 def get_changeset(self, repoid, raw_id, with_reviews=Optional(False)):
2345 def get_changeset(self, repoid, raw_id, with_reviews=Optional(False)):
2346 repo = get_repo_or_error(repoid)
2346 repo = get_repo_or_error(repoid)
2347 if not HasRepoPermissionLevel('read')(repo.repo_name):
2347 if not HasRepoPermissionLevel('read')(repo.repo_name):
2348 raise JSONRPCError('Access denied to repo %s' % repo.repo_name)
2348 raise JSONRPCError('Access denied to repo %s' % repo.repo_name)
2349 changeset = repo.get_changeset(raw_id)
2349 changeset = repo.get_changeset(raw_id)
2350 if isinstance(changeset, EmptyChangeset):
2350 if isinstance(changeset, EmptyChangeset):
2351 raise JSONRPCError('Changeset %s does not exist' % raw_id)
2351 raise JSONRPCError('Changeset %s does not exist' % raw_id)
2352
2352
2353 info = dict(changeset.as_dict())
2353 info = dict(changeset.as_dict())
2354
2354
2355 with_reviews = Optional.extract(with_reviews)
2355 with_reviews = Optional.extract(with_reviews)
2356 if with_reviews:
2356 if with_reviews:
2357 reviews = ChangesetStatusModel().get_statuses(
2357 reviews = ChangesetStatusModel().get_statuses(
2358 repo.repo_name, raw_id)
2358 repo.repo_name, raw_id)
2359 info["reviews"] = reviews
2359 info["reviews"] = reviews
2360
2360
2361 return info
2361 return info
2362
2362
2363 # permission check inside
2363 # permission check inside
2364 def get_pullrequest(self, pullrequest_id):
2364 def get_pullrequest(self, pullrequest_id):
2365 """
2365 """
2366 Get given pull request by id
2366 Get given pull request by id
2367 """
2367 """
2368 pull_request = PullRequest.get(pullrequest_id)
2368 pull_request = PullRequest.get(pullrequest_id)
2369 if pull_request is None:
2369 if pull_request is None:
2370 raise JSONRPCError('pull request `%s` does not exist' % (pullrequest_id,))
2370 raise JSONRPCError('pull request `%s` does not exist' % (pullrequest_id,))
2371 if not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name):
2371 if not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name):
2372 raise JSONRPCError('not allowed')
2372 raise JSONRPCError('not allowed')
2373 return pull_request.get_api_data()
2373 return pull_request.get_api_data()
2374
2374
2375 # permission check inside
2375 # permission check inside
2376 def comment_pullrequest(self, pull_request_id, comment_msg=u'', status=None, close_pr=False):
2376 def comment_pullrequest(self, pull_request_id, comment_msg=u'', status=None, close_pr=False):
2377 """
2377 """
2378 Add comment, close and change status of pull request.
2378 Add comment, close and change status of pull request.
2379 """
2379 """
2380 apiuser = get_user_or_error(request.authuser.user_id)
2380 apiuser = get_user_or_error(request.authuser.user_id)
2381 pull_request = PullRequest.get(pull_request_id)
2381 pull_request = PullRequest.get(pull_request_id)
2382 if pull_request is None:
2382 if pull_request is None:
2383 raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,))
2383 raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,))
2384 if (not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name)):
2384 if (not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name)):
2385 raise JSONRPCError('No permission to add comment. User needs at least reading permissions'
2385 raise JSONRPCError('No permission to add comment. User needs at least reading permissions'
2386 ' to the source repository.')
2386 ' to the source repository.')
2387 owner = apiuser.user_id == pull_request.owner_id
2387 owner = apiuser.user_id == pull_request.owner_id
2388 reviewer = apiuser.user_id in [reviewer.user_id for reviewer in pull_request.reviewers]
2388 reviewer = apiuser.user_id in [reviewer.user_id for reviewer in pull_request.reviewers]
2389 if close_pr and not (apiuser.admin or owner):
2389 if close_pr and not (apiuser.admin or owner):
2390 raise JSONRPCError('No permission to close pull request. User needs to be admin or owner.')
2390 raise JSONRPCError('No permission to close pull request. User needs to be admin or owner.')
2391 if status and not (apiuser.admin or owner or reviewer):
2391 if status and not (apiuser.admin or owner or reviewer):
2392 raise JSONRPCError('No permission to change pull request status. User needs to be admin, owner or reviewer.')
2392 raise JSONRPCError('No permission to change pull request status. User needs to be admin, owner or reviewer.')
2393 if pull_request.is_closed():
2393 if pull_request.is_closed():
2394 raise JSONRPCError('pull request is already closed')
2394 raise JSONRPCError('pull request is already closed')
2395
2395
2396 comment = ChangesetCommentsModel().create(
2396 comment = ChangesetCommentsModel().create(
2397 text=comment_msg,
2397 text=comment_msg,
2398 repo=pull_request.org_repo.repo_id,
2398 repo=pull_request.org_repo.repo_id,
2399 author=apiuser.user_id,
2399 author=apiuser.user_id,
2400 pull_request=pull_request.pull_request_id,
2400 pull_request=pull_request.pull_request_id,
2401 f_path=None,
2401 f_path=None,
2402 line_no=None,
2402 line_no=None,
2403 status_change=(ChangesetStatus.get_status_lbl(status)),
2403 status_change=(ChangesetStatus.get_status_lbl(status)),
2404 closing_pr=close_pr
2404 closing_pr=close_pr
2405 )
2405 )
2406 action_logger(apiuser,
2406 action_logger(apiuser,
2407 'user_commented_pull_request:%s' % pull_request_id,
2407 'user_commented_pull_request:%s' % pull_request_id,
2408 pull_request.org_repo, request.ip_addr)
2408 pull_request.org_repo, request.ip_addr)
2409 if status:
2409 if status:
2410 ChangesetStatusModel().set_status(
2410 ChangesetStatusModel().set_status(
2411 pull_request.org_repo_id,
2411 pull_request.org_repo_id,
2412 status,
2412 status,
2413 apiuser.user_id,
2413 apiuser.user_id,
2414 comment,
2414 comment,
2415 pull_request=pull_request_id
2415 pull_request=pull_request_id
2416 )
2416 )
2417 if close_pr:
2417 if close_pr:
2418 PullRequestModel().close_pull_request(pull_request_id)
2418 PullRequestModel().close_pull_request(pull_request_id)
2419 action_logger(apiuser,
2419 action_logger(apiuser,
2420 'user_closed_pull_request:%s' % pull_request_id,
2420 'user_closed_pull_request:%s' % pull_request_id,
2421 pull_request.org_repo, request.ip_addr)
2421 pull_request.org_repo, request.ip_addr)
2422 Session().commit()
2422 Session().commit()
2423 return True
2423 return True
@@ -1,488 +1,488 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.changeset
15 kallithea.controllers.changeset
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changeset controller showing changes between revisions
18 changeset controller showing changes between revisions
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 25, 2010
22 :created_on: Apr 25, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 from collections import OrderedDict, defaultdict
30 from collections import OrderedDict, defaultdict
31
31
32 from tg import request, response
32 from tg import request, response
33 from tg import tmpl_context as c
33 from tg import tmpl_context as c
34 from tg.i18n import ugettext as _
34 from tg.i18n import ugettext as _
35 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
35 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
36
36
37 import kallithea.lib.helpers as h
37 import kallithea.lib.helpers as h
38 from kallithea.lib import diffs
38 from kallithea.lib import diffs
39 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
39 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
40 from kallithea.lib.base import BaseRepoController, jsonify, render
40 from kallithea.lib.base import BaseRepoController, jsonify, render
41 from kallithea.lib.graphmod import graph_data
41 from kallithea.lib.graphmod import graph_data
42 from kallithea.lib.utils import action_logger
42 from kallithea.lib.utils import action_logger
43 from kallithea.lib.utils2 import safe_unicode
43 from kallithea.lib.utils2 import safe_unicode
44 from kallithea.lib.vcs.backends.base import EmptyChangeset
44 from kallithea.lib.vcs.backends.base import EmptyChangeset
45 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
45 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
46 from kallithea.model.changeset_status import ChangesetStatusModel
46 from kallithea.model.changeset_status import ChangesetStatusModel
47 from kallithea.model.comment import ChangesetCommentsModel
47 from kallithea.model.comment import ChangesetCommentsModel
48 from kallithea.model.db import ChangesetComment, ChangesetStatus
48 from kallithea.model.db import ChangesetComment, ChangesetStatus
49 from kallithea.model.meta import Session
49 from kallithea.model.meta import Session
50 from kallithea.model.pull_request import PullRequestModel
50 from kallithea.model.pull_request import PullRequestModel
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 def _update_with_GET(params, GET):
56 def _update_with_GET(params, GET):
57 for k in ['diff1', 'diff2', 'diff']:
57 for k in ['diff1', 'diff2', 'diff']:
58 params[k] += GET.getall(k)
58 params[k] += GET.getall(k)
59
59
60
60
61 def anchor_url(revision, path, GET):
61 def anchor_url(revision, path, GET):
62 fid = h.FID(revision, path)
62 fid = h.FID(revision, path)
63 return h.url.current(anchor=fid, **dict(GET))
63 return h.url.current(anchor=fid, **dict(GET))
64
64
65
65
66 def get_ignore_ws(fid, GET):
66 def get_ignore_ws(fid, GET):
67 ig_ws_global = GET.get('ignorews')
67 ig_ws_global = GET.get('ignorews')
68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
69 if ig_ws:
69 if ig_ws:
70 try:
70 try:
71 return int(ig_ws[0].split(':')[-1])
71 return int(ig_ws[0].split(':')[-1])
72 except ValueError:
72 except ValueError:
73 raise HTTPBadRequest()
73 raise HTTPBadRequest()
74 return ig_ws_global
74 return ig_ws_global
75
75
76
76
77 def _ignorews_url(GET, fileid=None):
77 def _ignorews_url(GET, fileid=None):
78 fileid = str(fileid) if fileid else None
78 fileid = str(fileid) if fileid else None
79 params = defaultdict(list)
79 params = defaultdict(list)
80 _update_with_GET(params, GET)
80 _update_with_GET(params, GET)
81 lbl = _('Show whitespace')
81 lbl = _('Show whitespace')
82 ig_ws = get_ignore_ws(fileid, GET)
82 ig_ws = get_ignore_ws(fileid, GET)
83 ln_ctx = get_line_ctx(fileid, GET)
83 ln_ctx = get_line_ctx(fileid, GET)
84 # global option
84 # global option
85 if fileid is None:
85 if fileid is None:
86 if ig_ws is None:
86 if ig_ws is None:
87 params['ignorews'] += [1]
87 params['ignorews'] += [1]
88 lbl = _('Ignore whitespace')
88 lbl = _('Ignore whitespace')
89 ctx_key = 'context'
89 ctx_key = 'context'
90 ctx_val = ln_ctx
90 ctx_val = ln_ctx
91 # per file options
91 # per file options
92 else:
92 else:
93 if ig_ws is None:
93 if ig_ws is None:
94 params[fileid] += ['WS:1']
94 params[fileid] += ['WS:1']
95 lbl = _('Ignore whitespace')
95 lbl = _('Ignore whitespace')
96
96
97 ctx_key = fileid
97 ctx_key = fileid
98 ctx_val = 'C:%s' % ln_ctx
98 ctx_val = 'C:%s' % ln_ctx
99 # if we have passed in ln_ctx pass it along to our params
99 # if we have passed in ln_ctx pass it along to our params
100 if ln_ctx:
100 if ln_ctx:
101 params[ctx_key] += [ctx_val]
101 params[ctx_key] += [ctx_val]
102
102
103 params['anchor'] = fileid
103 params['anchor'] = fileid
104 icon = h.literal('<i class="icon-strike"></i>')
104 icon = h.literal('<i class="icon-strike"></i>')
105 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
105 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
106
106
107
107
108 def get_line_ctx(fid, GET):
108 def get_line_ctx(fid, GET):
109 ln_ctx_global = GET.get('context')
109 ln_ctx_global = GET.get('context')
110 if fid:
110 if fid:
111 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
111 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
112 else:
112 else:
113 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
113 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
114 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
114 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
115 if ln_ctx:
115 if ln_ctx:
116 ln_ctx = [ln_ctx]
116 ln_ctx = [ln_ctx]
117
117
118 if ln_ctx:
118 if ln_ctx:
119 retval = ln_ctx[0].split(':')[-1]
119 retval = ln_ctx[0].split(':')[-1]
120 else:
120 else:
121 retval = ln_ctx_global
121 retval = ln_ctx_global
122
122
123 try:
123 try:
124 return int(retval)
124 return int(retval)
125 except Exception:
125 except Exception:
126 return 3
126 return 3
127
127
128
128
129 def _context_url(GET, fileid=None):
129 def _context_url(GET, fileid=None):
130 """
130 """
131 Generates url for context lines
131 Generates url for context lines
132
132
133 :param fileid:
133 :param fileid:
134 """
134 """
135
135
136 fileid = str(fileid) if fileid else None
136 fileid = str(fileid) if fileid else None
137 ig_ws = get_ignore_ws(fileid, GET)
137 ig_ws = get_ignore_ws(fileid, GET)
138 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
138 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
139
139
140 params = defaultdict(list)
140 params = defaultdict(list)
141 _update_with_GET(params, GET)
141 _update_with_GET(params, GET)
142
142
143 # global option
143 # global option
144 if fileid is None:
144 if fileid is None:
145 if ln_ctx > 0:
145 if ln_ctx > 0:
146 params['context'] += [ln_ctx]
146 params['context'] += [ln_ctx]
147
147
148 if ig_ws:
148 if ig_ws:
149 ig_ws_key = 'ignorews'
149 ig_ws_key = 'ignorews'
150 ig_ws_val = 1
150 ig_ws_val = 1
151
151
152 # per file option
152 # per file option
153 else:
153 else:
154 params[fileid] += ['C:%s' % ln_ctx]
154 params[fileid] += ['C:%s' % ln_ctx]
155 ig_ws_key = fileid
155 ig_ws_key = fileid
156 ig_ws_val = 'WS:%s' % 1
156 ig_ws_val = 'WS:%s' % 1
157
157
158 if ig_ws:
158 if ig_ws:
159 params[ig_ws_key] += [ig_ws_val]
159 params[ig_ws_key] += [ig_ws_val]
160
160
161 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
161 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
162
162
163 params['anchor'] = fileid
163 params['anchor'] = fileid
164 icon = h.literal('<i class="icon-sort"></i>')
164 icon = h.literal('<i class="icon-sort"></i>')
165 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
165 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
166
166
167
167
168 def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True):
168 def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True):
169 """
169 """
170 Add a comment to the specified changeset or pull request, using POST values
170 Add a comment to the specified changeset or pull request, using POST values
171 from the request.
171 from the request.
172
172
173 Comments can be inline (when a file path and line number is specified in
173 Comments can be inline (when a file path and line number is specified in
174 POST) or general comments.
174 POST) or general comments.
175 A comment can be accompanied by a review status change (accepted, rejected,
175 A comment can be accompanied by a review status change (accepted, rejected,
176 etc.). Pull requests can be closed or deleted.
176 etc.). Pull requests can be closed or deleted.
177
177
178 Parameter 'allowed_to_change_status' is used for both status changes and
178 Parameter 'allowed_to_change_status' is used for both status changes and
179 closing of pull requests. For deleting of pull requests, more specific
179 closing of pull requests. For deleting of pull requests, more specific
180 checks are done.
180 checks are done.
181 """
181 """
182
182
183 assert request.environ.get('HTTP_X_PARTIAL_XHR')
183 assert request.environ.get('HTTP_X_PARTIAL_XHR')
184 if pull_request:
184 if pull_request:
185 pull_request_id = pull_request.pull_request_id
185 pull_request_id = pull_request.pull_request_id
186 else:
186 else:
187 pull_request_id = None
187 pull_request_id = None
188
188
189 status = request.POST.get('changeset_status')
189 status = request.POST.get('changeset_status')
190 close_pr = request.POST.get('save_close')
190 close_pr = request.POST.get('save_close')
191 delete = request.POST.get('save_delete')
191 delete = request.POST.get('save_delete')
192 f_path = request.POST.get('f_path')
192 f_path = request.POST.get('f_path')
193 line_no = request.POST.get('line')
193 line_no = request.POST.get('line')
194
194
195 if (status or close_pr or delete) and (f_path or line_no):
195 if (status or close_pr or delete) and (f_path or line_no):
196 # status votes and closing is only possible in general comments
196 # status votes and closing is only possible in general comments
197 raise HTTPBadRequest()
197 raise HTTPBadRequest()
198
198
199 if not allowed_to_change_status:
199 if not allowed_to_change_status:
200 if status or close_pr:
200 if status or close_pr:
201 h.flash(_('No permission to change status'), 'error')
201 h.flash(_('No permission to change status'), 'error')
202 raise HTTPForbidden()
202 raise HTTPForbidden()
203
203
204 if pull_request and delete == "delete":
204 if pull_request and delete == "delete":
205 if (pull_request.owner_id == request.authuser.user_id or
205 if (pull_request.owner_id == request.authuser.user_id or
206 h.HasPermissionAny('hg.admin')() or
206 h.HasPermissionAny('hg.admin')() or
207 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
207 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
208 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
208 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
209 ) and not pull_request.is_closed():
209 ) and not pull_request.is_closed():
210 PullRequestModel().delete(pull_request)
210 PullRequestModel().delete(pull_request)
211 Session().commit()
211 Session().commit()
212 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
212 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
213 category='success')
213 category='success')
214 return {
214 return {
215 'location': h.url('my_pullrequests'), # or repo pr list?
215 'location': h.url('my_pullrequests'), # or repo pr list?
216 }
216 }
217 raise HTTPFound(location=h.url('my_pullrequests')) # or repo pr list?
217 raise HTTPFound(location=h.url('my_pullrequests')) # or repo pr list?
218 raise HTTPForbidden()
218 raise HTTPForbidden()
219
219
220 text = request.POST.get('text', '').strip()
220 text = request.POST.get('text', '').strip()
221
221
222 comment = ChangesetCommentsModel().create(
222 comment = ChangesetCommentsModel().create(
223 text=text,
223 text=text,
224 repo=c.db_repo.repo_id,
224 repo=c.db_repo.repo_id,
225 author=request.authuser.user_id,
225 author=request.authuser.user_id,
226 revision=revision,
226 revision=revision,
227 pull_request=pull_request_id,
227 pull_request=pull_request_id,
228 f_path=f_path or None,
228 f_path=f_path or None,
229 line_no=line_no or None,
229 line_no=line_no or None,
230 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
230 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
231 closing_pr=close_pr,
231 closing_pr=close_pr,
232 )
232 )
233
233
234 if status:
234 if status:
235 ChangesetStatusModel().set_status(
235 ChangesetStatusModel().set_status(
236 c.db_repo.repo_id,
236 c.db_repo.repo_id,
237 status,
237 status,
238 request.authuser.user_id,
238 request.authuser.user_id,
239 comment,
239 comment,
240 revision=revision,
240 revision=revision,
241 pull_request=pull_request_id,
241 pull_request=pull_request_id,
242 )
242 )
243
243
244 if pull_request:
244 if pull_request:
245 action = 'user_commented_pull_request:%s' % pull_request_id
245 action = 'user_commented_pull_request:%s' % pull_request_id
246 else:
246 else:
247 action = 'user_commented_revision:%s' % revision
247 action = 'user_commented_revision:%s' % revision
248 action_logger(request.authuser, action, c.db_repo, request.ip_addr)
248 action_logger(request.authuser, action, c.db_repo, request.ip_addr)
249
249
250 if pull_request and close_pr:
250 if pull_request and close_pr:
251 PullRequestModel().close_pull_request(pull_request_id)
251 PullRequestModel().close_pull_request(pull_request_id)
252 action_logger(request.authuser,
252 action_logger(request.authuser,
253 'user_closed_pull_request:%s' % pull_request_id,
253 'user_closed_pull_request:%s' % pull_request_id,
254 c.db_repo, request.ip_addr)
254 c.db_repo, request.ip_addr)
255
255
256 Session().commit()
256 Session().commit()
257
257
258 data = {
258 data = {
259 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
259 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
260 }
260 }
261 if comment is not None:
261 if comment is not None:
262 c.comment = comment
262 c.comment = comment
263 data.update(comment.get_dict())
263 data.update(comment.get_dict())
264 data.update({'rendered_text':
264 data.update({'rendered_text':
265 render('changeset/changeset_comment_block.html')})
265 render('changeset/changeset_comment_block.html')})
266
266
267 return data
267 return data
268
268
269 def delete_cs_pr_comment(repo_name, comment_id):
269 def delete_cs_pr_comment(repo_name, comment_id):
270 """Delete a comment from a changeset or pull request"""
270 """Delete a comment from a changeset or pull request"""
271 co = ChangesetComment.get_or_404(comment_id)
271 co = ChangesetComment.get_or_404(comment_id)
272 if co.repo.repo_name != repo_name:
272 if co.repo.repo_name != repo_name:
273 raise HTTPNotFound()
273 raise HTTPNotFound()
274 if co.pull_request and co.pull_request.is_closed():
274 if co.pull_request and co.pull_request.is_closed():
275 # don't allow deleting comments on closed pull request
275 # don't allow deleting comments on closed pull request
276 raise HTTPForbidden()
276 raise HTTPForbidden()
277
277
278 owner = co.author_id == request.authuser.user_id
278 owner = co.author_id == request.authuser.user_id
279 repo_admin = h.HasRepoPermissionLevel('admin')(repo_name)
279 repo_admin = h.HasRepoPermissionLevel('admin')(repo_name)
280 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
280 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
281 ChangesetCommentsModel().delete(comment=co)
281 ChangesetCommentsModel().delete(comment=co)
282 Session().commit()
282 Session().commit()
283 return True
283 return True
284 else:
284 else:
285 raise HTTPForbidden()
285 raise HTTPForbidden()
286
286
287 class ChangesetController(BaseRepoController):
287 class ChangesetController(BaseRepoController):
288
288
289 def _before(self, *args, **kwargs):
289 def _before(self, *args, **kwargs):
290 super(ChangesetController, self)._before(*args, **kwargs)
290 super(ChangesetController, self)._before(*args, **kwargs)
291 c.affected_files_cut_off = 60
291 c.affected_files_cut_off = 60
292
292
293 def _index(self, revision, method):
293 def _index(self, revision, method):
294 c.pull_request = None
294 c.pull_request = None
295 c.anchor_url = anchor_url
295 c.anchor_url = anchor_url
296 c.ignorews_url = _ignorews_url
296 c.ignorews_url = _ignorews_url
297 c.context_url = _context_url
297 c.context_url = _context_url
298 c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files
298 c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files
299 # get ranges of revisions if preset
299 # get ranges of revisions if preset
300 rev_range = revision.split('...')[:2]
300 rev_range = revision.split('...')[:2]
301 enable_comments = True
301 enable_comments = True
302 c.cs_repo = c.db_repo
302 c.cs_repo = c.db_repo
303 try:
303 try:
304 if len(rev_range) == 2:
304 if len(rev_range) == 2:
305 enable_comments = False
305 enable_comments = False
306 rev_start = rev_range[0]
306 rev_start = rev_range[0]
307 rev_end = rev_range[1]
307 rev_end = rev_range[1]
308 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
308 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
309 end=rev_end)
309 end=rev_end)
310 else:
310 else:
311 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
311 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
312
312
313 c.cs_ranges = list(rev_ranges)
313 c.cs_ranges = list(rev_ranges)
314 if not c.cs_ranges:
314 if not c.cs_ranges:
315 raise RepositoryError('Changeset range returned empty result')
315 raise RepositoryError('Changeset range returned empty result')
316
316
317 except (ChangesetDoesNotExistError, EmptyRepositoryError):
317 except (ChangesetDoesNotExistError, EmptyRepositoryError):
318 log.debug(traceback.format_exc())
318 log.debug(traceback.format_exc())
319 msg = _('Such revision does not exist for this repository')
319 msg = _('Such revision does not exist for this repository')
320 h.flash(msg, category='error')
320 h.flash(msg, category='error')
321 raise HTTPNotFound()
321 raise HTTPNotFound()
322
322
323 c.changes = OrderedDict()
323 c.changes = OrderedDict()
324
324
325 c.lines_added = 0 # count of lines added
325 c.lines_added = 0 # count of lines added
326 c.lines_deleted = 0 # count of lines removes
326 c.lines_deleted = 0 # count of lines removes
327
327
328 c.changeset_statuses = ChangesetStatus.STATUSES
328 c.changeset_statuses = ChangesetStatus.STATUSES
329 comments = dict()
329 comments = dict()
330 c.statuses = []
330 c.statuses = []
331 c.inline_comments = []
331 c.inline_comments = []
332 c.inline_cnt = 0
332 c.inline_cnt = 0
333
333
334 # Iterate over ranges (default changeset view is always one changeset)
334 # Iterate over ranges (default changeset view is always one changeset)
335 for changeset in c.cs_ranges:
335 for changeset in c.cs_ranges:
336 if method == 'show':
336 if method == 'show':
337 c.statuses.extend([ChangesetStatusModel().get_status(
337 c.statuses.extend([ChangesetStatusModel().get_status(
338 c.db_repo.repo_id, changeset.raw_id)])
338 c.db_repo.repo_id, changeset.raw_id)])
339
339
340 # Changeset comments
340 # Changeset comments
341 comments.update((com.comment_id, com)
341 comments.update((com.comment_id, com)
342 for com in ChangesetCommentsModel()
342 for com in ChangesetCommentsModel()
343 .get_comments(c.db_repo.repo_id,
343 .get_comments(c.db_repo.repo_id,
344 revision=changeset.raw_id))
344 revision=changeset.raw_id))
345
345
346 # Status change comments - mostly from pull requests
346 # Status change comments - mostly from pull requests
347 comments.update((st.comment_id, st.comment)
347 comments.update((st.comment_id, st.comment)
348 for st in ChangesetStatusModel()
348 for st in ChangesetStatusModel()
349 .get_statuses(c.db_repo.repo_id,
349 .get_statuses(c.db_repo.repo_id,
350 changeset.raw_id, with_revisions=True)
350 changeset.raw_id, with_revisions=True)
351 if st.comment_id is not None)
351 if st.comment_id is not None)
352
352
353 inlines = ChangesetCommentsModel() \
353 inlines = ChangesetCommentsModel() \
354 .get_inline_comments(c.db_repo.repo_id,
354 .get_inline_comments(c.db_repo.repo_id,
355 revision=changeset.raw_id)
355 revision=changeset.raw_id)
356 c.inline_comments.extend(inlines)
356 c.inline_comments.extend(inlines)
357
357
358 cs2 = changeset.raw_id
358 cs2 = changeset.raw_id
359 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
359 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
360 context_lcl = get_line_ctx('', request.GET)
360 context_lcl = get_line_ctx('', request.GET)
361 ign_whitespace_lcl = get_ignore_ws('', request.GET)
361 ign_whitespace_lcl = get_ignore_ws('', request.GET)
362
362
363 raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2,
363 raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2,
364 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
364 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
365 diff_limit = None if c.fulldiff else self.cut_off_limit
365 diff_limit = None if c.fulldiff else self.cut_off_limit
366 file_diff_data = []
366 file_diff_data = []
367 if method == 'show':
367 if method == 'show':
368 diff_processor = diffs.DiffProcessor(raw_diff,
368 diff_processor = diffs.DiffProcessor(raw_diff,
369 vcs=c.db_repo_scm_instance.alias,
369 vcs=c.db_repo_scm_instance.alias,
370 diff_limit=diff_limit)
370 diff_limit=diff_limit)
371 c.limited_diff = diff_processor.limited_diff
371 c.limited_diff = diff_processor.limited_diff
372 for f in diff_processor.parsed:
372 for f in diff_processor.parsed:
373 st = f['stats']
373 st = f['stats']
374 c.lines_added += st['added']
374 c.lines_added += st['added']
375 c.lines_deleted += st['deleted']
375 c.lines_deleted += st['deleted']
376 filename = f['filename']
376 filename = f['filename']
377 fid = h.FID(changeset.raw_id, filename)
377 fid = h.FID(changeset.raw_id, filename)
378 url_fid = h.FID('', filename)
378 url_fid = h.FID('', filename)
379 html_diff = diffs.as_html(enable_comments=enable_comments, parsed_lines=[f])
379 html_diff = diffs.as_html(enable_comments=enable_comments, parsed_lines=[f])
380 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st))
380 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, html_diff, st))
381 else:
381 else:
382 # downloads/raw we only need RAW diff nothing else
382 # downloads/raw we only need RAW diff nothing else
383 file_diff_data.append(('', None, None, None, raw_diff, None))
383 file_diff_data.append(('', None, None, None, raw_diff, None))
384 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
384 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
385
385
386 # sort comments in creation order
386 # sort comments in creation order
387 c.comments = [com for com_id, com in sorted(comments.items())]
387 c.comments = [com for com_id, com in sorted(comments.items())]
388
388
389 # count inline comments
389 # count inline comments
390 for __, lines in c.inline_comments:
390 for __, lines in c.inline_comments:
391 for comments in lines.values():
391 for comments in lines.values():
392 c.inline_cnt += len(comments)
392 c.inline_cnt += len(comments)
393
393
394 if len(c.cs_ranges) == 1:
394 if len(c.cs_ranges) == 1:
395 c.changeset = c.cs_ranges[0]
395 c.changeset = c.cs_ranges[0]
396 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
396 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
397 for x in c.changeset.parents])
397 for x in c.changeset.parents])
398 if method == 'download':
398 if method == 'download':
399 response.content_type = 'text/plain'
399 response.content_type = 'text/plain'
400 response.content_disposition = 'attachment; filename=%s.diff' \
400 response.content_disposition = 'attachment; filename=%s.diff' \
401 % revision[:12]
401 % revision[:12]
402 return raw_diff
402 return raw_diff
403 elif method == 'patch':
403 elif method == 'patch':
404 response.content_type = 'text/plain'
404 response.content_type = 'text/plain'
405 c.diff = safe_unicode(raw_diff)
405 c.diff = safe_unicode(raw_diff)
406 return render('changeset/patch_changeset.html')
406 return render('changeset/patch_changeset.html')
407 elif method == 'raw':
407 elif method == 'raw':
408 response.content_type = 'text/plain'
408 response.content_type = 'text/plain'
409 return raw_diff
409 return raw_diff
410 elif method == 'show':
410 elif method == 'show':
411 if len(c.cs_ranges) == 1:
411 if len(c.cs_ranges) == 1:
412 return render('changeset/changeset.html')
412 return render('changeset/changeset.html')
413 else:
413 else:
414 c.cs_ranges_org = None
414 c.cs_ranges_org = None
415 c.cs_comments = {}
415 c.cs_comments = {}
416 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
416 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
417 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
417 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
418 return render('changeset/changeset_range.html')
418 return render('changeset/changeset_range.html')
419
419
420 @LoginRequired(allow_default_user=True)
420 @LoginRequired(allow_default_user=True)
421 @HasRepoPermissionLevelDecorator('read')
421 @HasRepoPermissionLevelDecorator('read')
422 def index(self, revision, method='show'):
422 def index(self, revision, method='show'):
423 return self._index(revision, method=method)
423 return self._index(revision, method=method)
424
424
425 @LoginRequired(allow_default_user=True)
425 @LoginRequired(allow_default_user=True)
426 @HasRepoPermissionLevelDecorator('read')
426 @HasRepoPermissionLevelDecorator('read')
427 def changeset_raw(self, revision):
427 def changeset_raw(self, revision):
428 return self._index(revision, method='raw')
428 return self._index(revision, method='raw')
429
429
430 @LoginRequired(allow_default_user=True)
430 @LoginRequired(allow_default_user=True)
431 @HasRepoPermissionLevelDecorator('read')
431 @HasRepoPermissionLevelDecorator('read')
432 def changeset_patch(self, revision):
432 def changeset_patch(self, revision):
433 return self._index(revision, method='patch')
433 return self._index(revision, method='patch')
434
434
435 @LoginRequired(allow_default_user=True)
435 @LoginRequired(allow_default_user=True)
436 @HasRepoPermissionLevelDecorator('read')
436 @HasRepoPermissionLevelDecorator('read')
437 def changeset_download(self, revision):
437 def changeset_download(self, revision):
438 return self._index(revision, method='download')
438 return self._index(revision, method='download')
439
439
440 @LoginRequired()
440 @LoginRequired()
441 @HasRepoPermissionLevelDecorator('read')
441 @HasRepoPermissionLevelDecorator('read')
442 @jsonify
442 @jsonify
443 def comment(self, repo_name, revision):
443 def comment(self, repo_name, revision):
444 return create_cs_pr_comment(repo_name, revision=revision)
444 return create_cs_pr_comment(repo_name, revision=revision)
445
445
446 @LoginRequired()
446 @LoginRequired()
447 @HasRepoPermissionLevelDecorator('read')
447 @HasRepoPermissionLevelDecorator('read')
448 @jsonify
448 @jsonify
449 def delete_comment(self, repo_name, comment_id):
449 def delete_comment(self, repo_name, comment_id):
450 return delete_cs_pr_comment(repo_name, comment_id)
450 return delete_cs_pr_comment(repo_name, comment_id)
451
451
452 @LoginRequired(allow_default_user=True)
452 @LoginRequired(allow_default_user=True)
453 @HasRepoPermissionLevelDecorator('read')
453 @HasRepoPermissionLevelDecorator('read')
454 @jsonify
454 @jsonify
455 def changeset_info(self, repo_name, revision):
455 def changeset_info(self, repo_name, revision):
456 if request.is_xhr:
456 if request.is_xhr:
457 try:
457 try:
458 return c.db_repo_scm_instance.get_changeset(revision)
458 return c.db_repo_scm_instance.get_changeset(revision)
459 except ChangesetDoesNotExistError as e:
459 except ChangesetDoesNotExistError as e:
460 return EmptyChangeset(message=str(e))
460 return EmptyChangeset(message=str(e))
461 else:
461 else:
462 raise HTTPBadRequest()
462 raise HTTPBadRequest()
463
463
464 @LoginRequired(allow_default_user=True)
464 @LoginRequired(allow_default_user=True)
465 @HasRepoPermissionLevelDecorator('read')
465 @HasRepoPermissionLevelDecorator('read')
466 @jsonify
466 @jsonify
467 def changeset_children(self, repo_name, revision):
467 def changeset_children(self, repo_name, revision):
468 if request.is_xhr:
468 if request.is_xhr:
469 changeset = c.db_repo_scm_instance.get_changeset(revision)
469 changeset = c.db_repo_scm_instance.get_changeset(revision)
470 result = {"results": []}
470 result = {"results": []}
471 if changeset.children:
471 if changeset.children:
472 result = {"results": changeset.children}
472 result = {"results": changeset.children}
473 return result
473 return result
474 else:
474 else:
475 raise HTTPBadRequest()
475 raise HTTPBadRequest()
476
476
477 @LoginRequired(allow_default_user=True)
477 @LoginRequired(allow_default_user=True)
478 @HasRepoPermissionLevelDecorator('read')
478 @HasRepoPermissionLevelDecorator('read')
479 @jsonify
479 @jsonify
480 def changeset_parents(self, repo_name, revision):
480 def changeset_parents(self, repo_name, revision):
481 if request.is_xhr:
481 if request.is_xhr:
482 changeset = c.db_repo_scm_instance.get_changeset(revision)
482 changeset = c.db_repo_scm_instance.get_changeset(revision)
483 result = {"results": []}
483 result = {"results": []}
484 if changeset.parents:
484 if changeset.parents:
485 result = {"results": changeset.parents}
485 result = {"results": changeset.parents}
486 return result
486 return result
487 else:
487 else:
488 raise HTTPBadRequest()
488 raise HTTPBadRequest()
@@ -1,207 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.annotate
15 kallithea.lib.annotate
16 ~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Annotation library for usage in Kallithea, previously part of vcs
18 Annotation library for usage in Kallithea, previously part of vcs
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Dec 4, 2011
22 :created_on: Dec 4, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import StringIO
28 import StringIO
29
29
30 from pygments import highlight
30 from pygments import highlight
31 from pygments.formatters import HtmlFormatter
31 from pygments.formatters import HtmlFormatter
32
32
33 from kallithea.lib.vcs.exceptions import VCSError
33 from kallithea.lib.vcs.exceptions import VCSError
34 from kallithea.lib.vcs.nodes import FileNode
34 from kallithea.lib.vcs.nodes import FileNode
35
35
36
36
37 def annotate_highlight(filenode, annotate_from_changeset_func=None,
37 def annotate_highlight(filenode, annotate_from_changeset_func=None,
38 order=None, headers=None, **options):
38 order=None, headers=None, **options):
39 """
39 """
40 Returns html portion containing annotated table with 3 columns: line
40 Returns html portion containing annotated table with 3 columns: line
41 numbers, changeset information and pygmentized line of code.
41 numbers, changeset information and pygmentized line of code.
42
42
43 :param filenode: FileNode object
43 :param filenode: FileNode object
44 :param annotate_from_changeset_func: function taking changeset and
44 :param annotate_from_changeset_func: function taking changeset and
45 returning single annotate cell; needs break line at the end
45 returning single annotate cell; needs break line at the end
46 :param order: ordered sequence of ``ls`` (line numbers column),
46 :param order: ordered sequence of ``ls`` (line numbers column),
47 ``annotate`` (annotate column), ``code`` (code column); Default is
47 ``annotate`` (annotate column), ``code`` (code column); Default is
48 ``['ls', 'annotate', 'code']``
48 ``['ls', 'annotate', 'code']``
49 :param headers: dictionary with headers (keys are whats in ``order``
49 :param headers: dictionary with headers (keys are whats in ``order``
50 parameter)
50 parameter)
51 """
51 """
52 from kallithea.lib.pygmentsutils import get_custom_lexer
52 from kallithea.lib.pygmentsutils import get_custom_lexer
53 options['linenos'] = True
53 options['linenos'] = True
54 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
54 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
55 headers=headers,
55 headers=headers,
56 annotate_from_changeset_func=annotate_from_changeset_func, **options)
56 annotate_from_changeset_func=annotate_from_changeset_func, **options)
57 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
57 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
58 highlighted = highlight(filenode.content, lexer, formatter)
58 highlighted = highlight(filenode.content, lexer, formatter)
59 return highlighted
59 return highlighted
60
60
61
61
62 class AnnotateHtmlFormatter(HtmlFormatter):
62 class AnnotateHtmlFormatter(HtmlFormatter):
63
63
64 def __init__(self, filenode, annotate_from_changeset_func=None,
64 def __init__(self, filenode, annotate_from_changeset_func=None,
65 order=None, **options):
65 order=None, **options):
66 """
66 """
67 If ``annotate_from_changeset_func`` is passed it should be a function
67 If ``annotate_from_changeset_func`` is passed it should be a function
68 which returns string from the given changeset. For example, we may pass
68 which returns string from the given changeset. For example, we may pass
69 following function as ``annotate_from_changeset_func``::
69 following function as ``annotate_from_changeset_func``::
70
70
71 def changeset_to_anchor(changeset):
71 def changeset_to_anchor(changeset):
72 return '<a href="/changesets/%s/">%s</a>\n' % \
72 return '<a href="/changesets/%s/">%s</a>\n' % \
73 (changeset.id, changeset.id)
73 (changeset.id, changeset.id)
74
74
75 :param annotate_from_changeset_func: see above
75 :param annotate_from_changeset_func: see above
76 :param order: (default: ``['ls', 'annotate', 'code']``); order of
76 :param order: (default: ``['ls', 'annotate', 'code']``); order of
77 columns;
77 columns;
78 :param options: standard pygment's HtmlFormatter options, there is
78 :param options: standard pygment's HtmlFormatter options, there is
79 extra option tough, ``headers``. For instance we can pass::
79 extra option tough, ``headers``. For instance we can pass::
80
80
81 formatter = AnnotateHtmlFormatter(filenode, headers={
81 formatter = AnnotateHtmlFormatter(filenode, headers={
82 'ls': '#',
82 'ls': '#',
83 'annotate': 'Annotate',
83 'annotate': 'Annotate',
84 'code': 'Code',
84 'code': 'Code',
85 })
85 })
86
86
87 """
87 """
88 super(AnnotateHtmlFormatter, self).__init__(**options)
88 super(AnnotateHtmlFormatter, self).__init__(**options)
89 self.annotate_from_changeset_func = annotate_from_changeset_func
89 self.annotate_from_changeset_func = annotate_from_changeset_func
90 self.order = order or ('ls', 'annotate', 'code')
90 self.order = order or ('ls', 'annotate', 'code')
91 headers = options.pop('headers', None)
91 headers = options.pop('headers', None)
92 if headers and not ('ls' in headers and 'annotate' in headers and
92 if headers and not ('ls' in headers and 'annotate' in headers and
93 'code' in headers):
93 'code' in headers
94 ):
94 raise ValueError("If headers option dict is specified it must "
95 raise ValueError("If headers option dict is specified it must "
95 "all 'ls', 'annotate' and 'code' keys")
96 "all 'ls', 'annotate' and 'code' keys")
96 self.headers = headers
97 self.headers = headers
97 if isinstance(filenode, FileNode):
98 if isinstance(filenode, FileNode):
98 self.filenode = filenode
99 self.filenode = filenode
99 else:
100 else:
100 raise VCSError("This formatter expect FileNode parameter, not %r"
101 raise VCSError("This formatter expect FileNode parameter, not %r"
101 % type(filenode))
102 % type(filenode))
102
103
103 def annotate_from_changeset(self, changeset):
104 def annotate_from_changeset(self, changeset):
104 """
105 """
105 Returns full html line for single changeset per annotated line.
106 Returns full html line for single changeset per annotated line.
106 """
107 """
107 if self.annotate_from_changeset_func:
108 if self.annotate_from_changeset_func:
108 return self.annotate_from_changeset_func(changeset)
109 return self.annotate_from_changeset_func(changeset)
109 else:
110 else:
110 return ''.join((changeset.id, '\n'))
111 return ''.join((changeset.id, '\n'))
111
112
112 def _wrap_tablelinenos(self, inner):
113 def _wrap_tablelinenos(self, inner):
113 dummyoutfile = StringIO.StringIO()
114 dummyoutfile = StringIO.StringIO()
114 lncount = 0
115 lncount = 0
115 for t, line in inner:
116 for t, line in inner:
116 if t:
117 if t:
117 lncount += 1
118 lncount += 1
118 dummyoutfile.write(line)
119 dummyoutfile.write(line)
119
120
120 fl = self.linenostart
121 fl = self.linenostart
121 mw = len(str(lncount + fl - 1))
122 mw = len(str(lncount + fl - 1))
122 sp = self.linenospecial
123 sp = self.linenospecial
123 st = self.linenostep
124 st = self.linenostep
124 la = self.lineanchors
125 la = self.lineanchors
125 aln = self.anchorlinenos
126 aln = self.anchorlinenos
126 if sp:
127 if sp:
127 lines = []
128 lines = []
128
129
129 for i in range(fl, fl + lncount):
130 for i in range(fl, fl + lncount):
130 if i % st == 0:
131 if i % st == 0:
131 if i % sp == 0:
132 if i % sp == 0:
132 if aln:
133 if aln:
133 lines.append('<a href="#%s-%d" class="special">'
134 lines.append('<a href="#%s-%d" class="special">'
134 '%*d</a>' %
135 '%*d</a>' %
135 (la, i, mw, i))
136 (la, i, mw, i))
136 else:
137 else:
137 lines.append('<span class="special">'
138 lines.append('<span class="special">'
138 '%*d</span>' % (mw, i))
139 '%*d</span>' % (mw, i))
139 else:
140 else:
140 if aln:
141 if aln:
141 lines.append('<a href="#%s-%d">'
142 lines.append('<a href="#%s-%d">'
142 '%*d</a>' % (la, i, mw, i))
143 '%*d</a>' % (la, i, mw, i))
143 else:
144 else:
144 lines.append('%*d' % (mw, i))
145 lines.append('%*d' % (mw, i))
145 else:
146 else:
146 lines.append('')
147 lines.append('')
147 ls = '\n'.join(lines)
148 ls = '\n'.join(lines)
148 else:
149 else:
149 lines = []
150 lines = []
150 for i in range(fl, fl + lncount):
151 for i in range(fl, fl + lncount):
151 if i % st == 0:
152 if i % st == 0:
152 if aln:
153 if aln:
153 lines.append('<a href="#%s-%d">%*d</a>'
154 lines.append('<a href="#%s-%d">%*d</a>'
154 % (la, i, mw, i))
155 % (la, i, mw, i))
155 else:
156 else:
156 lines.append('%*d' % (mw, i))
157 lines.append('%*d' % (mw, i))
157 else:
158 else:
158 lines.append('')
159 lines.append('')
159 ls = '\n'.join(lines)
160 ls = '\n'.join(lines)
160
161
161 # annotate_changesets = [tup[1] for tup in self.filenode.annotate]
162 # annotate_changesets = [tup[1] for tup in self.filenode.annotate]
162 # # TODO: not sure what that fixes
163 # # TODO: not sure what that fixes
163 # # If pygments cropped last lines break we need do that too
164 # # If pygments cropped last lines break we need do that too
164 # ln_cs = len(annotate_changesets)
165 # ln_cs = len(annotate_changesets)
165 # ln_ = len(ls.splitlines())
166 # ln_ = len(ls.splitlines())
166 # if ln_cs > ln_:
167 # if ln_cs > ln_:
167 # annotate_changesets = annotate_changesets[:ln_ - ln_cs]
168 # annotate_changesets = annotate_changesets[:ln_ - ln_cs]
168 annotate = ''.join((self.annotate_from_changeset(el[2]())
169 annotate = ''.join((self.annotate_from_changeset(el[2]())
169 for el in self.filenode.annotate))
170 for el in self.filenode.annotate))
170 # in case you wonder about the seemingly redundant <div> here:
171 # in case you wonder about the seemingly redundant <div> here:
171 # since the content in the other cell also is wrapped in a div,
172 # since the content in the other cell also is wrapped in a div,
172 # some browsers in some configurations seem to mess up the formatting.
173 # some browsers in some configurations seem to mess up the formatting.
173 '''
174 '''
174 yield 0, ('<table class="%stable">' % self.cssclass +
175 yield 0, ('<table class="%stable">' % self.cssclass +
175 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
176 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
176 ls + '</pre></div></td>' +
177 ls + '</pre></div></td>' +
177 '<td class="code">')
178 '<td class="code">')
178 yield 0, dummyoutfile.getvalue()
179 yield 0, dummyoutfile.getvalue()
179 yield 0, '</td></tr></table>'
180 yield 0, '</td></tr></table>'
180
181
181 '''
182 '''
182 headers_row = []
183 headers_row = []
183 if self.headers:
184 if self.headers:
184 headers_row = ['<tr class="annotate-header">']
185 headers_row = ['<tr class="annotate-header">']
185 for key in self.order:
186 for key in self.order:
186 td = ''.join(('<td>', self.headers[key], '</td>'))
187 td = ''.join(('<td>', self.headers[key], '</td>'))
187 headers_row.append(td)
188 headers_row.append(td)
188 headers_row.append('</tr>')
189 headers_row.append('</tr>')
189
190
190 body_row_start = ['<tr>']
191 body_row_start = ['<tr>']
191 for key in self.order:
192 for key in self.order:
192 if key == 'ls':
193 if key == 'ls':
193 body_row_start.append(
194 body_row_start.append(
194 '<td class="linenos"><div class="linenodiv"><pre>' +
195 '<td class="linenos"><div class="linenodiv"><pre>' +
195 ls + '</pre></div></td>')
196 ls + '</pre></div></td>')
196 elif key == 'annotate':
197 elif key == 'annotate':
197 body_row_start.append(
198 body_row_start.append(
198 '<td class="annotate"><div class="annotatediv"><pre>' +
199 '<td class="annotate"><div class="annotatediv"><pre>' +
199 annotate + '</pre></div></td>')
200 annotate + '</pre></div></td>')
200 elif key == 'code':
201 elif key == 'code':
201 body_row_start.append('<td class="code">')
202 body_row_start.append('<td class="code">')
202 yield 0, ('<table class="%stable">' % self.cssclass +
203 yield 0, ('<table class="%stable">' % self.cssclass +
203 ''.join(headers_row) +
204 ''.join(headers_row) +
204 ''.join(body_row_start)
205 ''.join(body_row_start)
205 )
206 )
206 yield 0, dummyoutfile.getvalue()
207 yield 0, dummyoutfile.getvalue()
207 yield 0, '</td></tr></table>'
208 yield 0, '</td></tr></table>'
@@ -1,99 +1,99 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.middleware.simplegit
15 kallithea.lib.middleware.simplegit
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 SimpleGit middleware for handling Git protocol requests (push/clone etc.)
18 SimpleGit middleware for handling Git protocol requests (push/clone etc.)
19 It's implemented with basic auth function
19 It's implemented with basic auth function
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: Apr 28, 2010
23 :created_on: Apr 28, 2010
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27
27
28 """
28 """
29
29
30
30
31 import logging
31 import logging
32 import re
32 import re
33
33
34 from kallithea.lib.base import BaseVCSController
34 from kallithea.lib.base import BaseVCSController
35 from kallithea.lib.hooks import log_pull_action
35 from kallithea.lib.hooks import log_pull_action
36 from kallithea.lib.middleware.pygrack import make_wsgi_app
36 from kallithea.lib.middleware.pygrack import make_wsgi_app
37 from kallithea.lib.utils import make_ui
37 from kallithea.lib.utils import make_ui
38 from kallithea.lib.utils2 import safe_unicode
38 from kallithea.lib.utils2 import safe_unicode
39 from kallithea.model.db import Repository
39 from kallithea.model.db import Repository
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$')
45 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$')
46
46
47
47
48 cmd_mapping = {
48 cmd_mapping = {
49 'git-receive-pack': 'push',
49 'git-receive-pack': 'push',
50 'git-upload-pack': 'pull',
50 'git-upload-pack': 'pull',
51 }
51 }
52
52
53
53
54 class SimpleGit(BaseVCSController):
54 class SimpleGit(BaseVCSController):
55
55
56 scm_alias = 'git'
56 scm_alias = 'git'
57
57
58 @classmethod
58 @classmethod
59 def parse_request(cls, environ):
59 def parse_request(cls, environ):
60 path_info = environ.get('PATH_INFO', '')
60 path_info = environ.get('PATH_INFO', '')
61 m = GIT_PROTO_PAT.match(path_info)
61 m = GIT_PROTO_PAT.match(path_info)
62 if m is None:
62 if m is None:
63 return None
63 return None
64
64
65 class parsed_request(object):
65 class parsed_request(object):
66 # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol
66 # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol
67 repo_name = safe_unicode(m.group(1).rstrip('/'))
67 repo_name = safe_unicode(m.group(1).rstrip('/'))
68 cmd = m.group(2)
68 cmd = m.group(2)
69
69
70 query_string = environ['QUERY_STRING']
70 query_string = environ['QUERY_STRING']
71 if cmd == 'info/refs' and query_string.startswith('service='):
71 if cmd == 'info/refs' and query_string.startswith('service='):
72 service = query_string.split('=', 1)[1]
72 service = query_string.split('=', 1)[1]
73 action = cmd_mapping.get(service)
73 action = cmd_mapping.get(service)
74 else:
74 else:
75 service = None
75 service = None
76 action = cmd_mapping.get(cmd)
76 action = cmd_mapping.get(cmd)
77
77
78 return parsed_request
78 return parsed_request
79
79
80 def _make_app(self, parsed_request):
80 def _make_app(self, parsed_request):
81 """
81 """
82 Return a pygrack wsgi application.
82 Return a pygrack wsgi application.
83 """
83 """
84 pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath)
84 pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath)
85
85
86 def wrapper_app(environ, start_response):
86 def wrapper_app(environ, start_response):
87 if (parsed_request.cmd == 'info/refs' and
87 if (parsed_request.cmd == 'info/refs' and
88 parsed_request.service == 'git-upload-pack'
88 parsed_request.service == 'git-upload-pack'
89 ):
89 ):
90 baseui = make_ui()
90 baseui = make_ui()
91 repo = Repository.get_by_repo_name(parsed_request.repo_name)
91 repo = Repository.get_by_repo_name(parsed_request.repo_name)
92 scm_repo = repo.scm_instance
92 scm_repo = repo.scm_instance
93 # Run hooks, like Mercurial outgoing.pull_logger does
93 # Run hooks, like Mercurial outgoing.pull_logger does
94 log_pull_action(ui=baseui, repo=scm_repo._repo)
94 log_pull_action(ui=baseui, repo=scm_repo._repo)
95 # Note: push hooks are handled by post-receive hook
95 # Note: push hooks are handled by post-receive hook
96
96
97 return pygrack_app(environ, start_response)
97 return pygrack_app(environ, start_response)
98
98
99 return wrapper_app
99 return wrapper_app
@@ -1,254 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Custom paging classes
15 Custom paging classes
16 """
16 """
17 import logging
17 import logging
18 import math
18 import math
19 import re
19 import re
20
20
21 from webhelpers2.html import HTML, literal
21 from webhelpers2.html import HTML, literal
22 from webhelpers.paginate import Page as _Page
22 from webhelpers.paginate import Page as _Page
23
23
24 from kallithea.config.routing import url
24 from kallithea.config.routing import url
25
25
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 class Page(_Page):
30 class Page(_Page):
31 """
31 """
32 Custom pager emitting Bootstrap paginators
32 Custom pager emitting Bootstrap paginators
33 """
33 """
34
34
35 def __init__(self, *args, **kwargs):
35 def __init__(self, *args, **kwargs):
36 kwargs.setdefault('url', url.current)
36 kwargs.setdefault('url', url.current)
37 _Page.__init__(self, *args, **kwargs)
37 _Page.__init__(self, *args, **kwargs)
38
38
39 def _get_pos(self, cur_page, max_page, items):
39 def _get_pos(self, cur_page, max_page, items):
40 edge = (items / 2) + 1
40 edge = (items / 2) + 1
41 if (cur_page <= edge):
41 if (cur_page <= edge):
42 radius = max(items / 2, items - cur_page)
42 radius = max(items / 2, items - cur_page)
43 elif (max_page - cur_page) < edge:
43 elif (max_page - cur_page) < edge:
44 radius = (items - 1) - (max_page - cur_page)
44 radius = (items - 1) - (max_page - cur_page)
45 else:
45 else:
46 radius = items / 2
46 radius = items / 2
47
47
48 left = max(1, (cur_page - (radius)))
48 left = max(1, (cur_page - (radius)))
49 right = min(max_page, cur_page + (radius))
49 right = min(max_page, cur_page + (radius))
50 return left, cur_page, right
50 return left, cur_page, right
51
51
52 def _range(self, regexp_match):
52 def _range(self, regexp_match):
53 """
53 """
54 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
54 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
55
55
56 Arguments:
56 Arguments:
57
57
58 regexp_match
58 regexp_match
59 A "re" (regular expressions) match object containing the
59 A "re" (regular expressions) match object containing the
60 radius of linked pages around the current page in
60 radius of linked pages around the current page in
61 regexp_match.group(1) as a string
61 regexp_match.group(1) as a string
62
62
63 This function is supposed to be called as a callable in
63 This function is supposed to be called as a callable in
64 re.sub.
64 re.sub.
65
65
66 """
66 """
67 radius = int(regexp_match.group(1))
67 radius = int(regexp_match.group(1))
68
68
69 # Compute the first and last page number within the radius
69 # Compute the first and last page number within the radius
70 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
70 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
71 # -> leftmost_page = 5
71 # -> leftmost_page = 5
72 # -> rightmost_page = 9
72 # -> rightmost_page = 9
73 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
73 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
74 self.last_page,
74 self.last_page,
75 (radius * 2) + 1)
75 (radius * 2) + 1)
76 nav_items = []
76 nav_items = []
77
77
78 # Create a link to the first page (unless we are on the first page
78 # Create a link to the first page (unless we are on the first page
79 # or there would be no need to insert '..' spacers)
79 # or there would be no need to insert '..' spacers)
80 if self.page != self.first_page and self.first_page < leftmost_page:
80 if self.page != self.first_page and self.first_page < leftmost_page:
81 nav_items.append(HTML.li(self._pagerlink(self.first_page, self.first_page)))
81 nav_items.append(HTML.li(self._pagerlink(self.first_page, self.first_page)))
82
82
83 # Insert dots if there are pages between the first page
83 # Insert dots if there are pages between the first page
84 # and the currently displayed page range
84 # and the currently displayed page range
85 if leftmost_page - self.first_page > 1:
85 if leftmost_page - self.first_page > 1:
86 # Wrap in a SPAN tag if nolink_attr is set
86 # Wrap in a SPAN tag if nolink_attr is set
87 text_ = '..'
87 text_ = '..'
88 if self.dotdot_attr:
88 if self.dotdot_attr:
89 text_ = HTML.span(c=text_, **self.dotdot_attr)
89 text_ = HTML.span(c=text_, **self.dotdot_attr)
90 nav_items.append(HTML.li(text_))
90 nav_items.append(HTML.li(text_))
91
91
92 for thispage in xrange(leftmost_page, rightmost_page + 1):
92 for thispage in xrange(leftmost_page, rightmost_page + 1):
93 # Highlight the current page number and do not use a link
93 # Highlight the current page number and do not use a link
94 text_ = str(thispage)
94 text_ = str(thispage)
95 if thispage == self.page:
95 if thispage == self.page:
96 # Wrap in a SPAN tag if nolink_attr is set
96 # Wrap in a SPAN tag if nolink_attr is set
97 if self.curpage_attr:
97 if self.curpage_attr:
98 text_ = HTML.li(HTML.span(c=text_), **self.curpage_attr)
98 text_ = HTML.li(HTML.span(c=text_), **self.curpage_attr)
99 nav_items.append(text_)
99 nav_items.append(text_)
100 # Otherwise create just a link to that page
100 # Otherwise create just a link to that page
101 else:
101 else:
102 nav_items.append(HTML.li(self._pagerlink(thispage, text_)))
102 nav_items.append(HTML.li(self._pagerlink(thispage, text_)))
103
103
104 # Insert dots if there are pages between the displayed
104 # Insert dots if there are pages between the displayed
105 # page numbers and the end of the page range
105 # page numbers and the end of the page range
106 if self.last_page - rightmost_page > 1:
106 if self.last_page - rightmost_page > 1:
107 text_ = '..'
107 text_ = '..'
108 # Wrap in a SPAN tag if nolink_attr is set
108 # Wrap in a SPAN tag if nolink_attr is set
109 if self.dotdot_attr:
109 if self.dotdot_attr:
110 text_ = HTML.span(c=text_, **self.dotdot_attr)
110 text_ = HTML.span(c=text_, **self.dotdot_attr)
111 nav_items.append(HTML.li(text_))
111 nav_items.append(HTML.li(text_))
112
112
113 # Create a link to the very last page (unless we are on the last
113 # Create a link to the very last page (unless we are on the last
114 # page or there would be no need to insert '..' spacers)
114 # page or there would be no need to insert '..' spacers)
115 if self.page != self.last_page and rightmost_page < self.last_page:
115 if self.page != self.last_page and rightmost_page < self.last_page:
116 nav_items.append(HTML.li(self._pagerlink(self.last_page, self.last_page)))
116 nav_items.append(HTML.li(self._pagerlink(self.last_page, self.last_page)))
117
117
118 #_page_link = url.current()
118 #_page_link = url.current()
119 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
119 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
120 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
120 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
121 return self.separator.join(nav_items)
121 return self.separator.join(nav_items)
122
122
123 def pager(self, format='<ul class="pagination">$link_previous ~2~ $link_next</ul>', page_param='page', partial_param='partial',
123 def pager(self, format='<ul class="pagination">$link_previous ~2~ $link_next</ul>', page_param='page', partial_param='partial',
124 show_if_single_page=False, separator=' ', onclick=None,
124 show_if_single_page=False, separator=' ', onclick=None,
125 symbol_first='<<', symbol_last='>>',
125 symbol_first='<<', symbol_last='>>',
126 symbol_previous='<', symbol_next='>',
126 symbol_previous='<', symbol_next='>',
127 link_attr=None,
127 link_attr=None,
128 curpage_attr=None,
128 curpage_attr=None,
129 dotdot_attr=None, **kwargs):
129 dotdot_attr=None, **kwargs
130 ):
130 self.curpage_attr = curpage_attr or {'class': 'active'}
131 self.curpage_attr = curpage_attr or {'class': 'active'}
131 self.separator = separator
132 self.separator = separator
132 self.pager_kwargs = kwargs
133 self.pager_kwargs = kwargs
133 self.page_param = page_param
134 self.page_param = page_param
134 self.partial_param = partial_param
135 self.partial_param = partial_param
135 self.onclick = onclick
136 self.onclick = onclick
136 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
137 self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
137 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
138 self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
138
139
139 # Don't show navigator if there is no more than one page
140 # Don't show navigator if there is no more than one page
140 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
141 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
141 return ''
142 return ''
142
143
143 from string import Template
144 from string import Template
144 # Replace ~...~ in token format by range of pages
145 # Replace ~...~ in token format by range of pages
145 result = re.sub(r'~(\d+)~', self._range, format)
146 result = re.sub(r'~(\d+)~', self._range, format)
146
147
147 # Interpolate '%' variables
148 # Interpolate '%' variables
148 result = Template(result).safe_substitute({
149 result = Template(result).safe_substitute({
149 'first_page': self.first_page,
150 'first_page': self.first_page,
150 'last_page': self.last_page,
151 'last_page': self.last_page,
151 'page': self.page,
152 'page': self.page,
152 'page_count': self.page_count,
153 'page_count': self.page_count,
153 'items_per_page': self.items_per_page,
154 'items_per_page': self.items_per_page,
154 'first_item': self.first_item,
155 'first_item': self.first_item,
155 'last_item': self.last_item,
156 'last_item': self.last_item,
156 'item_count': self.item_count,
157 'item_count': self.item_count,
157 'link_first': self.page > self.first_page and
158 'link_first': self.page > self.first_page and
158 self._pagerlink(self.first_page, symbol_first) or '',
159 self._pagerlink(self.first_page, symbol_first) or '',
159 'link_last': self.page < self.last_page and
160 'link_last': self.page < self.last_page and
160 self._pagerlink(self.last_page, symbol_last) or '',
161 self._pagerlink(self.last_page, symbol_last) or '',
161 'link_previous': HTML.li(self.previous_page and
162 'link_previous': HTML.li(self.previous_page and
162 self._pagerlink(self.previous_page, symbol_previous)
163 self._pagerlink(self.previous_page, symbol_previous)
163 or HTML.a(symbol_previous)),
164 or HTML.a(symbol_previous)),
164 'link_next': HTML.li(self.next_page and
165 'link_next': HTML.li(self.next_page and
165 self._pagerlink(self.next_page, symbol_next)
166 self._pagerlink(self.next_page, symbol_next)
166 or HTML.a(symbol_next))
167 or HTML.a(symbol_next))
167 })
168 })
168
169
169 return literal(result)
170 return literal(result)
170
171
171
172
172 class RepoPage(Page):
173 class RepoPage(Page):
173
174
174 def __init__(self, collection, page=1, items_per_page=20,
175 def __init__(self, collection, page=1, items_per_page=20,
175 item_count=None, **kwargs):
176 item_count=None, **kwargs):
176
177
177 """Create a "RepoPage" instance. special pager for paging
178 """Create a "RepoPage" instance. special pager for paging
178 repository
179 repository
179 """
180 """
180 # TODO: call baseclass __init__
181 # TODO: call baseclass __init__
181 self._url_generator = kwargs.pop('url', url.current)
182 self._url_generator = kwargs.pop('url', url.current)
182
183
183 # Safe the kwargs class-wide so they can be used in the pager() method
184 # Safe the kwargs class-wide so they can be used in the pager() method
184 self.kwargs = kwargs
185 self.kwargs = kwargs
185
186
186 # Save a reference to the collection
187 # Save a reference to the collection
187 self.original_collection = collection
188 self.original_collection = collection
188
189
189 self.collection = collection
190 self.collection = collection
190
191
191 # The self.page is the number of the current page.
192 # The self.page is the number of the current page.
192 # The first page has the number 1!
193 # The first page has the number 1!
193 try:
194 try:
194 self.page = int(page) # make it int() if we get it as a string
195 self.page = int(page) # make it int() if we get it as a string
195 except (ValueError, TypeError):
196 except (ValueError, TypeError):
196 log.error("Invalid page value: %r", page)
197 log.error("Invalid page value: %r", page)
197 self.page = 1
198 self.page = 1
198
199
199 self.items_per_page = items_per_page
200 self.items_per_page = items_per_page
200
201
201 # Unless the user tells us how many items the collections has
202 # Unless the user tells us how many items the collections has
202 # we calculate that ourselves.
203 # we calculate that ourselves.
203 if item_count is not None:
204 if item_count is not None:
204 self.item_count = item_count
205 self.item_count = item_count
205 else:
206 else:
206 self.item_count = len(self.collection)
207 self.item_count = len(self.collection)
207
208
208 # Compute the number of the first and last available page
209 # Compute the number of the first and last available page
209 if self.item_count > 0:
210 if self.item_count > 0:
210 self.first_page = 1
211 self.first_page = 1
211 self.page_count = int(math.ceil(float(self.item_count) /
212 self.page_count = int(math.ceil(float(self.item_count) /
212 self.items_per_page))
213 self.items_per_page))
213 self.last_page = self.first_page + self.page_count - 1
214 self.last_page = self.first_page + self.page_count - 1
214
215
215 # Make sure that the requested page number is the range of
216 # Make sure that the requested page number is the range of
216 # valid pages
217 # valid pages
217 if self.page > self.last_page:
218 if self.page > self.last_page:
218 self.page = self.last_page
219 self.page = self.last_page
219 elif self.page < self.first_page:
220 elif self.page < self.first_page:
220 self.page = self.first_page
221 self.page = self.first_page
221
222
222 # Note: the number of items on this page can be less than
223 # Note: the number of items on this page can be less than
223 # items_per_page if the last page is not full
224 # items_per_page if the last page is not full
224 self.first_item = max(0, (self.item_count) - (self.page *
225 self.first_item = max(0, (self.item_count) - (self.page *
225 items_per_page))
226 items_per_page))
226 self.last_item = ((self.item_count - 1) - items_per_page *
227 self.last_item = ((self.item_count - 1) - items_per_page *
227 (self.page - 1))
228 (self.page - 1))
228
229
229 self.items = list(self.collection[self.first_item:self.last_item + 1])
230 self.items = list(self.collection[self.first_item:self.last_item + 1])
230
231
231 # Links to previous and next page
232 # Links to previous and next page
232 if self.page > self.first_page:
233 if self.page > self.first_page:
233 self.previous_page = self.page - 1
234 self.previous_page = self.page - 1
234 else:
235 else:
235 self.previous_page = None
236 self.previous_page = None
236
237
237 if self.page < self.last_page:
238 if self.page < self.last_page:
238 self.next_page = self.page + 1
239 self.next_page = self.page + 1
239 else:
240 else:
240 self.next_page = None
241 self.next_page = None
241
242
242 # No items available
243 # No items available
243 else:
244 else:
244 self.first_page = None
245 self.first_page = None
245 self.page_count = 0
246 self.page_count = 0
246 self.last_page = None
247 self.last_page = None
247 self.first_item = None
248 self.first_item = None
248 self.last_item = None
249 self.last_item = None
249 self.previous_page = None
250 self.previous_page = None
250 self.next_page = None
251 self.next_page = None
251 self.items = []
252 self.items = []
252
253
253 # This is a subclass of the 'list' type. Initialise the list now.
254 # This is a subclass of the 'list' type. Initialise the list now.
254 list.__init__(self, reversed(self.items))
255 list.__init__(self, reversed(self.items))
@@ -1,699 +1,700 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils2
15 kallithea.lib.utils2
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Some simple helper functions.
18 Some simple helper functions.
19 Note: all these functions should be independent of Kallithea classes, i.e.
19 Note: all these functions should be independent of Kallithea classes, i.e.
20 models, controllers, etc. to prevent import cycles.
20 models, controllers, etc. to prevent import cycles.
21
21
22 This file was forked by the Kallithea project in July 2014.
22 This file was forked by the Kallithea project in July 2014.
23 Original author and date, and relevant copyright and licensing information is below:
23 Original author and date, and relevant copyright and licensing information is below:
24 :created_on: Jan 5, 2011
24 :created_on: Jan 5, 2011
25 :author: marcink
25 :author: marcink
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :license: GPLv3, see LICENSE.md for more details.
27 :license: GPLv3, see LICENSE.md for more details.
28 """
28 """
29
29
30
30
31 import binascii
31 import binascii
32 import datetime
32 import datetime
33 import os
33 import os
34 import pwd
34 import pwd
35 import re
35 import re
36 import time
36 import time
37 import urllib
37 import urllib
38
38
39 import urlobject
39 import urlobject
40 from tg.i18n import ugettext as _
40 from tg.i18n import ugettext as _
41 from tg.i18n import ungettext
41 from tg.i18n import ungettext
42 from webhelpers2.text import collapse, remove_formatting, strip_tags
42 from webhelpers2.text import collapse, remove_formatting, strip_tags
43
43
44 from kallithea.lib.compat import json
44 from kallithea.lib.compat import json
45 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.utils.lazy import LazyProperty
46
46
47
47
48 def str2bool(_str):
48 def str2bool(_str):
49 """
49 """
50 returns True/False value from given string, it tries to translate the
50 returns True/False value from given string, it tries to translate the
51 string into boolean
51 string into boolean
52
52
53 :param _str: string value to translate into boolean
53 :param _str: string value to translate into boolean
54 :rtype: boolean
54 :rtype: boolean
55 :returns: boolean from given string
55 :returns: boolean from given string
56 """
56 """
57 if _str is None:
57 if _str is None:
58 return False
58 return False
59 if _str in (True, False):
59 if _str in (True, False):
60 return _str
60 return _str
61 _str = str(_str).strip().lower()
61 _str = str(_str).strip().lower()
62 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
62 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
63
63
64
64
65 def aslist(obj, sep=None, strip=True):
65 def aslist(obj, sep=None, strip=True):
66 """
66 """
67 Returns given string separated by sep as list
67 Returns given string separated by sep as list
68
68
69 :param obj:
69 :param obj:
70 :param sep:
70 :param sep:
71 :param strip:
71 :param strip:
72 """
72 """
73 if isinstance(obj, (basestring)):
73 if isinstance(obj, (basestring)):
74 lst = obj.split(sep)
74 lst = obj.split(sep)
75 if strip:
75 if strip:
76 lst = [v.strip() for v in lst]
76 lst = [v.strip() for v in lst]
77 return lst
77 return lst
78 elif isinstance(obj, (list, tuple)):
78 elif isinstance(obj, (list, tuple)):
79 return obj
79 return obj
80 elif obj is None:
80 elif obj is None:
81 return []
81 return []
82 else:
82 else:
83 return [obj]
83 return [obj]
84
84
85
85
86 def convert_line_endings(line, mode):
86 def convert_line_endings(line, mode):
87 """
87 """
88 Converts a given line "line end" according to given mode
88 Converts a given line "line end" according to given mode
89
89
90 Available modes are::
90 Available modes are::
91 0 - Unix
91 0 - Unix
92 1 - Mac
92 1 - Mac
93 2 - DOS
93 2 - DOS
94
94
95 :param line: given line to convert
95 :param line: given line to convert
96 :param mode: mode to convert to
96 :param mode: mode to convert to
97 :rtype: str
97 :rtype: str
98 :return: converted line according to mode
98 :return: converted line according to mode
99 """
99 """
100 from string import replace
100 from string import replace
101
101
102 if mode == 0:
102 if mode == 0:
103 line = replace(line, '\r\n', '\n')
103 line = replace(line, '\r\n', '\n')
104 line = replace(line, '\r', '\n')
104 line = replace(line, '\r', '\n')
105 elif mode == 1:
105 elif mode == 1:
106 line = replace(line, '\r\n', '\r')
106 line = replace(line, '\r\n', '\r')
107 line = replace(line, '\n', '\r')
107 line = replace(line, '\n', '\r')
108 elif mode == 2:
108 elif mode == 2:
109 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
109 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
110 return line
110 return line
111
111
112
112
113 def detect_mode(line, default):
113 def detect_mode(line, default):
114 """
114 """
115 Detects line break for given line, if line break couldn't be found
115 Detects line break for given line, if line break couldn't be found
116 given default value is returned
116 given default value is returned
117
117
118 :param line: str line
118 :param line: str line
119 :param default: default
119 :param default: default
120 :rtype: int
120 :rtype: int
121 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
121 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
122 """
122 """
123 if line.endswith('\r\n'):
123 if line.endswith('\r\n'):
124 return 2
124 return 2
125 elif line.endswith('\n'):
125 elif line.endswith('\n'):
126 return 0
126 return 0
127 elif line.endswith('\r'):
127 elif line.endswith('\r'):
128 return 1
128 return 1
129 else:
129 else:
130 return default
130 return default
131
131
132
132
133 def generate_api_key():
133 def generate_api_key():
134 """
134 """
135 Generates a random (presumably unique) API key.
135 Generates a random (presumably unique) API key.
136
136
137 This value is used in URLs and "Bearer" HTTP Authorization headers,
137 This value is used in URLs and "Bearer" HTTP Authorization headers,
138 which in practice means it should only contain URL-safe characters
138 which in practice means it should only contain URL-safe characters
139 (RFC 3986):
139 (RFC 3986):
140
140
141 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
141 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
142 """
142 """
143 # Hexadecimal certainly qualifies as URL-safe.
143 # Hexadecimal certainly qualifies as URL-safe.
144 return binascii.hexlify(os.urandom(20))
144 return binascii.hexlify(os.urandom(20))
145
145
146
146
147 def safe_int(val, default=None):
147 def safe_int(val, default=None):
148 """
148 """
149 Returns int() of val if val is not convertable to int use default
149 Returns int() of val if val is not convertable to int use default
150 instead
150 instead
151
151
152 :param val:
152 :param val:
153 :param default:
153 :param default:
154 """
154 """
155
155
156 try:
156 try:
157 val = int(val)
157 val = int(val)
158 except (ValueError, TypeError):
158 except (ValueError, TypeError):
159 val = default
159 val = default
160
160
161 return val
161 return val
162
162
163
163
164 def safe_unicode(str_, from_encoding=None):
164 def safe_unicode(str_, from_encoding=None):
165 """
165 """
166 safe unicode function. Does few trick to turn str_ into unicode
166 safe unicode function. Does few trick to turn str_ into unicode
167
167
168 In case of UnicodeDecode error we try to return it with encoding detected
168 In case of UnicodeDecode error we try to return it with encoding detected
169 by chardet library if it fails fallback to unicode with errors replaced
169 by chardet library if it fails fallback to unicode with errors replaced
170
170
171 :param str_: string to decode
171 :param str_: string to decode
172 :rtype: unicode
172 :rtype: unicode
173 :returns: unicode object
173 :returns: unicode object
174 """
174 """
175 if isinstance(str_, unicode):
175 if isinstance(str_, unicode):
176 return str_
176 return str_
177
177
178 if not from_encoding:
178 if not from_encoding:
179 import kallithea
179 import kallithea
180 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
180 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
181 'utf-8'), sep=',')
181 'utf-8'), sep=',')
182 from_encoding = DEFAULT_ENCODINGS
182 from_encoding = DEFAULT_ENCODINGS
183
183
184 if not isinstance(from_encoding, (list, tuple)):
184 if not isinstance(from_encoding, (list, tuple)):
185 from_encoding = [from_encoding]
185 from_encoding = [from_encoding]
186
186
187 try:
187 try:
188 return unicode(str_)
188 return unicode(str_)
189 except UnicodeDecodeError:
189 except UnicodeDecodeError:
190 pass
190 pass
191
191
192 for enc in from_encoding:
192 for enc in from_encoding:
193 try:
193 try:
194 return unicode(str_, enc)
194 return unicode(str_, enc)
195 except UnicodeDecodeError:
195 except UnicodeDecodeError:
196 pass
196 pass
197
197
198 try:
198 try:
199 import chardet
199 import chardet
200 encoding = chardet.detect(str_)['encoding']
200 encoding = chardet.detect(str_)['encoding']
201 if encoding is None:
201 if encoding is None:
202 raise Exception()
202 raise Exception()
203 return str_.decode(encoding)
203 return str_.decode(encoding)
204 except (ImportError, UnicodeDecodeError, Exception):
204 except (ImportError, UnicodeDecodeError, Exception):
205 return unicode(str_, from_encoding[0], 'replace')
205 return unicode(str_, from_encoding[0], 'replace')
206
206
207
207
208 def safe_str(unicode_, to_encoding=None):
208 def safe_str(unicode_, to_encoding=None):
209 """
209 """
210 safe str function. Does few trick to turn unicode_ into string
210 safe str function. Does few trick to turn unicode_ into string
211
211
212 In case of UnicodeEncodeError we try to return it with encoding detected
212 In case of UnicodeEncodeError we try to return it with encoding detected
213 by chardet library if it fails fallback to string with errors replaced
213 by chardet library if it fails fallback to string with errors replaced
214
214
215 :param unicode_: unicode to encode
215 :param unicode_: unicode to encode
216 :rtype: str
216 :rtype: str
217 :returns: str object
217 :returns: str object
218 """
218 """
219
219
220 # if it's not basestr cast to str
220 # if it's not basestr cast to str
221 if not isinstance(unicode_, basestring):
221 if not isinstance(unicode_, basestring):
222 return str(unicode_)
222 return str(unicode_)
223
223
224 if isinstance(unicode_, str):
224 if isinstance(unicode_, str):
225 return unicode_
225 return unicode_
226
226
227 if not to_encoding:
227 if not to_encoding:
228 import kallithea
228 import kallithea
229 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
229 DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
230 'utf-8'), sep=',')
230 'utf-8'), sep=',')
231 to_encoding = DEFAULT_ENCODINGS
231 to_encoding = DEFAULT_ENCODINGS
232
232
233 if not isinstance(to_encoding, (list, tuple)):
233 if not isinstance(to_encoding, (list, tuple)):
234 to_encoding = [to_encoding]
234 to_encoding = [to_encoding]
235
235
236 for enc in to_encoding:
236 for enc in to_encoding:
237 try:
237 try:
238 return unicode_.encode(enc)
238 return unicode_.encode(enc)
239 except UnicodeEncodeError:
239 except UnicodeEncodeError:
240 pass
240 pass
241
241
242 try:
242 try:
243 import chardet
243 import chardet
244 encoding = chardet.detect(unicode_)['encoding']
244 encoding = chardet.detect(unicode_)['encoding']
245 if encoding is None:
245 if encoding is None:
246 raise UnicodeEncodeError()
246 raise UnicodeEncodeError()
247
247
248 return unicode_.encode(encoding)
248 return unicode_.encode(encoding)
249 except (ImportError, UnicodeEncodeError):
249 except (ImportError, UnicodeEncodeError):
250 return unicode_.encode(to_encoding[0], 'replace')
250 return unicode_.encode(to_encoding[0], 'replace')
251
251
252
252
253 def remove_suffix(s, suffix):
253 def remove_suffix(s, suffix):
254 if s.endswith(suffix):
254 if s.endswith(suffix):
255 s = s[:-1 * len(suffix)]
255 s = s[:-1 * len(suffix)]
256 return s
256 return s
257
257
258
258
259 def remove_prefix(s, prefix):
259 def remove_prefix(s, prefix):
260 if s.startswith(prefix):
260 if s.startswith(prefix):
261 s = s[len(prefix):]
261 s = s[len(prefix):]
262 return s
262 return s
263
263
264
264
265 def age(prevdate, show_short_version=False, now=None):
265 def age(prevdate, show_short_version=False, now=None):
266 """
266 """
267 turns a datetime into an age string.
267 turns a datetime into an age string.
268 If show_short_version is True, then it will generate a not so accurate but shorter string,
268 If show_short_version is True, then it will generate a not so accurate but shorter string,
269 example: 2days ago, instead of 2 days and 23 hours ago.
269 example: 2days ago, instead of 2 days and 23 hours ago.
270
270
271 :param prevdate: datetime object
271 :param prevdate: datetime object
272 :param show_short_version: if it should approximate the date and return a shorter string
272 :param show_short_version: if it should approximate the date and return a shorter string
273 :rtype: unicode
273 :rtype: unicode
274 :returns: unicode words describing age
274 :returns: unicode words describing age
275 """
275 """
276 now = now or datetime.datetime.now()
276 now = now or datetime.datetime.now()
277 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
277 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
278 deltas = {}
278 deltas = {}
279 future = False
279 future = False
280
280
281 if prevdate > now:
281 if prevdate > now:
282 now, prevdate = prevdate, now
282 now, prevdate = prevdate, now
283 future = True
283 future = True
284 if future:
284 if future:
285 prevdate = prevdate.replace(microsecond=0)
285 prevdate = prevdate.replace(microsecond=0)
286 # Get date parts deltas
286 # Get date parts deltas
287 from dateutil import relativedelta
287 from dateutil import relativedelta
288 for part in order:
288 for part in order:
289 d = relativedelta.relativedelta(now, prevdate)
289 d = relativedelta.relativedelta(now, prevdate)
290 deltas[part] = getattr(d, part + 's')
290 deltas[part] = getattr(d, part + 's')
291
291
292 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
292 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
293 # not 1 hour, -59 minutes and -59 seconds)
293 # not 1 hour, -59 minutes and -59 seconds)
294 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
294 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
295 part = order[num]
295 part = order[num]
296 carry_part = order[num - 1]
296 carry_part = order[num - 1]
297
297
298 if deltas[part] < 0:
298 if deltas[part] < 0:
299 deltas[part] += length
299 deltas[part] += length
300 deltas[carry_part] -= 1
300 deltas[carry_part] -= 1
301
301
302 # Same thing for days except that the increment depends on the (variable)
302 # Same thing for days except that the increment depends on the (variable)
303 # number of days in the month
303 # number of days in the month
304 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
304 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
305 if deltas['day'] < 0:
305 if deltas['day'] < 0:
306 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
306 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
307 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
307 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)
308 ):
308 deltas['day'] += 29
309 deltas['day'] += 29
309 else:
310 else:
310 deltas['day'] += month_lengths[prevdate.month - 1]
311 deltas['day'] += month_lengths[prevdate.month - 1]
311
312
312 deltas['month'] -= 1
313 deltas['month'] -= 1
313
314
314 if deltas['month'] < 0:
315 if deltas['month'] < 0:
315 deltas['month'] += 12
316 deltas['month'] += 12
316 deltas['year'] -= 1
317 deltas['year'] -= 1
317
318
318 # In short version, we want nicer handling of ages of more than a year
319 # In short version, we want nicer handling of ages of more than a year
319 if show_short_version:
320 if show_short_version:
320 if deltas['year'] == 1:
321 if deltas['year'] == 1:
321 # ages between 1 and 2 years: show as months
322 # ages between 1 and 2 years: show as months
322 deltas['month'] += 12
323 deltas['month'] += 12
323 deltas['year'] = 0
324 deltas['year'] = 0
324 if deltas['year'] >= 2:
325 if deltas['year'] >= 2:
325 # ages 2+ years: round
326 # ages 2+ years: round
326 if deltas['month'] > 6:
327 if deltas['month'] > 6:
327 deltas['year'] += 1
328 deltas['year'] += 1
328 deltas['month'] = 0
329 deltas['month'] = 0
329
330
330 # Format the result
331 # Format the result
331 fmt_funcs = {
332 fmt_funcs = {
332 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
333 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
333 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
334 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
334 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
335 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
335 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
336 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
336 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
337 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
337 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
338 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
338 }
339 }
339
340
340 for i, part in enumerate(order):
341 for i, part in enumerate(order):
341 value = deltas[part]
342 value = deltas[part]
342 if value == 0:
343 if value == 0:
343 continue
344 continue
344
345
345 if i < 5:
346 if i < 5:
346 sub_part = order[i + 1]
347 sub_part = order[i + 1]
347 sub_value = deltas[sub_part]
348 sub_value = deltas[sub_part]
348 else:
349 else:
349 sub_value = 0
350 sub_value = 0
350
351
351 if sub_value == 0 or show_short_version:
352 if sub_value == 0 or show_short_version:
352 if future:
353 if future:
353 return _('in %s') % fmt_funcs[part](value)
354 return _('in %s') % fmt_funcs[part](value)
354 else:
355 else:
355 return _('%s ago') % fmt_funcs[part](value)
356 return _('%s ago') % fmt_funcs[part](value)
356 if future:
357 if future:
357 return _('in %s and %s') % (fmt_funcs[part](value),
358 return _('in %s and %s') % (fmt_funcs[part](value),
358 fmt_funcs[sub_part](sub_value))
359 fmt_funcs[sub_part](sub_value))
359 else:
360 else:
360 return _('%s and %s ago') % (fmt_funcs[part](value),
361 return _('%s and %s ago') % (fmt_funcs[part](value),
361 fmt_funcs[sub_part](sub_value))
362 fmt_funcs[sub_part](sub_value))
362
363
363 return _('just now')
364 return _('just now')
364
365
365
366
366 def uri_filter(uri):
367 def uri_filter(uri):
367 """
368 """
368 Removes user:password from given url string
369 Removes user:password from given url string
369
370
370 :param uri:
371 :param uri:
371 :rtype: unicode
372 :rtype: unicode
372 :returns: filtered list of strings
373 :returns: filtered list of strings
373 """
374 """
374 if not uri:
375 if not uri:
375 return ''
376 return ''
376
377
377 proto = ''
378 proto = ''
378
379
379 for pat in ('https://', 'http://', 'git://'):
380 for pat in ('https://', 'http://', 'git://'):
380 if uri.startswith(pat):
381 if uri.startswith(pat):
381 uri = uri[len(pat):]
382 uri = uri[len(pat):]
382 proto = pat
383 proto = pat
383 break
384 break
384
385
385 # remove passwords and username
386 # remove passwords and username
386 uri = uri[uri.find('@') + 1:]
387 uri = uri[uri.find('@') + 1:]
387
388
388 # get the port
389 # get the port
389 cred_pos = uri.find(':')
390 cred_pos = uri.find(':')
390 if cred_pos == -1:
391 if cred_pos == -1:
391 host, port = uri, None
392 host, port = uri, None
392 else:
393 else:
393 host, port = uri[:cred_pos], uri[cred_pos + 1:]
394 host, port = uri[:cred_pos], uri[cred_pos + 1:]
394
395
395 return filter(None, [proto, host, port])
396 return filter(None, [proto, host, port])
396
397
397
398
398 def credentials_filter(uri):
399 def credentials_filter(uri):
399 """
400 """
400 Returns a url with removed credentials
401 Returns a url with removed credentials
401
402
402 :param uri:
403 :param uri:
403 """
404 """
404
405
405 uri = uri_filter(uri)
406 uri = uri_filter(uri)
406 # check if we have port
407 # check if we have port
407 if len(uri) > 2 and uri[2]:
408 if len(uri) > 2 and uri[2]:
408 uri[2] = ':' + uri[2]
409 uri[2] = ':' + uri[2]
409
410
410 return ''.join(uri)
411 return ''.join(uri)
411
412
412
413
413 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
414 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
414 parsed_url = urlobject.URLObject(prefix_url)
415 parsed_url = urlobject.URLObject(prefix_url)
415 prefix = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
416 prefix = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
416 try:
417 try:
417 system_user = pwd.getpwuid(os.getuid()).pw_name
418 system_user = pwd.getpwuid(os.getuid()).pw_name
418 except Exception: # TODO: support all systems - especially Windows
419 except Exception: # TODO: support all systems - especially Windows
419 system_user = 'kallithea' # hardcoded default value ...
420 system_user = 'kallithea' # hardcoded default value ...
420 args = {
421 args = {
421 'scheme': parsed_url.scheme,
422 'scheme': parsed_url.scheme,
422 'user': safe_unicode(urllib.quote(safe_str(username or ''))),
423 'user': safe_unicode(urllib.quote(safe_str(username or ''))),
423 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
424 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
424 'prefix': prefix, # undocumented, empty or starting with /
425 'prefix': prefix, # undocumented, empty or starting with /
425 'repo': repo_name,
426 'repo': repo_name,
426 'repoid': str(repo_id),
427 'repoid': str(repo_id),
427 'system_user': safe_unicode(system_user),
428 'system_user': safe_unicode(system_user),
428 'hostname': parsed_url.hostname,
429 'hostname': parsed_url.hostname,
429 }
430 }
430 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
431 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
431
432
432 # remove leading @ sign if it's present. Case of empty user
433 # remove leading @ sign if it's present. Case of empty user
433 url_obj = urlobject.URLObject(url)
434 url_obj = urlobject.URLObject(url)
434 if not url_obj.username:
435 if not url_obj.username:
435 url_obj = url_obj.with_username(None)
436 url_obj = url_obj.with_username(None)
436
437
437 return safe_unicode(url_obj)
438 return safe_unicode(url_obj)
438
439
439
440
440 def get_changeset_safe(repo, rev):
441 def get_changeset_safe(repo, rev):
441 """
442 """
442 Safe version of get_changeset if this changeset doesn't exists for a
443 Safe version of get_changeset if this changeset doesn't exists for a
443 repo it returns a Dummy one instead
444 repo it returns a Dummy one instead
444
445
445 :param repo:
446 :param repo:
446 :param rev:
447 :param rev:
447 """
448 """
448 from kallithea.lib.vcs.backends.base import BaseRepository
449 from kallithea.lib.vcs.backends.base import BaseRepository
449 from kallithea.lib.vcs.exceptions import RepositoryError
450 from kallithea.lib.vcs.exceptions import RepositoryError
450 from kallithea.lib.vcs.backends.base import EmptyChangeset
451 from kallithea.lib.vcs.backends.base import EmptyChangeset
451 if not isinstance(repo, BaseRepository):
452 if not isinstance(repo, BaseRepository):
452 raise Exception('You must pass an Repository '
453 raise Exception('You must pass an Repository '
453 'object as first argument got %s', type(repo))
454 'object as first argument got %s', type(repo))
454
455
455 try:
456 try:
456 cs = repo.get_changeset(rev)
457 cs = repo.get_changeset(rev)
457 except (RepositoryError, LookupError):
458 except (RepositoryError, LookupError):
458 cs = EmptyChangeset(requested_revision=rev)
459 cs = EmptyChangeset(requested_revision=rev)
459 return cs
460 return cs
460
461
461
462
462 def datetime_to_time(dt):
463 def datetime_to_time(dt):
463 if dt:
464 if dt:
464 return time.mktime(dt.timetuple())
465 return time.mktime(dt.timetuple())
465
466
466
467
467 def time_to_datetime(tm):
468 def time_to_datetime(tm):
468 if tm:
469 if tm:
469 if isinstance(tm, basestring):
470 if isinstance(tm, basestring):
470 try:
471 try:
471 tm = float(tm)
472 tm = float(tm)
472 except ValueError:
473 except ValueError:
473 return
474 return
474 return datetime.datetime.fromtimestamp(tm)
475 return datetime.datetime.fromtimestamp(tm)
475
476
476
477
477 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
478 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
478 # Check char before @ - it must not look like we are in an email addresses.
479 # Check char before @ - it must not look like we are in an email addresses.
479 # Matching is greedy so we don't have to look beyond the end.
480 # Matching is greedy so we don't have to look beyond the end.
480 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
481 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
481
482
482
483
483 def extract_mentioned_usernames(text):
484 def extract_mentioned_usernames(text):
484 r"""
485 r"""
485 Returns list of (possible) usernames @mentioned in given text.
486 Returns list of (possible) usernames @mentioned in given text.
486
487
487 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
488 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
488 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
489 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
489 """
490 """
490 return MENTIONS_REGEX.findall(text)
491 return MENTIONS_REGEX.findall(text)
491
492
492
493
493 def extract_mentioned_users(text):
494 def extract_mentioned_users(text):
494 """ Returns set of actual database Users @mentioned in given text. """
495 """ Returns set of actual database Users @mentioned in given text. """
495 from kallithea.model.db import User
496 from kallithea.model.db import User
496 result = set()
497 result = set()
497 for name in extract_mentioned_usernames(text):
498 for name in extract_mentioned_usernames(text):
498 user = User.get_by_username(name, case_insensitive=True)
499 user = User.get_by_username(name, case_insensitive=True)
499 if user is not None and not user.is_default_user:
500 if user is not None and not user.is_default_user:
500 result.add(user)
501 result.add(user)
501 return result
502 return result
502
503
503
504
504 class AttributeDict(dict):
505 class AttributeDict(dict):
505 def __getattr__(self, attr):
506 def __getattr__(self, attr):
506 return self.get(attr, None)
507 return self.get(attr, None)
507 __setattr__ = dict.__setitem__
508 __setattr__ = dict.__setitem__
508 __delattr__ = dict.__delitem__
509 __delattr__ = dict.__delitem__
509
510
510
511
511 def obfuscate_url_pw(engine):
512 def obfuscate_url_pw(engine):
512 from sqlalchemy.engine import url as sa_url
513 from sqlalchemy.engine import url as sa_url
513 from sqlalchemy.exc import ArgumentError
514 from sqlalchemy.exc import ArgumentError
514 try:
515 try:
515 _url = sa_url.make_url(engine or '')
516 _url = sa_url.make_url(engine or '')
516 except ArgumentError:
517 except ArgumentError:
517 return engine
518 return engine
518 if _url.password:
519 if _url.password:
519 _url.password = 'XXXXX'
520 _url.password = 'XXXXX'
520 return str(_url)
521 return str(_url)
521
522
522
523
523 def get_hook_environment():
524 def get_hook_environment():
524 """
525 """
525 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
526 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
526 variable.
527 variable.
527
528
528 Called early in Git out-of-process hooks to get .ini config path so the
529 Called early in Git out-of-process hooks to get .ini config path so the
529 basic environment can be configured properly. Also used in all hooks to get
530 basic environment can be configured properly. Also used in all hooks to get
530 information about the action that triggered it.
531 information about the action that triggered it.
531 """
532 """
532
533
533 try:
534 try:
534 extras = json.loads(os.environ['KALLITHEA_EXTRAS'])
535 extras = json.loads(os.environ['KALLITHEA_EXTRAS'])
535 except KeyError:
536 except KeyError:
536 raise Exception("Environment variable KALLITHEA_EXTRAS not found")
537 raise Exception("Environment variable KALLITHEA_EXTRAS not found")
537
538
538 try:
539 try:
539 for k in ['username', 'repository', 'scm', 'action', 'ip']:
540 for k in ['username', 'repository', 'scm', 'action', 'ip']:
540 extras[k]
541 extras[k]
541 except KeyError:
542 except KeyError:
542 raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
543 raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
543
544
544 return AttributeDict(extras)
545 return AttributeDict(extras)
545
546
546
547
547 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
548 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
548 """Prepare global context for running hooks by serializing data in the
549 """Prepare global context for running hooks by serializing data in the
549 global KALLITHEA_EXTRAS environment variable.
550 global KALLITHEA_EXTRAS environment variable.
550
551
551 Most importantly, this allow Git hooks to do proper logging and updating of
552 Most importantly, this allow Git hooks to do proper logging and updating of
552 caches after pushes.
553 caches after pushes.
553
554
554 Must always be called before anything with hooks are invoked.
555 Must always be called before anything with hooks are invoked.
555 """
556 """
556 from kallithea import CONFIG
557 from kallithea import CONFIG
557 extras = {
558 extras = {
558 'ip': ip_addr, # used in log_push/pull_action action_logger
559 'ip': ip_addr, # used in log_push/pull_action action_logger
559 'username': username,
560 'username': username,
560 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
561 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
561 'repository': repo_name,
562 'repository': repo_name,
562 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
563 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
563 'config': CONFIG['__file__'], # used by git hook to read config
564 'config': CONFIG['__file__'], # used by git hook to read config
564 }
565 }
565 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
566 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
566
567
567
568
568 def get_current_authuser():
569 def get_current_authuser():
569 """
570 """
570 Gets kallithea user from threadlocal tmpl_context variable if it's
571 Gets kallithea user from threadlocal tmpl_context variable if it's
571 defined, else returns None.
572 defined, else returns None.
572 """
573 """
573 from tg import tmpl_context
574 from tg import tmpl_context
574 if hasattr(tmpl_context, 'authuser'):
575 if hasattr(tmpl_context, 'authuser'):
575 return tmpl_context.authuser
576 return tmpl_context.authuser
576
577
577 return None
578 return None
578
579
579
580
580 class OptionalAttr(object):
581 class OptionalAttr(object):
581 """
582 """
582 Special Optional Option that defines other attribute. Example::
583 Special Optional Option that defines other attribute. Example::
583
584
584 def test(apiuser, userid=Optional(OAttr('apiuser')):
585 def test(apiuser, userid=Optional(OAttr('apiuser')):
585 user = Optional.extract(userid)
586 user = Optional.extract(userid)
586 # calls
587 # calls
587
588
588 """
589 """
589
590
590 def __init__(self, attr_name):
591 def __init__(self, attr_name):
591 self.attr_name = attr_name
592 self.attr_name = attr_name
592
593
593 def __repr__(self):
594 def __repr__(self):
594 return '<OptionalAttr:%s>' % self.attr_name
595 return '<OptionalAttr:%s>' % self.attr_name
595
596
596 def __call__(self):
597 def __call__(self):
597 return self
598 return self
598
599
599
600
600 # alias
601 # alias
601 OAttr = OptionalAttr
602 OAttr = OptionalAttr
602
603
603
604
604 class Optional(object):
605 class Optional(object):
605 """
606 """
606 Defines an optional parameter::
607 Defines an optional parameter::
607
608
608 param = param.getval() if isinstance(param, Optional) else param
609 param = param.getval() if isinstance(param, Optional) else param
609 param = param() if isinstance(param, Optional) else param
610 param = param() if isinstance(param, Optional) else param
610
611
611 is equivalent of::
612 is equivalent of::
612
613
613 param = Optional.extract(param)
614 param = Optional.extract(param)
614
615
615 """
616 """
616
617
617 def __init__(self, type_):
618 def __init__(self, type_):
618 self.type_ = type_
619 self.type_ = type_
619
620
620 def __repr__(self):
621 def __repr__(self):
621 return '<Optional:%s>' % self.type_.__repr__()
622 return '<Optional:%s>' % self.type_.__repr__()
622
623
623 def __call__(self):
624 def __call__(self):
624 return self.getval()
625 return self.getval()
625
626
626 def getval(self):
627 def getval(self):
627 """
628 """
628 returns value from this Optional instance
629 returns value from this Optional instance
629 """
630 """
630 if isinstance(self.type_, OAttr):
631 if isinstance(self.type_, OAttr):
631 # use params name
632 # use params name
632 return self.type_.attr_name
633 return self.type_.attr_name
633 return self.type_
634 return self.type_
634
635
635 @classmethod
636 @classmethod
636 def extract(cls, val):
637 def extract(cls, val):
637 """
638 """
638 Extracts value from Optional() instance
639 Extracts value from Optional() instance
639
640
640 :param val:
641 :param val:
641 :return: original value if it's not Optional instance else
642 :return: original value if it's not Optional instance else
642 value of instance
643 value of instance
643 """
644 """
644 if isinstance(val, cls):
645 if isinstance(val, cls):
645 return val.getval()
646 return val.getval()
646 return val
647 return val
647
648
648
649
649 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
650 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
650 return _cleanstringsub('_', safe_str(s)).rstrip('_')
651 return _cleanstringsub('_', safe_str(s)).rstrip('_')
651
652
652
653
653 def recursive_replace(str_, replace=' '):
654 def recursive_replace(str_, replace=' '):
654 """
655 """
655 Recursive replace of given sign to just one instance
656 Recursive replace of given sign to just one instance
656
657
657 :param str_: given string
658 :param str_: given string
658 :param replace: char to find and replace multiple instances
659 :param replace: char to find and replace multiple instances
659
660
660 Examples::
661 Examples::
661 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
662 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
662 'Mighty-Mighty-Bo-sstones'
663 'Mighty-Mighty-Bo-sstones'
663 """
664 """
664
665
665 if str_.find(replace * 2) == -1:
666 if str_.find(replace * 2) == -1:
666 return str_
667 return str_
667 else:
668 else:
668 str_ = str_.replace(replace * 2, replace)
669 str_ = str_.replace(replace * 2, replace)
669 return recursive_replace(str_, replace)
670 return recursive_replace(str_, replace)
670
671
671
672
672 def repo_name_slug(value):
673 def repo_name_slug(value):
673 """
674 """
674 Return slug of name of repository
675 Return slug of name of repository
675 This function is called on each creation/modification
676 This function is called on each creation/modification
676 of repository to prevent bad names in repo
677 of repository to prevent bad names in repo
677 """
678 """
678
679
679 slug = remove_formatting(value)
680 slug = remove_formatting(value)
680 slug = strip_tags(slug)
681 slug = strip_tags(slug)
681
682
682 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
683 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
683 slug = slug.replace(c, '-')
684 slug = slug.replace(c, '-')
684 slug = recursive_replace(slug, '-')
685 slug = recursive_replace(slug, '-')
685 slug = collapse(slug, '-')
686 slug = collapse(slug, '-')
686 return slug
687 return slug
687
688
688
689
689 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
690 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
690 while True:
691 while True:
691 ok = raw_input(prompt)
692 ok = raw_input(prompt)
692 if ok in ('y', 'ye', 'yes'):
693 if ok in ('y', 'ye', 'yes'):
693 return True
694 return True
694 if ok in ('n', 'no', 'nop', 'nope'):
695 if ok in ('n', 'no', 'nop', 'nope'):
695 return False
696 return False
696 retries = retries - 1
697 retries = retries - 1
697 if retries < 0:
698 if retries < 0:
698 raise IOError
699 raise IOError
699 print complaint
700 print complaint
@@ -1,200 +1,201 b''
1 import datetime
1 import datetime
2 import posixpath
2 import posixpath
3 import stat
3 import stat
4 import time
4 import time
5
5
6 from dulwich import objects
6 from dulwich import objects
7
7
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
9 from kallithea.lib.vcs.exceptions import RepositoryError
9 from kallithea.lib.vcs.exceptions import RepositoryError
10 from kallithea.lib.vcs.utils import safe_str
10 from kallithea.lib.vcs.utils import safe_str
11
11
12
12
13 class GitInMemoryChangeset(BaseInMemoryChangeset):
13 class GitInMemoryChangeset(BaseInMemoryChangeset):
14
14
15 def commit(self, message, author, parents=None, branch=None, date=None,
15 def commit(self, message, author, parents=None, branch=None, date=None,
16 **kwargs):
16 **kwargs):
17 """
17 """
18 Performs in-memory commit (doesn't check workdir in any way) and
18 Performs in-memory commit (doesn't check workdir in any way) and
19 returns newly created ``Changeset``. Updates repository's
19 returns newly created ``Changeset``. Updates repository's
20 ``revisions``.
20 ``revisions``.
21
21
22 :param message: message of the commit
22 :param message: message of the commit
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
24 :param parents: single parent or sequence of parents from which commit
24 :param parents: single parent or sequence of parents from which commit
25 would be derived
25 would be derived
26 :param date: ``datetime.datetime`` instance. Defaults to
26 :param date: ``datetime.datetime`` instance. Defaults to
27 ``datetime.datetime.now()``.
27 ``datetime.datetime.now()``.
28 :param branch: branch name, as string. If none given, default backend's
28 :param branch: branch name, as string. If none given, default backend's
29 branch would be used.
29 branch would be used.
30
30
31 :raises ``CommitError``: if any error occurs while committing
31 :raises ``CommitError``: if any error occurs while committing
32 """
32 """
33 self.check_integrity(parents)
33 self.check_integrity(parents)
34
34
35 from .repository import GitRepository
35 from .repository import GitRepository
36 if branch is None:
36 if branch is None:
37 branch = GitRepository.DEFAULT_BRANCH_NAME
37 branch = GitRepository.DEFAULT_BRANCH_NAME
38
38
39 repo = self.repository._repo
39 repo = self.repository._repo
40 object_store = repo.object_store
40 object_store = repo.object_store
41
41
42 ENCODING = "UTF-8"
42 ENCODING = "UTF-8"
43
43
44 # Create tree and populates it with blobs
44 # Create tree and populates it with blobs
45 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
45 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
46 objects.Tree()
46 objects.Tree()
47 for node in self.added + self.changed:
47 for node in self.added + self.changed:
48 # Compute subdirs if needed
48 # Compute subdirs if needed
49 dirpath, nodename = posixpath.split(node.path)
49 dirpath, nodename = posixpath.split(node.path)
50 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
50 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
51 parent = commit_tree
51 parent = commit_tree
52 ancestors = [('', parent)]
52 ancestors = [('', parent)]
53
53
54 # Tries to dig for the deepest existing tree
54 # Tries to dig for the deepest existing tree
55 while dirnames:
55 while dirnames:
56 curdir = dirnames.pop(0)
56 curdir = dirnames.pop(0)
57 try:
57 try:
58 dir_id = parent[curdir][1]
58 dir_id = parent[curdir][1]
59 except KeyError:
59 except KeyError:
60 # put curdir back into dirnames and stops
60 # put curdir back into dirnames and stops
61 dirnames.insert(0, curdir)
61 dirnames.insert(0, curdir)
62 break
62 break
63 else:
63 else:
64 # If found, updates parent
64 # If found, updates parent
65 parent = self.repository._repo[dir_id]
65 parent = self.repository._repo[dir_id]
66 ancestors.append((curdir, parent))
66 ancestors.append((curdir, parent))
67 # Now parent is deepest existing tree and we need to create subtrees
67 # Now parent is deepest existing tree and we need to create subtrees
68 # for dirnames (in reverse order) [this only applies for nodes from added]
68 # for dirnames (in reverse order) [this only applies for nodes from added]
69 new_trees = []
69 new_trees = []
70
70
71 if not node.is_binary:
71 if not node.is_binary:
72 content = node.content.encode(ENCODING)
72 content = node.content.encode(ENCODING)
73 else:
73 else:
74 content = node.content
74 content = node.content
75 blob = objects.Blob.from_string(content)
75 blob = objects.Blob.from_string(content)
76
76
77 node_path = node.name.encode(ENCODING)
77 node_path = node.name.encode(ENCODING)
78 if dirnames:
78 if dirnames:
79 # If there are trees which should be created we need to build
79 # If there are trees which should be created we need to build
80 # them now (in reverse order)
80 # them now (in reverse order)
81 reversed_dirnames = list(reversed(dirnames))
81 reversed_dirnames = list(reversed(dirnames))
82 curtree = objects.Tree()
82 curtree = objects.Tree()
83 curtree[node_path] = node.mode, blob.id
83 curtree[node_path] = node.mode, blob.id
84 new_trees.append(curtree)
84 new_trees.append(curtree)
85 for dirname in reversed_dirnames[:-1]:
85 for dirname in reversed_dirnames[:-1]:
86 newtree = objects.Tree()
86 newtree = objects.Tree()
87 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
87 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
88 newtree[dirname] = stat.S_IFDIR, curtree.id
88 newtree[dirname] = stat.S_IFDIR, curtree.id
89 new_trees.append(newtree)
89 new_trees.append(newtree)
90 curtree = newtree
90 curtree = newtree
91 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
91 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
92 else:
92 else:
93 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
93 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
94
94
95 new_trees.append(parent)
95 new_trees.append(parent)
96 # Update ancestors
96 # Update ancestors
97 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
97 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
98 zip(ancestors, ancestors[1:])]):
98 zip(ancestors, ancestors[1:])]
99 ):
99 parent[path] = stat.S_IFDIR, tree.id
100 parent[path] = stat.S_IFDIR, tree.id
100 object_store.add_object(tree)
101 object_store.add_object(tree)
101
102
102 object_store.add_object(blob)
103 object_store.add_object(blob)
103 for tree in new_trees:
104 for tree in new_trees:
104 object_store.add_object(tree)
105 object_store.add_object(tree)
105 for node in self.removed:
106 for node in self.removed:
106 paths = node.path.split('/')
107 paths = node.path.split('/')
107 tree = commit_tree
108 tree = commit_tree
108 trees = [tree]
109 trees = [tree]
109 # Traverse deep into the forest...
110 # Traverse deep into the forest...
110 for path in paths:
111 for path in paths:
111 try:
112 try:
112 obj = self.repository._repo[tree[path][1]]
113 obj = self.repository._repo[tree[path][1]]
113 if isinstance(obj, objects.Tree):
114 if isinstance(obj, objects.Tree):
114 trees.append(obj)
115 trees.append(obj)
115 tree = obj
116 tree = obj
116 except KeyError:
117 except KeyError:
117 break
118 break
118 # Cut down the blob and all rotten trees on the way back...
119 # Cut down the blob and all rotten trees on the way back...
119 for path, tree in reversed(zip(paths, trees)):
120 for path, tree in reversed(zip(paths, trees)):
120 del tree[path]
121 del tree[path]
121 if tree:
122 if tree:
122 # This tree still has elements - don't remove it or any
123 # This tree still has elements - don't remove it or any
123 # of it's parents
124 # of it's parents
124 break
125 break
125
126
126 object_store.add_object(commit_tree)
127 object_store.add_object(commit_tree)
127
128
128 # Create commit
129 # Create commit
129 commit = objects.Commit()
130 commit = objects.Commit()
130 commit.tree = commit_tree.id
131 commit.tree = commit_tree.id
131 commit.parents = [p._commit.id for p in self.parents if p]
132 commit.parents = [p._commit.id for p in self.parents if p]
132 commit.author = commit.committer = safe_str(author)
133 commit.author = commit.committer = safe_str(author)
133 commit.encoding = ENCODING
134 commit.encoding = ENCODING
134 commit.message = safe_str(message)
135 commit.message = safe_str(message)
135
136
136 # Compute date
137 # Compute date
137 if date is None:
138 if date is None:
138 date = time.time()
139 date = time.time()
139 elif isinstance(date, datetime.datetime):
140 elif isinstance(date, datetime.datetime):
140 date = time.mktime(date.timetuple())
141 date = time.mktime(date.timetuple())
141
142
142 author_time = kwargs.pop('author_time', date)
143 author_time = kwargs.pop('author_time', date)
143 commit.commit_time = int(date)
144 commit.commit_time = int(date)
144 commit.author_time = int(author_time)
145 commit.author_time = int(author_time)
145 tz = time.timezone
146 tz = time.timezone
146 author_tz = kwargs.pop('author_timezone', tz)
147 author_tz = kwargs.pop('author_timezone', tz)
147 commit.commit_timezone = tz
148 commit.commit_timezone = tz
148 commit.author_timezone = author_tz
149 commit.author_timezone = author_tz
149
150
150 object_store.add_object(commit)
151 object_store.add_object(commit)
151
152
152 ref = 'refs/heads/%s' % branch
153 ref = 'refs/heads/%s' % branch
153 repo.refs[ref] = commit.id
154 repo.refs[ref] = commit.id
154
155
155 # Update vcs repository object & recreate dulwich repo
156 # Update vcs repository object & recreate dulwich repo
156 self.repository.revisions.append(commit.id)
157 self.repository.revisions.append(commit.id)
157 # invalidate parsed refs after commit
158 # invalidate parsed refs after commit
158 self.repository._parsed_refs = self.repository._get_parsed_refs()
159 self.repository._parsed_refs = self.repository._get_parsed_refs()
159 tip = self.repository.get_changeset()
160 tip = self.repository.get_changeset()
160 self.reset()
161 self.reset()
161 return tip
162 return tip
162
163
163 def _get_missing_trees(self, path, root_tree):
164 def _get_missing_trees(self, path, root_tree):
164 """
165 """
165 Creates missing ``Tree`` objects for the given path.
166 Creates missing ``Tree`` objects for the given path.
166
167
167 :param path: path given as a string. It may be a path to a file node
168 :param path: path given as a string. It may be a path to a file node
168 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
169 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
169 end with slash (i.e. ``foo/bar/``).
170 end with slash (i.e. ``foo/bar/``).
170 :param root_tree: ``dulwich.objects.Tree`` object from which we start
171 :param root_tree: ``dulwich.objects.Tree`` object from which we start
171 traversing (should be commit's root tree)
172 traversing (should be commit's root tree)
172 """
173 """
173 dirpath = posixpath.split(path)[0]
174 dirpath = posixpath.split(path)[0]
174 dirs = dirpath.split('/')
175 dirs = dirpath.split('/')
175 if not dirs or dirs == ['']:
176 if not dirs or dirs == ['']:
176 return []
177 return []
177
178
178 def get_tree_for_dir(tree, dirname):
179 def get_tree_for_dir(tree, dirname):
179 for name, mode, id in tree.iteritems():
180 for name, mode, id in tree.iteritems():
180 if name == dirname:
181 if name == dirname:
181 obj = self.repository._repo[id]
182 obj = self.repository._repo[id]
182 if isinstance(obj, objects.Tree):
183 if isinstance(obj, objects.Tree):
183 return obj
184 return obj
184 else:
185 else:
185 raise RepositoryError("Cannot create directory %s "
186 raise RepositoryError("Cannot create directory %s "
186 "at tree %s as path is occupied and is not a "
187 "at tree %s as path is occupied and is not a "
187 "Tree" % (dirname, tree))
188 "Tree" % (dirname, tree))
188 return None
189 return None
189
190
190 trees = []
191 trees = []
191 parent = root_tree
192 parent = root_tree
192 for dirname in dirs:
193 for dirname in dirs:
193 tree = get_tree_for_dir(parent, dirname)
194 tree = get_tree_for_dir(parent, dirname)
194 if tree is None:
195 if tree is None:
195 tree = objects.Tree()
196 tree = objects.Tree()
196 parent.add(stat.S_IFDIR, dirname, tree.id)
197 parent.add(stat.S_IFDIR, dirname, tree.id)
197 parent = tree
198 parent = tree
198 # Always append tree
199 # Always append tree
199 trees.append(tree)
200 trees.append(tree)
200 return trees
201 return trees
@@ -1,178 +1,179 b''
1 import StringIO
1 import StringIO
2
2
3 from pygments import highlight
3 from pygments import highlight
4 from pygments.formatters import HtmlFormatter
4 from pygments.formatters import HtmlFormatter
5
5
6 from kallithea.lib.vcs.exceptions import VCSError
6 from kallithea.lib.vcs.exceptions import VCSError
7 from kallithea.lib.vcs.nodes import FileNode
7 from kallithea.lib.vcs.nodes import FileNode
8
8
9
9
10 def annotate_highlight(filenode, annotate_from_changeset_func=None,
10 def annotate_highlight(filenode, annotate_from_changeset_func=None,
11 order=None, headers=None, **options):
11 order=None, headers=None, **options):
12 """
12 """
13 Returns html portion containing annotated table with 3 columns: line
13 Returns html portion containing annotated table with 3 columns: line
14 numbers, changeset information and pygmentized line of code.
14 numbers, changeset information and pygmentized line of code.
15
15
16 :param filenode: FileNode object
16 :param filenode: FileNode object
17 :param annotate_from_changeset_func: function taking changeset and
17 :param annotate_from_changeset_func: function taking changeset and
18 returning single annotate cell; needs break line at the end
18 returning single annotate cell; needs break line at the end
19 :param order: ordered sequence of ``ls`` (line numbers column),
19 :param order: ordered sequence of ``ls`` (line numbers column),
20 ``annotate`` (annotate column), ``code`` (code column); Default is
20 ``annotate`` (annotate column), ``code`` (code column); Default is
21 ``['ls', 'annotate', 'code']``
21 ``['ls', 'annotate', 'code']``
22 :param headers: dictionary with headers (keys are whats in ``order``
22 :param headers: dictionary with headers (keys are whats in ``order``
23 parameter)
23 parameter)
24 """
24 """
25 options['linenos'] = True
25 options['linenos'] = True
26 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
26 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
27 headers=headers,
27 headers=headers,
28 annotate_from_changeset_func=annotate_from_changeset_func, **options)
28 annotate_from_changeset_func=annotate_from_changeset_func, **options)
29 lexer = filenode.lexer
29 lexer = filenode.lexer
30 highlighted = highlight(filenode.content, lexer, formatter)
30 highlighted = highlight(filenode.content, lexer, formatter)
31 return highlighted
31 return highlighted
32
32
33
33
34 class AnnotateHtmlFormatter(HtmlFormatter):
34 class AnnotateHtmlFormatter(HtmlFormatter):
35
35
36 def __init__(self, filenode, annotate_from_changeset_func=None,
36 def __init__(self, filenode, annotate_from_changeset_func=None,
37 order=None, **options):
37 order=None, **options):
38 """
38 """
39 If ``annotate_from_changeset_func`` is passed it should be a function
39 If ``annotate_from_changeset_func`` is passed it should be a function
40 which returns string from the given changeset. For example, we may pass
40 which returns string from the given changeset. For example, we may pass
41 following function as ``annotate_from_changeset_func``::
41 following function as ``annotate_from_changeset_func``::
42
42
43 def changeset_to_anchor(changeset):
43 def changeset_to_anchor(changeset):
44 return '<a href="/changesets/%s/">%s</a>\n' % \
44 return '<a href="/changesets/%s/">%s</a>\n' % \
45 (changeset.id, changeset.id)
45 (changeset.id, changeset.id)
46
46
47 :param annotate_from_changeset_func: see above
47 :param annotate_from_changeset_func: see above
48 :param order: (default: ``['ls', 'annotate', 'code']``); order of
48 :param order: (default: ``['ls', 'annotate', 'code']``); order of
49 columns;
49 columns;
50 :param options: standard pygment's HtmlFormatter options, there is
50 :param options: standard pygment's HtmlFormatter options, there is
51 extra option tough, ``headers``. For instance we can pass::
51 extra option tough, ``headers``. For instance we can pass::
52
52
53 formatter = AnnotateHtmlFormatter(filenode, headers={
53 formatter = AnnotateHtmlFormatter(filenode, headers={
54 'ls': '#',
54 'ls': '#',
55 'annotate': 'Annotate',
55 'annotate': 'Annotate',
56 'code': 'Code',
56 'code': 'Code',
57 })
57 })
58
58
59 """
59 """
60 super(AnnotateHtmlFormatter, self).__init__(**options)
60 super(AnnotateHtmlFormatter, self).__init__(**options)
61 self.annotate_from_changeset_func = annotate_from_changeset_func
61 self.annotate_from_changeset_func = annotate_from_changeset_func
62 self.order = order or ('ls', 'annotate', 'code')
62 self.order = order or ('ls', 'annotate', 'code')
63 headers = options.pop('headers', None)
63 headers = options.pop('headers', None)
64 if headers and not ('ls' in headers and 'annotate' in headers and
64 if headers and not ('ls' in headers and 'annotate' in headers and
65 'code' in headers):
65 'code' in headers
66 ):
66 raise ValueError("If headers option dict is specified it must "
67 raise ValueError("If headers option dict is specified it must "
67 "all 'ls', 'annotate' and 'code' keys")
68 "all 'ls', 'annotate' and 'code' keys")
68 self.headers = headers
69 self.headers = headers
69 if isinstance(filenode, FileNode):
70 if isinstance(filenode, FileNode):
70 self.filenode = filenode
71 self.filenode = filenode
71 else:
72 else:
72 raise VCSError("This formatter expect FileNode parameter, not %r"
73 raise VCSError("This formatter expect FileNode parameter, not %r"
73 % type(filenode))
74 % type(filenode))
74
75
75 def annotate_from_changeset(self, changeset):
76 def annotate_from_changeset(self, changeset):
76 """
77 """
77 Returns full html line for single changeset per annotated line.
78 Returns full html line for single changeset per annotated line.
78 """
79 """
79 if self.annotate_from_changeset_func:
80 if self.annotate_from_changeset_func:
80 return self.annotate_from_changeset_func(changeset)
81 return self.annotate_from_changeset_func(changeset)
81 else:
82 else:
82 return ''.join((changeset.id, '\n'))
83 return ''.join((changeset.id, '\n'))
83
84
84 def _wrap_tablelinenos(self, inner):
85 def _wrap_tablelinenos(self, inner):
85 dummyoutfile = StringIO.StringIO()
86 dummyoutfile = StringIO.StringIO()
86 lncount = 0
87 lncount = 0
87 for t, line in inner:
88 for t, line in inner:
88 if t:
89 if t:
89 lncount += 1
90 lncount += 1
90 dummyoutfile.write(line)
91 dummyoutfile.write(line)
91
92
92 fl = self.linenostart
93 fl = self.linenostart
93 mw = len(str(lncount + fl - 1))
94 mw = len(str(lncount + fl - 1))
94 sp = self.linenospecial
95 sp = self.linenospecial
95 st = self.linenostep
96 st = self.linenostep
96 la = self.lineanchors
97 la = self.lineanchors
97 aln = self.anchorlinenos
98 aln = self.anchorlinenos
98 if sp:
99 if sp:
99 lines = []
100 lines = []
100
101
101 for i in range(fl, fl + lncount):
102 for i in range(fl, fl + lncount):
102 if i % st == 0:
103 if i % st == 0:
103 if i % sp == 0:
104 if i % sp == 0:
104 if aln:
105 if aln:
105 lines.append('<a href="#%s-%d" class="special">'
106 lines.append('<a href="#%s-%d" class="special">'
106 '%*d</a>' %
107 '%*d</a>' %
107 (la, i, mw, i))
108 (la, i, mw, i))
108 else:
109 else:
109 lines.append('<span class="special">'
110 lines.append('<span class="special">'
110 '%*d</span>' % (mw, i))
111 '%*d</span>' % (mw, i))
111 else:
112 else:
112 if aln:
113 if aln:
113 lines.append('<a href="#%s-%d">'
114 lines.append('<a href="#%s-%d">'
114 '%*d</a>' % (la, i, mw, i))
115 '%*d</a>' % (la, i, mw, i))
115 else:
116 else:
116 lines.append('%*d' % (mw, i))
117 lines.append('%*d' % (mw, i))
117 else:
118 else:
118 lines.append('')
119 lines.append('')
119 ls = '\n'.join(lines)
120 ls = '\n'.join(lines)
120 else:
121 else:
121 lines = []
122 lines = []
122 for i in range(fl, fl + lncount):
123 for i in range(fl, fl + lncount):
123 if i % st == 0:
124 if i % st == 0:
124 if aln:
125 if aln:
125 lines.append('<a href="#%s-%d">%*d</a>'
126 lines.append('<a href="#%s-%d">%*d</a>'
126 % (la, i, mw, i))
127 % (la, i, mw, i))
127 else:
128 else:
128 lines.append('%*d' % (mw, i))
129 lines.append('%*d' % (mw, i))
129 else:
130 else:
130 lines.append('')
131 lines.append('')
131 ls = '\n'.join(lines)
132 ls = '\n'.join(lines)
132
133
133 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
134 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
134 # If pygments cropped last lines break we need do that too
135 # If pygments cropped last lines break we need do that too
135 ln_cs = len(annotate_changesets)
136 ln_cs = len(annotate_changesets)
136 ln_ = len(ls.splitlines())
137 ln_ = len(ls.splitlines())
137 if ln_cs > ln_:
138 if ln_cs > ln_:
138 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
139 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
139 annotate = ''.join((self.annotate_from_changeset(changeset)
140 annotate = ''.join((self.annotate_from_changeset(changeset)
140 for changeset in annotate_changesets))
141 for changeset in annotate_changesets))
141 # in case you wonder about the seemingly redundant <div> here:
142 # in case you wonder about the seemingly redundant <div> here:
142 # since the content in the other cell also is wrapped in a div,
143 # since the content in the other cell also is wrapped in a div,
143 # some browsers in some configurations seem to mess up the formatting.
144 # some browsers in some configurations seem to mess up the formatting.
144 '''
145 '''
145 yield 0, ('<table class="%stable">' % self.cssclass +
146 yield 0, ('<table class="%stable">' % self.cssclass +
146 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
147 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
147 ls + '</pre></div></td>' +
148 ls + '</pre></div></td>' +
148 '<td class="code">')
149 '<td class="code">')
149 yield 0, dummyoutfile.getvalue()
150 yield 0, dummyoutfile.getvalue()
150 yield 0, '</td></tr></table>'
151 yield 0, '</td></tr></table>'
151
152
152 '''
153 '''
153 headers_row = []
154 headers_row = []
154 if self.headers:
155 if self.headers:
155 headers_row = ['<tr class="annotate-header">']
156 headers_row = ['<tr class="annotate-header">']
156 for key in self.order:
157 for key in self.order:
157 td = ''.join(('<td>', self.headers[key], '</td>'))
158 td = ''.join(('<td>', self.headers[key], '</td>'))
158 headers_row.append(td)
159 headers_row.append(td)
159 headers_row.append('</tr>')
160 headers_row.append('</tr>')
160
161
161 body_row_start = ['<tr>']
162 body_row_start = ['<tr>']
162 for key in self.order:
163 for key in self.order:
163 if key == 'ls':
164 if key == 'ls':
164 body_row_start.append(
165 body_row_start.append(
165 '<td class="linenos"><div class="linenodiv"><pre>' +
166 '<td class="linenos"><div class="linenodiv"><pre>' +
166 ls + '</pre></div></td>')
167 ls + '</pre></div></td>')
167 elif key == 'annotate':
168 elif key == 'annotate':
168 body_row_start.append(
169 body_row_start.append(
169 '<td class="annotate"><div class="annotatediv"><pre>' +
170 '<td class="annotate"><div class="annotatediv"><pre>' +
170 annotate + '</pre></div></td>')
171 annotate + '</pre></div></td>')
171 elif key == 'code':
172 elif key == 'code':
172 body_row_start.append('<td class="code">')
173 body_row_start.append('<td class="code">')
173 yield 0, ('<table class="%stable">' % self.cssclass +
174 yield 0, ('<table class="%stable">' % self.cssclass +
174 ''.join(headers_row) +
175 ''.join(headers_row) +
175 ''.join(body_row_start)
176 ''.join(body_row_start)
176 )
177 )
177 yield 0, dummyoutfile.getvalue()
178 yield 0, dummyoutfile.getvalue()
178 yield 0, '</td></tr></table>'
179 yield 0, '</td></tr></table>'
General Comments 0
You need to be logged in to leave comments. Login now