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