##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4528:5055a30b merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,54 b''
1 |RCE| 4.21.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-09-28
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Pull requests: overhaul of the UX/UI by adding new sidebar
14 - Pull requests: new live reviewer present indicator (requires channelstream enabled)
15 - Pull requests: new live new comments indicator (requires channelstream enabled)
16 - Pull requests: new sidebar with comments/todos/referenced tickets navigation
17 - Commits page: Introduced sidebar for single commits pages
18
19
20 General
21 ^^^^^^^
22
23 - API: allow repo admins to get/set settings.
24 Previously it was only super-admins that could do that.
25 - Sessions: patch baker to take expire time for redis for auto session cleanup feature.
26 - Git: bumped git version to 2.27.0
27 - Packages: bumped to channelstream==0.6.14
28
29
30 Security
31 ^^^^^^^^
32
33 - Issue trackers: fix XSS with description field.
34
35
36 Performance
37 ^^^^^^^^^^^
38
39 - Artifacts: speed-up of artifacts download request processing.
40
41
42 Fixes
43 ^^^^^
44
45 - Pull requests: properly save merge failure metadata.
46 In rare cases merge check reported conflicts which there were none.
47 - Sessions: fixed cleanup with corrupted session data issue.
48
49
50 Upgrade notes
51 ^^^^^^^^^^^^^
52
53 - Scheduled feature release.
54 - Git version was bumped to 2.27.0
@@ -0,0 +1,55 b''
1 |RCE| 4.22.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-10-12
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Reviewers: added observers as another role for reviewers.
14 Observers is a role that doesn't require voting, but still gets notified about
15 PR and should participate in review process.
16 - Issue trackers: implemented more sophisticated ticket data extraction based on
17 advanced regex module. This allows using ticket references without false positives
18 like catching ticket data in an URL.
19 - Channelstream: Notification about updates and comments now works via API, and both
20 Pull-requests and individual commits.
21
22
23 General
24 ^^^^^^^
25
26 - Data tables: unified tables look for main pages of rhodecode repo pages.
27 - Users: autocomplete now sorts by matched username to show best matches first.
28 - Pull requests: only allow actual reviewers to leave status/votes in order to not
29 confuse others users about voting from people who aren't actual reviewers.
30
31 Security
32 ^^^^^^^^
33
34
35
36 Performance
37 ^^^^^^^^^^^
38
39 - Default reviewers: optimize diff data, and creation of PR with advanced default reviewers
40 - default-reviewers: diff data should load more things lazy for better performance.
41 - Pull requests: limit the amount of data saved in default reviewers data for better memory usage
42 - DB: don't use lazy loaders on PR related objects, to optimize memory usage on large
43 Pull requests with lots of comments, and commits.
44
45 Fixes
46 ^^^^^
47
48 - Quick search bar: fixes #5634, crash when search on non-ascii characters.
49 - Sidebar: few fixes for panel rendering of reviewers/observers for both commits and PRS.
50
51 Upgrade notes
52 ^^^^^^^^^^^^^
53
54 - Scheduled feature release.
55
@@ -0,0 +1,68 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRuleUser.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('role', Unicode(255), nullable=True)
32 batch_op.add_column(new_column)
33
34 _fill_rule_user_role(op, meta.Session)
35
36 table = db.RepoReviewRuleUserGroup.__table__
37 with op.batch_alter_table(table.name) as batch_op:
38 new_column = Column('role', Unicode(255), nullable=True)
39 batch_op.add_column(new_column)
40
41 _fill_rule_user_group_role(op, meta.Session)
42
43
44 def downgrade(migrate_engine):
45 meta = MetaData()
46 meta.bind = migrate_engine
47
48
49 def fixups(models, _SESSION):
50 pass
51
52
53 def _fill_rule_user_role(op, session):
54 params = {'role': 'reviewer'}
55 query = text(
56 'UPDATE repo_review_rules_users SET role = :role'
57 ).bindparams(**params)
58 op.execute(query)
59 session().commit()
60
61
62 def _fill_rule_user_group_role(op, session):
63 params = {'role': 'reviewer'}
64 query = text(
65 'UPDATE repo_review_rules_users_groups SET role = :role'
66 ).bindparams(**params)
67 op.execute(query)
68 session().commit()
@@ -0,0 +1,35 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def html(info):
23 """
24 Custom string as html content_type renderer for pyramid
25 """
26 def _render(value, system):
27 request = system.get('request')
28 if request is not None:
29 response = request.response
30 ct = response.content_type
31 if ct == response.default_content_type:
32 response.content_type = 'text/html'
33 return value
34
35 return _render
@@ -68,3 +68,5 b' 7ac623a4a2405917e2af660d645ded662011e40d'
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
71 8bb5fece08ab65986225b184e46f53d2a71729cb v4.21.0
72 90734aac31ee4563bbe665a43ff73190cc762275 v4.22.0
@@ -90,7 +90,7 b' comment_pull_request'
90 create_pull_request
90 create_pull_request
91 -------------------
91 -------------------
92
92
93 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, owner=<Optional:<OptionalAttr:apiuser>>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>)
93 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, owner=<Optional:<OptionalAttr:apiuser>>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, observers=<Optional:None>)
94
94
95 Creates a new pull request.
95 Creates a new pull request.
96
96
@@ -128,6 +128,13 b' create_pull_request'
128 Accepts username strings or objects of the format:
128 Accepts username strings or objects of the format:
129
129
130 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
130 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
131 :param observers: Set the new pull request observers list.
132 Reviewer defined by review rules will be added automatically to the
133 defined list. This feature is only available in RhodeCode EE
134 :type observers: Optional(list)
135 Accepts username strings or objects of the format:
136
137 [{'username': 'nick', 'reasons': ['original author']}]
131
138
132
139
133 get_pull_request
140 get_pull_request
@@ -392,7 +399,7 b' merge_pull_request'
392 update_pull_request
399 update_pull_request
393 -------------------
400 -------------------
394
401
395 .. py:function:: update_pull_request(apiuser, pullrequestid, repoid=<Optional:None>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>)
402 .. py:function:: update_pull_request(apiuser, pullrequestid, repoid=<Optional:None>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, observers=<Optional:None>, update_commits=<Optional:None>)
396
403
397 Updates a pull request.
404 Updates a pull request.
398
405
@@ -414,7 +421,11 b' update_pull_request'
414 Accepts username strings or objects of the format:
421 Accepts username strings or objects of the format:
415
422
416 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
423 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
424 :param observers: Update pull request observers list with new value.
425 :type observers: Optional(list)
426 Accepts username strings or objects of the format:
417
427
428 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
418 :param update_commits: Trigger update of commits for this pull request
429 :param update_commits: Trigger update of commits for this pull request
419 :type: update_commits: Optional(bool)
430 :type: update_commits: Optional(bool)
420
431
@@ -432,6 +443,12 b' update_pull_request'
432 ],
443 ],
433 "removed": []
444 "removed": []
434 },
445 },
446 "updated_observers": {
447 "added": [
448 "username"
449 ],
450 "removed": []
451 },
435 "updated_commits": {
452 "updated_commits": {
436 "added": [
453 "added": [
437 "<sha1_hash>"
454 "<sha1_hash>"
@@ -9,6 +9,8 b' Release Notes'
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.22.0.rst
13 release-notes-4.21.0.rst
12 release-notes-4.20.1.rst
14 release-notes-4.20.1.rst
13 release-notes-4.20.0.rst
15 release-notes-4.20.0.rst
14 release-notes-4.19.3.rst
16 release-notes-4.19.3.rst
@@ -1816,6 +1816,17 b' self: super: {'
1816 license = [ pkgs.lib.licenses.mit ];
1816 license = [ pkgs.lib.licenses.mit ];
1817 };
1817 };
1818 };
1818 };
1819 "regex" = super.buildPythonPackage {
1820 name = "regex-2020.9.27";
1821 doCheck = false;
1822 src = fetchurl {
1823 url = "https://files.pythonhosted.org/packages/93/8c/17f45cdfb39b13d4b5f909e4b4c2917abcbdef9c0036919a0399769148cf/regex-2020.9.27.tar.gz";
1824 sha256 = "179ngfzwbsjvn5vhyzdahvmg0f7acahkwwy9bpjy1pv08bm2mwx6";
1825 };
1826 meta = {
1827 license = [ pkgs.lib.licenses.psfl ];
1828 };
1829 };
1819 "redis" = super.buildPythonPackage {
1830 "redis" = super.buildPythonPackage {
1820 name = "redis-3.4.1";
1831 name = "redis-3.4.1";
1821 doCheck = false;
1832 doCheck = false;
@@ -1872,7 +1883,7 b' self: super: {'
1872 };
1883 };
1873 };
1884 };
1874 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1885 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1875 name = "rhodecode-enterprise-ce-4.20.0";
1886 name = "rhodecode-enterprise-ce-4.22.0";
1876 buildInputs = [
1887 buildInputs = [
1877 self."pytest"
1888 self."pytest"
1878 self."py"
1889 self."py"
@@ -1946,6 +1957,7 b' self: super: {'
1946 self."tzlocal"
1957 self."tzlocal"
1947 self."pyzmq"
1958 self."pyzmq"
1948 self."py-gfm"
1959 self."py-gfm"
1960 self."regex"
1949 self."redis"
1961 self."redis"
1950 self."repoze.lru"
1962 self."repoze.lru"
1951 self."requests"
1963 self."requests"
@@ -56,6 +56,7 b' pytz==2019.3'
56 tzlocal==1.5.1
56 tzlocal==1.5.1
57 pyzmq==14.6.0
57 pyzmq==14.6.0
58 py-gfm==0.1.4
58 py-gfm==0.1.4
59 regex==2020.9.27
59 redis==3.4.1
60 redis==3.4.1
60 repoze.lru==0.7
61 repoze.lru==0.7
61 requests==2.22.0
62 requests==2.22.0
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 109 # defines current db version for migrations
51 __dbversion__ = 110 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
@@ -320,7 +320,7 b' class TestCreatePullRequestApi(object):'
320 id_, params = build_data(
320 id_, params = build_data(
321 self.apikey_regular, 'create_pull_request', **data)
321 self.apikey_regular, 'create_pull_request', **data)
322 response = api_call(self.app, params)
322 response = api_call(self.app, params)
323 expected_message = 'no commits found'
323 expected_message = 'no commits found for merge between specified references'
324 assert_error(id_, expected_message, given=response.body)
324 assert_error(id_, expected_message, given=response.body)
325
325
326 @pytest.mark.backends("git", "hg")
326 @pytest.mark.backends("git", "hg")
@@ -29,6 +29,7 b' from rhodecode.api.tests.utils import ('
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestGetPullRequest(object):
31 class TestGetPullRequest(object):
32
32 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
33 def test_api_get_pull_requests(self, pr_util):
34 def test_api_get_pull_requests(self, pr_util):
34 pull_request = pr_util.create_pull_request()
35 pull_request = pr_util.create_pull_request()
@@ -40,6 +41,7 b' class TestGetPullRequest(object):'
40 target_ref=pull_request.target_ref,
41 target_ref=pull_request.target_ref,
41 revisions=pull_request.revisions,
42 revisions=pull_request.revisions,
42 reviewers=(),
43 reviewers=(),
44 observers=(),
43 title=pull_request.title,
45 title=pull_request.title,
44 description=pull_request.description,
46 description=pull_request.description,
45 )
47 )
@@ -51,6 +51,7 b' class TestUpdatePullRequest(object):'
51 "pull_request": response.json['result']['pull_request'],
51 "pull_request": response.json['result']['pull_request'],
52 "updated_commits": {"added": [], "common": [], "removed": []},
52 "updated_commits": {"added": [], "common": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
53 "updated_reviewers": {"added": [], "removed": []},
54 "updated_observers": {"added": [], "removed": []},
54 }
55 }
55
56
56 response_json = response.json['result']
57 response_json = response.json['result']
@@ -111,6 +112,7 b' class TestUpdatePullRequest(object):'
111 "total": total_commits,
112 "total": total_commits,
112 "removed": []},
113 "removed": []},
113 "updated_reviewers": {"added": [], "removed": []},
114 "updated_reviewers": {"added": [], "removed": []},
115 "updated_observers": {"added": [], "removed": []},
114 }
116 }
115
117
116 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
@@ -132,7 +134,7 b' class TestUpdatePullRequest(object):'
132 removed = [a.username]
134 removed = [a.username]
133
135
134 pull_request = pr_util.create_pull_request(
136 pull_request = pr_util.create_pull_request(
135 reviewers=[(a.username, ['added via API'], False, [])])
137 reviewers=[(a.username, ['added via API'], False, 'reviewer', [])])
136
138
137 id_, params = build_data(
139 id_, params = build_data(
138 self.apikey, 'update_pull_request',
140 self.apikey, 'update_pull_request',
@@ -146,6 +148,7 b' class TestUpdatePullRequest(object):'
146 "pull_request": response.json['result']['pull_request'],
148 "pull_request": response.json['result']['pull_request'],
147 "updated_commits": {"added": [], "common": [], "removed": []},
149 "updated_commits": {"added": [], "common": [], "removed": []},
148 "updated_reviewers": {"added": added, "removed": removed},
150 "updated_reviewers": {"added": added, "removed": removed},
151 "updated_observers": {"added": [], "removed": []},
149 }
152 }
150
153
151 assert_ok(id_, expected, response.body)
154 assert_ok(id_, expected, response.body)
@@ -26,12 +26,15 b' from rhodecode.api.utils import ('
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.lib.vcs.backends.base import unicode_to_reference
32 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.db import (
37 Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers)
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
39 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.validation_schema import Invalid
40 from rhodecode.model.validation_schema import Invalid
@@ -502,16 +505,19 b' def comment_pull_request('
502 },
505 },
503 error : null
506 error : null
504 """
507 """
508 _ = request.translate
509
505 pull_request = get_pull_request_or_error(pullrequestid)
510 pull_request = get_pull_request_or_error(pullrequestid)
506 if Optional.extract(repoid):
511 if Optional.extract(repoid):
507 repo = get_repo_or_error(repoid)
512 repo = get_repo_or_error(repoid)
508 else:
513 else:
509 repo = pull_request.target_repo
514 repo = pull_request.target_repo
510
515
516 db_repo_name = repo.repo_name
511 auth_user = apiuser
517 auth_user = apiuser
512 if not isinstance(userid, Optional):
518 if not isinstance(userid, Optional):
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
519 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
520 user=apiuser, repo_name=db_repo_name)
515 if has_superadmin_permission(apiuser) or is_repo_admin:
521 if has_superadmin_permission(apiuser) or is_repo_admin:
516 apiuser = get_user_or_error(userid)
522 apiuser = get_user_or_error(userid)
517 auth_user = apiuser.AuthUser()
523 auth_user = apiuser.AuthUser()
@@ -596,6 +602,7 b' def comment_pull_request('
596 extra_recipients=extra_recipients,
602 extra_recipients=extra_recipients,
597 send_email=send_email
603 send_email=send_email
598 )
604 )
605 is_inline = comment.is_inline
599
606
600 if allowed_to_change_status and status:
607 if allowed_to_change_status and status:
601 old_calculated_status = pull_request.calculated_review_status()
608 old_calculated_status = pull_request.calculated_review_status()
@@ -628,14 +635,39 b' def comment_pull_request('
628 'comment_id': comment.comment_id if comment else None,
635 'comment_id': comment.comment_id if comment else None,
629 'status': {'given': status, 'was_changed': status_change},
636 'status': {'given': status, 'was_changed': status_change},
630 }
637 }
638
639 comment_broadcast_channel = channelstream.comment_channel(
640 db_repo_name, pull_request_obj=pull_request)
641
642 comment_data = data
643 comment_type = 'inline' if is_inline else 'general'
644 channelstream.comment_channelstream_push(
645 request, comment_broadcast_channel, apiuser,
646 _('posted a new {} comment').format(comment_type),
647 comment_data=comment_data)
648
631 return data
649 return data
632
650
651 def _reviewers_validation(obj_list):
652 schema = ReviewerListSchema()
653 try:
654 reviewer_objects = schema.deserialize(obj_list)
655 except Invalid as err:
656 raise JSONRPCValidationError(colander_exc=err)
657
658 # validate users
659 for reviewer_object in reviewer_objects:
660 user = get_user_or_error(reviewer_object['username'])
661 reviewer_object['user_id'] = user.user_id
662 return reviewer_objects
663
633
664
634 @jsonrpc_method()
665 @jsonrpc_method()
635 def create_pull_request(
666 def create_pull_request(
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
667 request, apiuser, source_repo, target_repo, source_ref, target_ref,
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
668 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
638 description_renderer=Optional(''), reviewers=Optional(None)):
669 description_renderer=Optional(''),
670 reviewers=Optional(None), observers=Optional(None)):
639 """
671 """
640 Creates a new pull request.
672 Creates a new pull request.
641
673
@@ -673,6 +705,13 b' def create_pull_request('
673 Accepts username strings or objects of the format:
705 Accepts username strings or objects of the format:
674
706
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
707 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
708 :param observers: Set the new pull request observers list.
709 Reviewer defined by review rules will be added automatically to the
710 defined list. This feature is only available in RhodeCode EE
711 :type observers: Optional(list)
712 Accepts username strings or objects of the format:
713
714 [{'username': 'nick', 'reasons': ['original author']}]
676 """
715 """
677
716
678 source_db_repo = get_repo_or_error(source_repo)
717 source_db_repo = get_repo_or_error(source_repo)
@@ -686,34 +725,39 b' def create_pull_request('
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
725 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
726 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
688
727
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
728 get_commit_or_error(full_source_ref, source_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
729 get_commit_or_error(full_target_ref, target_db_repo)
691
730
692 reviewer_objects = Optional.extract(reviewers) or []
731 reviewer_objects = Optional.extract(reviewers) or []
732 observer_objects = Optional.extract(observers) or []
693
733
694 # serialize and validate passed in given reviewers
734 # serialize and validate passed in given reviewers
695 if reviewer_objects:
735 if reviewer_objects:
696 schema = ReviewerListSchema()
736 reviewer_objects = _reviewers_validation(reviewer_objects)
697 try:
737
698 reviewer_objects = schema.deserialize(reviewer_objects)
738 if observer_objects:
699 except Invalid as err:
739 observer_objects = _reviewers_validation(reviewer_objects)
700 raise JSONRPCValidationError(colander_exc=err)
701
740
702 # validate users
741 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
703 for reviewer_object in reviewer_objects:
742 PullRequestModel().get_reviewer_functions()
704 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
706
743
707 get_default_reviewers_data, validate_default_reviewers = \
744 source_ref_obj = unicode_to_reference(full_source_ref)
708 PullRequestModel().get_reviewer_functions()
745 target_ref_obj = unicode_to_reference(full_target_ref)
709
746
710 # recalculate reviewers logic, to make sure we can validate this
747 # recalculate reviewers logic, to make sure we can validate this
711 default_reviewers_data = get_default_reviewers_data(
748 default_reviewers_data = get_default_reviewers_data(
712 owner, source_db_repo,
749 owner,
713 source_commit, target_db_repo, target_commit)
750 source_db_repo,
751 source_ref_obj,
752 target_db_repo,
753 target_ref_obj,
754 )
714
755
715 # now MERGE our given with the calculated
756 # now MERGE our given with the calculated from the default rules
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
757 just_reviewers = [
758 x for x in default_reviewers_data['reviewers']
759 if x['role'] == PullRequestReviewers.ROLE_REVIEWER]
760 reviewer_objects = just_reviewers + reviewer_objects
717
761
718 try:
762 try:
719 reviewers = validate_default_reviewers(
763 reviewers = validate_default_reviewers(
@@ -721,9 +765,21 b' def create_pull_request('
721 except ValueError as e:
765 except ValueError as e:
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
766 raise JSONRPCError('Reviewers Validation: {}'.format(e))
723
767
768 # now MERGE our given with the calculated from the default rules
769 just_observers = [
770 x for x in default_reviewers_data['reviewers']
771 if x['role'] == PullRequestReviewers.ROLE_OBSERVER]
772 observer_objects = just_observers + observer_objects
773
774 try:
775 observers = validate_observers(
776 observer_objects, default_reviewers_data)
777 except ValueError as e:
778 raise JSONRPCError('Observer Validation: {}'.format(e))
779
724 title = Optional.extract(title)
780 title = Optional.extract(title)
725 if not title:
781 if not title:
726 title_source_ref = source_ref.split(':', 2)[1]
782 title_source_ref = source_ref_obj.name
727 title = PullRequestModel().generate_pullrequest_title(
783 title = PullRequestModel().generate_pullrequest_title(
728 source=source_repo,
784 source=source_repo,
729 source_ref=title_source_ref,
785 source_ref=title_source_ref,
@@ -732,20 +788,17 b' def create_pull_request('
732
788
733 diff_info = default_reviewers_data['diff_info']
789 diff_info = default_reviewers_data['diff_info']
734 common_ancestor_id = diff_info['ancestor']
790 common_ancestor_id = diff_info['ancestor']
735 commits = diff_info['commits']
791 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
792 commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])]
736
793
737 if not common_ancestor_id:
794 if not common_ancestor_id:
738 raise JSONRPCError('no common ancestor found')
795 raise JSONRPCError('no common ancestor found between specified references')
739
796
740 if not commits:
797 if not commits:
741 raise JSONRPCError('no commits found')
798 raise JSONRPCError('no commits found for merge between specified references')
742
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
744 revisions = [commit.raw_id for commit in reversed(commits)]
745
799
746 # recalculate target ref based on ancestor
800 # recalculate target ref based on ancestor
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
801 full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id))
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
749
802
750 # fetch renderer, if set fallback to plain in case of PR
803 # fetch renderer, if set fallback to plain in case of PR
751 rc_config = SettingsModel().get_all_settings()
804 rc_config = SettingsModel().get_all_settings()
@@ -760,8 +813,9 b' def create_pull_request('
760 target_repo=target_repo,
813 target_repo=target_repo,
761 target_ref=full_target_ref,
814 target_ref=full_target_ref,
762 common_ancestor_id=common_ancestor_id,
815 common_ancestor_id=common_ancestor_id,
763 revisions=revisions,
816 revisions=commits,
764 reviewers=reviewers,
817 reviewers=reviewers,
818 observers=observers,
765 title=title,
819 title=title,
766 description=description,
820 description=description,
767 description_renderer=description_renderer,
821 description_renderer=description_renderer,
@@ -781,7 +835,7 b' def create_pull_request('
781 def update_pull_request(
835 def update_pull_request(
782 request, apiuser, pullrequestid, repoid=Optional(None),
836 request, apiuser, pullrequestid, repoid=Optional(None),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
837 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
784 reviewers=Optional(None), update_commits=Optional(None)):
838 reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)):
785 """
839 """
786 Updates a pull request.
840 Updates a pull request.
787
841
@@ -803,7 +857,11 b' def update_pull_request('
803 Accepts username strings or objects of the format:
857 Accepts username strings or objects of the format:
804
858
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
859 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
860 :param observers: Update pull request observers list with new value.
861 :type observers: Optional(list)
862 Accepts username strings or objects of the format:
806
863
864 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
807 :param update_commits: Trigger update of commits for this pull request
865 :param update_commits: Trigger update of commits for this pull request
808 :type: update_commits: Optional(bool)
866 :type: update_commits: Optional(bool)
809
867
@@ -821,6 +879,12 b' def update_pull_request('
821 ],
879 ],
822 "removed": []
880 "removed": []
823 },
881 },
882 "updated_observers": {
883 "added": [
884 "username"
885 ],
886 "removed": []
887 },
824 "updated_commits": {
888 "updated_commits": {
825 "added": [
889 "added": [
826 "<sha1_hash>"
890 "<sha1_hash>"
@@ -852,36 +916,14 b' def update_pull_request('
852 pullrequestid,))
916 pullrequestid,))
853
917
854 reviewer_objects = Optional.extract(reviewers) or []
918 reviewer_objects = Optional.extract(reviewers) or []
855
919 observer_objects = Optional.extract(observers) or []
856 if reviewer_objects:
857 schema = ReviewerListSchema()
858 try:
859 reviewer_objects = schema.deserialize(reviewer_objects)
860 except Invalid as err:
861 raise JSONRPCValidationError(colander_exc=err)
862
863 # validate users
864 for reviewer_object in reviewer_objects:
865 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
867
868 get_default_reviewers_data, get_validated_reviewers = \
869 PullRequestModel().get_reviewer_functions()
870
871 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
873 try:
874 reviewers = get_validated_reviewers(
875 reviewer_objects, reviewer_rules)
876 except ValueError as e:
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 else:
879 reviewers = []
880
920
881 title = Optional.extract(title)
921 title = Optional.extract(title)
882 description = Optional.extract(description)
922 description = Optional.extract(description)
883 description_renderer = Optional.extract(description_renderer)
923 description_renderer = Optional.extract(description_renderer)
884
924
925 # Update title/description
926 title_changed = False
885 if title or description:
927 if title or description:
886 PullRequestModel().edit(
928 PullRequestModel().edit(
887 pull_request,
929 pull_request,
@@ -890,8 +932,12 b' def update_pull_request('
890 description_renderer or pull_request.description_renderer,
932 description_renderer or pull_request.description_renderer,
891 apiuser)
933 apiuser)
892 Session().commit()
934 Session().commit()
935 title_changed = True
893
936
894 commit_changes = {"added": [], "common": [], "removed": []}
937 commit_changes = {"added": [], "common": [], "removed": []}
938
939 # Update commits
940 commits_changed = False
895 if str2bool(Optional.extract(update_commits)):
941 if str2bool(Optional.extract(update_commits)):
896
942
897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
943 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
@@ -907,12 +953,44 b' def update_pull_request('
907 pull_request, db_user)
953 pull_request, db_user)
908 commit_changes = update_response.changes or commit_changes
954 commit_changes = update_response.changes or commit_changes
909 Session().commit()
955 Session().commit()
956 commits_changed = True
910
957
958 # Update reviewers
959 # serialize and validate passed in given reviewers
960 if reviewer_objects:
961 reviewer_objects = _reviewers_validation(reviewer_objects)
962
963 if observer_objects:
964 observer_objects = _reviewers_validation(reviewer_objects)
965
966 # re-use stored rules
967 default_reviewers_data = pull_request.reviewer_data
968
969 __, validate_default_reviewers, validate_observers = \
970 PullRequestModel().get_reviewer_functions()
971
972 if reviewer_objects:
973 try:
974 reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
975 except ValueError as e:
976 raise JSONRPCError('Reviewers Validation: {}'.format(e))
977 else:
978 reviewers = []
979
980 if observer_objects:
981 try:
982 observers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
983 except ValueError as e:
984 raise JSONRPCError('Observer Validation: {}'.format(e))
985 else:
986 observers = []
987
988 reviewers_changed = False
911 reviewers_changes = {"added": [], "removed": []}
989 reviewers_changes = {"added": [], "removed": []}
912 if reviewers:
990 if reviewers:
913 old_calculated_status = pull_request.calculated_review_status()
991 old_calculated_status = pull_request.calculated_review_status()
914 added_reviewers, removed_reviewers = \
992 added_reviewers, removed_reviewers = \
915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
993 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance())
916
994
917 reviewers_changes['added'] = sorted(
995 reviewers_changes['added'] = sorted(
918 [get_user_or_error(n).username for n in added_reviewers])
996 [get_user_or_error(n).username for n in added_reviewers])
@@ -926,13 +1004,35 b' def update_pull_request('
926 PullRequestModel().trigger_pull_request_hook(
1004 PullRequestModel().trigger_pull_request_hook(
927 pull_request, apiuser, 'review_status_change',
1005 pull_request, apiuser, 'review_status_change',
928 data={'status': calculated_status})
1006 data={'status': calculated_status})
1007 reviewers_changed = True
1008
1009 observers_changed = False
1010 observers_changes = {"added": [], "removed": []}
1011 if observers:
1012 added_observers, removed_observers = \
1013 PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance())
1014
1015 observers_changes['added'] = sorted(
1016 [get_user_or_error(n).username for n in added_observers])
1017 observers_changes['removed'] = sorted(
1018 [get_user_or_error(n).username for n in removed_observers])
1019 Session().commit()
1020
1021 reviewers_changed = True
1022
1023 # push changed to channelstream
1024 if commits_changed or reviewers_changed or observers_changed:
1025 pr_broadcast_channel = channelstream.pr_channel(pull_request)
1026 msg = 'Pull request was updated.'
1027 channelstream.pr_update_channelstream_push(
1028 request, pr_broadcast_channel, apiuser, msg)
929
1029
930 data = {
1030 data = {
931 'msg': 'Updated pull request `{}`'.format(
1031 'msg': 'Updated pull request `{}`'.format(pull_request.pull_request_id),
932 pull_request.pull_request_id),
933 'pull_request': pull_request.get_api_data(),
1032 'pull_request': pull_request.get_api_data(),
934 'updated_commits': commit_changes,
1033 'updated_commits': commit_changes,
935 'updated_reviewers': reviewers_changes
1034 'updated_reviewers': reviewers_changes,
1035 'updated_observers': observers_changes,
936 }
1036 }
937
1037
938 return data
1038 return data
@@ -29,7 +29,7 b' from rhodecode.api.utils import ('
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
@@ -1597,10 +1597,13 b' def comment_commit('
1597 }
1597 }
1598
1598
1599 """
1599 """
1600 _ = request.translate
1601
1600 repo = get_repo_or_error(repoid)
1602 repo = get_repo_or_error(repoid)
1601 if not has_superadmin_permission(apiuser):
1603 if not has_superadmin_permission(apiuser):
1602 _perms = ('repository.read', 'repository.write', 'repository.admin')
1604 _perms = ('repository.read', 'repository.write', 'repository.admin')
1603 validate_repo_permissions(apiuser, repoid, repo, _perms)
1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 db_repo_name = repo.repo_name
1604
1607
1605 try:
1608 try:
1606 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1609 commit = repo.scm_instance().get_commit(commit_id=commit_id)
@@ -1650,6 +1653,8 b' def comment_commit('
1650 extra_recipients=extra_recipients,
1653 extra_recipients=extra_recipients,
1651 send_email=send_email
1654 send_email=send_email
1652 )
1655 )
1656 is_inline = comment.is_inline
1657
1653 if status:
1658 if status:
1654 # also do a status change
1659 # also do a status change
1655 try:
1660 try:
@@ -1669,6 +1674,17 b' def comment_commit('
1669 data={'comment': comment, 'commit': commit})
1674 data={'comment': comment, 'commit': commit})
1670
1675
1671 Session().commit()
1676 Session().commit()
1677
1678 comment_broadcast_channel = channelstream.comment_channel(
1679 db_repo_name, commit_obj=commit)
1680
1681 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1682 comment_type = 'inline' if is_inline else 'general'
1683 channelstream.comment_channelstream_push(
1684 request, comment_broadcast_channel, apiuser,
1685 _('posted a new {} comment').format(comment_type),
1686 comment_data=comment_data)
1687
1672 return {
1688 return {
1673 'msg': (
1689 'msg': (
1674 'Commented on commit `%s` for repository `%s`' % (
1690 'Commented on commit `%s` for repository `%s`' % (
@@ -474,9 +474,18 b' class AdminSettingsView(BaseAppView):'
474 route_name='admin_settings_issuetracker_test', request_method='POST',
474 route_name='admin_settings_issuetracker_test', request_method='POST',
475 renderer='string', xhr=True)
475 renderer='string', xhr=True)
476 def settings_issuetracker_test(self):
476 def settings_issuetracker_test(self):
477 return h.urlify_commit_message(
477 error_container = []
478
479 urlified_commit = h.urlify_commit_message(
478 self.request.POST.get('test_text', ''),
480 self.request.POST.get('test_text', ''),
479 'repo_group/test_repo1')
481 'repo_group/test_repo1', error_container=error_container)
482 if error_container:
483 def converter(inp):
484 return h.html_escape(unicode(inp))
485
486 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
487
488 return urlified_commit
480
489
481 @LoginRequired()
490 @LoginRequired()
482 @HasPermissionAllDecorator('hg.admin')
491 @HasPermissionAllDecorator('hg.admin')
@@ -34,6 +34,7 b' log = logging.getLogger(__name__)'
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37
37 def load_default_context(self):
38 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
39
40
@@ -75,6 +76,7 b' Check if we should use full-topic or min'
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
78 )
79
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
@@ -83,6 +85,7 b' Check if we should use full-topic or min'
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
86 'removed': ['eeeeeeeeeee'],
85 })
87 })
88
86 file_changes = AttributeDict({
89 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
90 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
91 'modified': ['b/modified_file.rst'],
@@ -97,15 +100,19 b' Check if we should use full-topic or min'
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 'exc_type': 'AttributeError'
101 'exc_type': 'AttributeError'
99 }
102 }
103
100 email_kwargs = {
104 email_kwargs = {
101 'test': {},
105 'test': {},
106
102 'message': {
107 'message': {
103 'body': 'message body !'
108 'body': 'message body !'
104 },
109 },
110
105 'email_test': {
111 'email_test': {
106 'user': user,
112 'user': user,
107 'date': datetime.datetime.now(),
113 'date': datetime.datetime.now(),
108 },
114 },
115
109 'exception': {
116 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
117 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
118 'exc_id': exc_traceback['exc_id'],
@@ -113,6 +120,7 b' Check if we should use full-topic or min'
113 'exc_type_name': 'NameError',
120 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
121 'exc_traceback': exc_traceback,
115 },
122 },
123
116 'password_reset': {
124 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118
126
@@ -121,6 +129,7 b' Check if we should use full-topic or min'
121 'email': 'test@rhodecode.com',
129 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
130 'first_admin_email': User.get_first_super_admin().email
123 },
131 },
132
124 'password_reset_confirmation': {
133 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
134 'new_password': 'new-password-example',
126 'user': user,
135 'user': user,
@@ -128,6 +137,7 b' Check if we should use full-topic or min'
128 'email': 'test@rhodecode.com',
137 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
138 'first_admin_email': User.get_first_super_admin().email
130 },
139 },
140
131 'registration': {
141 'registration': {
132 'user': user,
142 'user': user,
133 'date': datetime.datetime.now(),
143 'date': datetime.datetime.now(),
@@ -161,6 +171,7 b' Check if we should use full-topic or min'
161 'mention': True,
171 'mention': True,
162
172
163 },
173 },
174
164 'pull_request_comment+status': {
175 'pull_request_comment+status': {
165 'user': user,
176 'user': user,
166
177
@@ -201,6 +212,7 b' def db():'
201 'mention': True,
212 'mention': True,
202
213
203 },
214 },
215
204 'pull_request_comment+file': {
216 'pull_request_comment+file': {
205 'user': user,
217 'user': user,
206
218
@@ -303,6 +315,7 b' This should work better !'
303 'renderer_type': 'markdown',
315 'renderer_type': 'markdown',
304 'mention': True,
316 'mention': True,
305 },
317 },
318
306 'cs_comment+status': {
319 'cs_comment+status': {
307 'user': user,
320 'user': user,
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -328,6 +341,7 b' This is a multiline comment :)'
328 'renderer_type': 'markdown',
341 'renderer_type': 'markdown',
329 'mention': True,
342 'mention': True,
330 },
343 },
344
331 'cs_comment+file': {
345 'cs_comment+file': {
332 'user': user,
346 'user': user,
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -371,8 +385,58 b' users: description edit fixes'
371 'pull_request_source_repo_url': 'http://source-repo/url',
385 'pull_request_source_repo_url': 'http://source-repo/url',
372
386
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 'user_role': 'reviewer',
389 },
390
391 'pull_request+reviewer_role': {
392 'user': user,
393 'pull_request': pr,
394 'pull_request_commits': [
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
396 my-account: moved email closer to profile as it's similar data just moved outside.
397 '''),
398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
399 users: description edit fixes
400
401 - tests
402 - added metatags info
403 '''),
404 ],
405
406 'pull_request_target_repo': target_repo,
407 'pull_request_target_repo_url': 'http://target-repo/url',
408
409 'pull_request_source_repo': source_repo,
410 'pull_request_source_repo_url': 'http://source-repo/url',
411
412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 'user_role': 'reviewer',
414 },
415
416 'pull_request+observer_role': {
417 'user': user,
418 'pull_request': pr,
419 'pull_request_commits': [
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 my-account: moved email closer to profile as it's similar data just moved outside.
422 '''),
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 users: description edit fixes
425
426 - tests
427 - added metatags info
428 '''),
429 ],
430
431 'pull_request_target_repo': target_repo,
432 'pull_request_target_repo_url': 'http://target-repo/url',
433
434 'pull_request_source_repo': source_repo,
435 'pull_request_source_repo_url': 'http://source-repo/url',
436
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 'user_role': 'observer'
374 }
439 }
375
376 }
440 }
377
441
378 template_type = email_id.split('+')[0]
442 template_type = email_id.split('+')[0]
@@ -401,6 +465,7 b' users: description edit fixes'
401 c = self.load_default_context()
465 c = self.load_default_context()
402 c.active = os.path.splitext(t_path)[0]
466 c.active = os.path.splitext(t_path)[0]
403 c.came_from = ''
467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
404 c.email_types = {
469 c.email_types = {
405 'cs_comment+file': {},
470 'cs_comment+file': {},
406 'cs_comment+status': {},
471 'cs_comment+status': {},
@@ -409,6 +474,9 b' users: description edit fixes'
409 'pull_request_comment+status': {},
474 'pull_request_comment+status': {},
410
475
411 'pull_request_update': {},
476 'pull_request_update': {},
477
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
412 }
480 }
413 c.email_types.update(EmailNotificationModel.email_types)
481 c.email_types.update(EmailNotificationModel.email_types)
414
482
@@ -32,7 +32,7 b' from rhodecode.lib.auth import ('
32 HasRepoGroupPermissionAny, AuthUser)
32 HasRepoGroupPermissionAny, AuthUser)
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.lib.index import searcher_from_config
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int, safe_str
36 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.model.db import (
37 from rhodecode.model.db import (
38 func, true, or_, case, cast, in_filter_generator, String, Session,
38 func, true, or_, case, cast, in_filter_generator, String, Session,
@@ -331,7 +331,8 b' class HomeView(BaseAppView, DataGridAppV'
331 {
331 {
332 'id': obj.pull_request_id,
332 'id': obj.pull_request_id,
333 'value': org_query,
333 'value': org_query,
334 'value_display': 'pull request: `!{} - {}`'.format(obj.pull_request_id, obj.title[:50]),
334 'value_display': 'pull request: `!{} - {}`'.format(
335 obj.pull_request_id, safe_str(obj.title[:50])),
335 'type': 'pull_request',
336 'type': 'pull_request',
336 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
337 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
337 }
338 }
@@ -734,8 +734,8 b' class MyAccountView(BaseAppView, DataGri'
734 comments_model = CommentsModel()
734 comments_model = CommentsModel()
735 for pr in pull_requests:
735 for pr in pull_requests:
736 repo_id = pr.target_repo_id
736 repo_id = pr.target_repo_id
737 comments = comments_model.get_all_comments(
737 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr)
738 repo_id, pull_request=pr, count_only=True)
739 owned = pr.user_id == self._rhodecode_user.user_id
739 owned = pr.user_id == self._rhodecode_user.user_id
740
740
741 data.append({
741 data.append({
@@ -760,8 +760,8 b' class MyAccountView(BaseAppView, DataGri'
760 'author': _render('pullrequest_author',
760 'author': _render('pullrequest_author',
761 pr.author.full_contact, ),
761 pr.author.full_contact, ),
762 'author_raw': pr.author.full_name,
762 'author_raw': pr.author.full_name,
763 'comments': _render('pullrequest_comments', len(comments)),
763 'comments': _render('pullrequest_comments', comments_count),
764 'comments_raw': len(comments),
764 'comments_raw': comments_count,
765 'closed': pr.is_closed(),
765 'closed': pr.is_closed(),
766 'owned': owned
766 'owned': owned
767 })
767 })
@@ -523,7 +523,9 b' class TestPullrequestsView(object):'
523 pull_request = pr_util.create_pull_request()
523 pull_request = pr_util.create_pull_request()
524 pull_request_id = pull_request.pull_request_id
524 pull_request_id = pull_request.pull_request_id
525 PullRequestModel().update_reviewers(
525 PullRequestModel().update_reviewers(
526 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
526 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
527 pull_request.author)
529 pull_request.author)
528 author = pull_request.user_id
530 author = pull_request.user_id
529 repo = pull_request.target_repo.repo_id
531 repo = pull_request.target_repo.repo_id
@@ -906,12 +908,13 b' class TestPullrequestsView(object):'
906
908
907 # Change reviewers and check that a notification was made
909 # Change reviewers and check that a notification was made
908 PullRequestModel().update_reviewers(
910 PullRequestModel().update_reviewers(
909 pull_request.pull_request_id, [(1, [], False, [])],
911 pull_request.pull_request_id, [
912 (1, [], False, 'reviewer', [])
913 ],
910 pull_request.author)
914 pull_request.author)
911 assert len(notifications.all()) == 2
915 assert len(notifications.all()) == 2
912
916
913 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
917 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
914 csrf_token):
915 commits = [
918 commits = [
916 {'message': 'ancestor',
919 {'message': 'ancestor',
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
920 'added': [FileNode('file_A', content='content_of_ancestor')]},
@@ -18,14 +18,16 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h, rc_cache
22 from rhodecode.lib.utils2 import safe_int
22 from rhodecode.lib.utils2 import safe_int
23 from rhodecode.model.pull_request import get_diff_info
23 from rhodecode.model.pull_request import get_diff_info
24
24 from rhodecode.model.db import PullRequestReviewers
25 REVIEWER_API_VERSION = 'V3'
25 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
26
28
27
29
28 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
30 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
29 """
31 """
30 Returns json struct of a reviewer for frontend
32 Returns json struct of a reviewer for frontend
31
33
@@ -33,11 +35,15 b' def reviewer_as_json(user, reasons=None,'
33 :param reasons: list of strings of why they are reviewers
35 :param reasons: list of strings of why they are reviewers
34 :param mandatory: bool, to set user as mandatory
36 :param mandatory: bool, to set user as mandatory
35 """
37 """
38 role = role or PullRequestReviewers.ROLE_REVIEWER
39 if role not in PullRequestReviewers.ROLES:
40 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
36
41
37 return {
42 return {
38 'user_id': user.user_id,
43 'user_id': user.user_id,
39 'reasons': reasons or [],
44 'reasons': reasons or [],
40 'rules': rules or [],
45 'rules': rules or [],
46 'role': role,
41 'mandatory': mandatory,
47 'mandatory': mandatory,
42 'user_group': user_group,
48 'user_group': user_group,
43 'username': user.username,
49 'username': user.username,
@@ -48,21 +54,36 b' def reviewer_as_json(user, reasons=None,'
48 }
54 }
49
55
50
56
51 def get_default_reviewers_data(
57 def to_reviewers(e):
52 current_user, source_repo, source_commit, target_repo, target_commit):
58 if isinstance(e, (tuple, list)):
59 return map(reviewer_as_json, e)
60 else:
61 return reviewer_as_json(e)
62
63
64 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
65 include_diff_info=True):
53 """
66 """
54 Return json for default reviewers of a repository
67 Return json for default reviewers of a repository
55 """
68 """
56
69
70 diff_info = {}
71 if include_diff_info:
57 diff_info = get_diff_info(
72 diff_info = get_diff_info(
58 source_repo, source_commit.raw_id, target_repo, target_commit.raw_id)
73 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
59
74
60 reasons = ['Default reviewer', 'Repository owner']
75 reasons = ['Default reviewer', 'Repository owner']
61 json_reviewers = [reviewer_as_json(
76 json_reviewers = [reviewer_as_json(
62 user=target_repo.user, reasons=reasons, mandatory=False, rules=None)]
77 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
78
79 compute_key = rc_cache.utils.compute_key_from_params(
80 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
81 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
82 target_ref.commit_id)
63
83
64 return {
84 return {
65 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
85 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
86 'compute_key': compute_key,
66 'diff_info': diff_info,
87 'diff_info': diff_info,
67 'reviewers': json_reviewers,
88 'reviewers': json_reviewers,
68 'rules': {},
89 'rules': {},
@@ -73,15 +94,18 b' def get_default_reviewers_data('
73 def validate_default_reviewers(review_members, reviewer_rules):
94 def validate_default_reviewers(review_members, reviewer_rules):
74 """
95 """
75 Function to validate submitted reviewers against the saved rules
96 Function to validate submitted reviewers against the saved rules
76
77 """
97 """
78 reviewers = []
98 reviewers = []
79 reviewer_by_id = {}
99 reviewer_by_id = {}
80 for r in review_members:
100 for r in review_members:
81 reviewer_user_id = safe_int(r['user_id'])
101 reviewer_user_id = safe_int(r['user_id'])
82 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
102 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
83
103
84 reviewer_by_id[reviewer_user_id] = entry
104 reviewer_by_id[reviewer_user_id] = entry
85 reviewers.append(entry)
105 reviewers.append(entry)
86
106
87 return reviewers
107 return reviewers
108
109
110 def validate_observers(observer_members, reviewer_rules):
111 return {}
@@ -31,7 +31,7 b' from rhodecode.apps._base import RepoApp'
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
@@ -170,7 +170,9 b' class RepoCommitsView(RepoAppView):'
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.allowed_reviewers = reviewers
173 c.reviewers_count = len(reviewers)
174 c.observers_count = 0
175
174 # from associated statuses, check the pull requests, and
176 # from associated statuses, check the pull requests, and
175 # show comments from them
177 # show comments from them
176 for pr in prs:
178 for pr in prs:
@@ -193,7 +195,7 b' class RepoCommitsView(RepoAppView):'
193
195
194 for review_obj, member, reasons, mandatory, status in review_statuses:
196 for review_obj, member, reasons, mandatory, status in review_statuses:
195 member_reviewer = h.reviewer_as_json(
197 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
198 member, reasons=reasons, mandatory=mandatory, role=None,
197 user_group=None
199 user_group=None
198 )
200 )
199
201
@@ -207,10 +209,7 b' class RepoCommitsView(RepoAppView):'
207
209
208 # NOTE(marcink): this uses the same voting logic as in pull-requests
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
211 c.repo_name,
212 commit.raw_id
213 )
214
213
215 diff = None
214 diff = None
216 # Iterate over ranges (default commit view is always one commit)
215 # Iterate over ranges (default commit view is always one commit)
@@ -414,6 +413,7 b' class RepoCommitsView(RepoAppView):'
414 resolves_comment_id=resolves_comment_id,
413 resolves_comment_id=resolves_comment_id,
415 auth_user=self._rhodecode_user
414 auth_user=self._rhodecode_user
416 )
415 )
416 is_inline = comment.is_inline
417
417
418 # get status if set !
418 # get status if set !
419 if status:
419 if status:
@@ -461,6 +461,16 b' class RepoCommitsView(RepoAppView):'
461 data.update(comment.get_dict())
461 data.update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
462 data.update({'rendered_text': rendered_comment})
463
463
464 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
466
467 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
473
464 return data
474 return data
465
475
466 @LoginRequired()
476 @LoginRequired()
@@ -39,14 +39,16 b' from rhodecode.lib.ext_json import json'
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
49 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
50 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
@@ -104,13 +106,14 b' class RepoPullRequestsView(RepoAppView, '
104 data = []
106 data = []
105 comments_model = CommentsModel()
107 comments_model = CommentsModel()
106 for pr in pull_requests:
108 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
109
111
110 data.append({
112 data.append({
111 'name': _render('pullrequest_name',
113 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
114 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
115 pr.work_in_progress, pr.target_repo.repo_name,
116 short=True),
114 'name_raw': pr.pull_request_id,
117 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
118 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
119 pr.calculated_review_status()),
@@ -126,8 +129,8 b' class RepoPullRequestsView(RepoAppView, '
126 'author': _render('pullrequest_author',
129 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
130 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
131 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
132 'comments': _render('pullrequest_comments', comments_count),
130 'comments_raw': len(comments),
133 'comments_raw': comments_count,
131 'closed': pr.is_closed(),
134 'closed': pr.is_closed(),
132 })
135 })
133
136
@@ -310,8 +313,7 b' class RepoPullRequestsView(RepoAppView, '
310 pull_request_id = pull_request.pull_request_id
313 pull_request_id = pull_request.pull_request_id
311
314
312 c.state_progressing = pull_request.is_state_changing()
315 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
316 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
315
317
316 _new_state = {
318 _new_state = {
317 'created': PullRequest.STATE_CREATED,
319 'created': PullRequest.STATE_CREATED,
@@ -454,15 +456,18 b' class RepoPullRequestsView(RepoAppView, '
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 return self._get_template_context(c)
457 return self._get_template_context(c)
456
458
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
458
461
459 # reviewers and statuses
462 # reviewers and statuses
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
462
466
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
465 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
466 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
467 )
472 )
468
473
@@ -474,6 +479,17 b' class RepoPullRequestsView(RepoAppView, '
474
479
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
476
481
482 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
487 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
477 general_comments, inline_comments = \
493 general_comments, inline_comments = \
478 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
479
495
@@ -745,7 +761,9 b' class RepoPullRequestsView(RepoAppView, '
745
761
746 # current user review statuses for each version
762 # current user review statuses for each version
747 c.review_versions = {}
763 c.review_versions = {}
748 if self._rhodecode_user.user_id in c.allowed_reviewers:
764 is_reviewer = PullRequestModel().is_user_reviewer(
765 pull_request, self._rhodecode_user)
766 if is_reviewer:
749 for co in general_comments:
767 for co in general_comments:
750 if co.author.user_id == self._rhodecode_user.user_id:
768 if co.author.user_id == self._rhodecode_user.user_id:
751 status = co.status_change
769 status = co.status_change
@@ -961,13 +979,16 b' class RepoPullRequestsView(RepoAppView, '
961 }
979 }
962 return data
980 return data
963
981
982 def _get_existing_ids(self, post_data):
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
984
964 @LoginRequired()
985 @LoginRequired()
965 @NotAnonymous()
986 @NotAnonymous()
966 @HasRepoPermissionAnyDecorator(
987 @HasRepoPermissionAnyDecorator(
967 'repository.read', 'repository.write', 'repository.admin')
988 'repository.read', 'repository.write', 'repository.admin')
968 @view_config(
989 @view_config(
969 route_name='pullrequest_comments', request_method='POST',
990 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
991 renderer='string_html', xhr=True)
971 def pullrequest_comments(self):
992 def pullrequest_comments(self):
972 self.load_default_context()
993 self.load_default_context()
973
994
@@ -997,8 +1018,7 b' class RepoPullRequestsView(RepoAppView, '
997 self.register_comments_vars(c, pull_request_latest, versions)
1018 self.register_comments_vars(c, pull_request_latest, versions)
998 all_comments = c.inline_comments_flat + c.comments
1019 all_comments = c.inline_comments_flat + c.comments
999
1020
1000 existing_ids = filter(
1021 existing_ids = self._get_existing_ids(self.request.POST)
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1002 return _render('comments_table', all_comments, len(all_comments),
1022 return _render('comments_table', all_comments, len(all_comments),
1003 existing_ids=existing_ids)
1023 existing_ids=existing_ids)
1004
1024
@@ -1008,7 +1028,7 b' class RepoPullRequestsView(RepoAppView, '
1008 'repository.read', 'repository.write', 'repository.admin')
1028 'repository.read', 'repository.write', 'repository.admin')
1009 @view_config(
1029 @view_config(
1010 route_name='pullrequest_todos', request_method='POST',
1030 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1031 renderer='string_html', xhr=True)
1012 def pullrequest_todos(self):
1032 def pullrequest_todos(self):
1013 self.load_default_context()
1033 self.load_default_context()
1014
1034
@@ -1040,8 +1060,7 b' class RepoPullRequestsView(RepoAppView, '
1040 .get_pull_request_resolved_todos(pull_request)
1060 .get_pull_request_resolved_todos(pull_request)
1041
1061
1042 all_comments = c.unresolved_comments + c.resolved_comments
1062 all_comments = c.unresolved_comments + c.resolved_comments
1043 existing_ids = filter(
1063 existing_ids = self._get_existing_ids(self.request.POST)
1044 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1045 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 return _render('comments_table', all_comments, len(c.unresolved_comments),
1046 todo_comments=True, existing_ids=existing_ids)
1065 todo_comments=True, existing_ids=existing_ids)
1047
1066
@@ -1128,30 +1147,35 b' class RepoPullRequestsView(RepoAppView, '
1128 source_scm = source_db_repo.scm_instance()
1147 source_scm = source_db_repo.scm_instance()
1129 target_scm = target_db_repo.scm_instance()
1148 target_scm = target_db_repo.scm_instance()
1130
1149
1131 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1150 source_ref_obj = unicode_to_reference(source_ref)
1132 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1151 target_ref_obj = unicode_to_reference(target_ref)
1152
1153 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1154 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1133
1155
1134 ancestor = source_scm.get_common_ancestor(
1156 ancestor = source_scm.get_common_ancestor(
1135 source_commit.raw_id, target_commit.raw_id, target_scm)
1157 source_commit.raw_id, target_commit.raw_id, target_scm)
1136
1158
1137 # recalculate target ref based on ancestor
1159 # recalculate target ref based on ancestor
1138 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1160 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1139 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1140
1161
1141 get_default_reviewers_data, validate_default_reviewers = \
1162 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1142 PullRequestModel().get_reviewer_functions()
1163 PullRequestModel().get_reviewer_functions()
1143
1164
1144 # recalculate reviewers logic, to make sure we can validate this
1165 # recalculate reviewers logic, to make sure we can validate this
1145 reviewer_rules = get_default_reviewers_data(
1166 reviewer_rules = get_default_reviewers_data(
1146 self._rhodecode_db_user, source_db_repo,
1167 self._rhodecode_db_user,
1147 source_commit, target_db_repo, target_commit)
1168 source_db_repo,
1169 source_ref_obj,
1170 target_db_repo,
1171 target_ref_obj,
1172 include_diff_info=False)
1148
1173
1149 given_reviewers = _form['review_members']
1174 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1150 reviewers = validate_default_reviewers(
1175 observers = validate_observers(_form['observer_members'], reviewer_rules)
1151 given_reviewers, reviewer_rules)
1152
1176
1153 pullrequest_title = _form['pullrequest_title']
1177 pullrequest_title = _form['pullrequest_title']
1154 title_source_ref = source_ref.split(':', 2)[1]
1178 title_source_ref = source_ref_obj.name
1155 if not pullrequest_title:
1179 if not pullrequest_title:
1156 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1180 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1157 source=source_repo,
1181 source=source_repo,
@@ -1172,6 +1196,7 b' class RepoPullRequestsView(RepoAppView, '
1172 revisions=commit_ids,
1196 revisions=commit_ids,
1173 common_ancestor_id=common_ancestor_id,
1197 common_ancestor_id=common_ancestor_id,
1174 reviewers=reviewers,
1198 reviewers=reviewers,
1199 observers=observers,
1175 title=pullrequest_title,
1200 title=pullrequest_title,
1176 description=description,
1201 description=description,
1177 description_renderer=description_renderer,
1202 description_renderer=description_renderer,
@@ -1221,20 +1246,28 b' class RepoPullRequestsView(RepoAppView, '
1221 'redirect_url': redirect_url}
1246 'redirect_url': redirect_url}
1222
1247
1223 is_state_changing = pull_request.is_state_changing()
1248 is_state_changing = pull_request.is_state_changing()
1224 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1249 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1225 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1226
1250
1227 # only owner or admin can update it
1251 # only owner or admin can update it
1228 allowed_to_update = PullRequestModel().check_user_update(
1252 allowed_to_update = PullRequestModel().check_user_update(
1229 pull_request, self._rhodecode_user)
1253 pull_request, self._rhodecode_user)
1254
1230 if allowed_to_update:
1255 if allowed_to_update:
1231 controls = peppercorn.parse(self.request.POST.items())
1256 controls = peppercorn.parse(self.request.POST.items())
1232 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1257 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1233
1258
1234 if 'review_members' in controls:
1259 if 'review_members' in controls:
1235 self._update_reviewers(
1260 self._update_reviewers(
1261 c,
1236 pull_request, controls['review_members'],
1262 pull_request, controls['review_members'],
1237 pull_request.reviewer_data)
1263 pull_request.reviewer_data,
1264 PullRequestReviewers.ROLE_REVIEWER)
1265 elif 'observer_members' in controls:
1266 self._update_reviewers(
1267 c,
1268 pull_request, controls['observer_members'],
1269 pull_request.reviewer_data,
1270 PullRequestReviewers.ROLE_OBSERVER)
1238 elif str2bool(self.request.POST.get('update_commits', 'false')):
1271 elif str2bool(self.request.POST.get('update_commits', 'false')):
1239 if is_state_changing:
1272 if is_state_changing:
1240 log.debug('commits update: forbidden because pull request is in state %s',
1273 log.debug('commits update: forbidden because pull request is in state %s',
@@ -1255,6 +1288,7 b' class RepoPullRequestsView(RepoAppView, '
1255 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1288 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1256 self._edit_pull_request(pull_request)
1289 self._edit_pull_request(pull_request)
1257 else:
1290 else:
1291 log.error('Unhandled update data.')
1258 raise HTTPBadRequest()
1292 raise HTTPBadRequest()
1259
1293
1260 return {'response': True,
1294 return {'response': True,
@@ -1262,6 +1296,9 b' class RepoPullRequestsView(RepoAppView, '
1262 raise HTTPForbidden()
1296 raise HTTPForbidden()
1263
1297
1264 def _edit_pull_request(self, pull_request):
1298 def _edit_pull_request(self, pull_request):
1299 """
1300 Edit title and description
1301 """
1265 _ = self.request.translate
1302 _ = self.request.translate
1266
1303
1267 try:
1304 try:
@@ -1302,27 +1339,15 b' class RepoPullRequestsView(RepoAppView, '
1302
1339
1303 msg = _(u'Pull request updated to "{source_commit_id}" with '
1340 msg = _(u'Pull request updated to "{source_commit_id}" with '
1304 u'{count_added} added, {count_removed} removed commits. '
1341 u'{count_added} added, {count_removed} removed commits. '
1305 u'Source of changes: {change_source}')
1342 u'Source of changes: {change_source}.')
1306 msg = msg.format(
1343 msg = msg.format(
1307 source_commit_id=pull_request.source_ref_parts.commit_id,
1344 source_commit_id=pull_request.source_ref_parts.commit_id,
1308 count_added=len(resp.changes.added),
1345 count_added=len(resp.changes.added),
1309 count_removed=len(resp.changes.removed),
1346 count_removed=len(resp.changes.removed),
1310 change_source=changed)
1347 change_source=changed)
1311 h.flash(msg, category='success')
1348 h.flash(msg, category='success')
1312
1349 channelstream.pr_update_channelstream_push(
1313 message = msg + (
1350 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1314 ' - <a onclick="window.location.reload()">'
1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1323 channelstream.post_message(
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1325 registry=self.request.registry)
1326 else:
1351 else:
1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1352 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1328 warning_reasons = [
1353 warning_reasons = [
@@ -1332,6 +1357,55 b' class RepoPullRequestsView(RepoAppView, '
1332 category = 'warning' if resp.reason in warning_reasons else 'error'
1357 category = 'warning' if resp.reason in warning_reasons else 'error'
1333 h.flash(msg, category=category)
1358 h.flash(msg, category=category)
1334
1359
1360 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1361 _ = self.request.translate
1362
1363 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1364 PullRequestModel().get_reviewer_functions()
1365
1366 if role == PullRequestReviewers.ROLE_REVIEWER:
1367 try:
1368 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1369 except ValueError as e:
1370 log.error('Reviewers Validation: {}'.format(e))
1371 h.flash(e, category='error')
1372 return
1373
1374 old_calculated_status = pull_request.calculated_review_status()
1375 PullRequestModel().update_reviewers(
1376 pull_request, reviewers, self._rhodecode_db_user)
1377
1378 Session().commit()
1379
1380 msg = _('Pull request reviewers updated.')
1381 h.flash(msg, category='success')
1382 channelstream.pr_update_channelstream_push(
1383 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1384
1385 # trigger status changed if change in reviewers changes the status
1386 calculated_status = pull_request.calculated_review_status()
1387 if old_calculated_status != calculated_status:
1388 PullRequestModel().trigger_pull_request_hook(
1389 pull_request, self._rhodecode_user, 'review_status_change',
1390 data={'status': calculated_status})
1391
1392 elif role == PullRequestReviewers.ROLE_OBSERVER:
1393 try:
1394 observers = validate_observers(review_members, reviewer_rules)
1395 except ValueError as e:
1396 log.error('Observers Validation: {}'.format(e))
1397 h.flash(e, category='error')
1398 return
1399
1400 PullRequestModel().update_observers(
1401 pull_request, observers, self._rhodecode_db_user)
1402
1403 Session().commit()
1404 msg = _('Pull request observers updated.')
1405 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1335 @LoginRequired()
1409 @LoginRequired()
1336 @NotAnonymous()
1410 @NotAnonymous()
1337 @HasRepoPermissionAnyDecorator(
1411 @HasRepoPermissionAnyDecorator(
@@ -1408,32 +1482,6 b' class RepoPullRequestsView(RepoAppView, '
1408 msg = merge_resp.merge_status_message
1482 msg = merge_resp.merge_status_message
1409 h.flash(msg, category='error')
1483 h.flash(msg, category='error')
1410
1484
1411 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1412 _ = self.request.translate
1413
1414 get_default_reviewers_data, validate_default_reviewers = \
1415 PullRequestModel().get_reviewer_functions()
1416
1417 try:
1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1419 except ValueError as e:
1420 log.error('Reviewers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1422 return
1423
1424 old_calculated_status = pull_request.calculated_review_status()
1425 PullRequestModel().update_reviewers(
1426 pull_request, reviewers, self._rhodecode_user)
1427 h.flash(_('Pull request reviewers updated.'), category='success')
1428 Session().commit()
1429
1430 # trigger status changed if change in reviewers changes the status
1431 calculated_status = pull_request.calculated_review_status()
1432 if old_calculated_status != calculated_status:
1433 PullRequestModel().trigger_pull_request_hook(
1434 pull_request, self._rhodecode_user, 'review_status_change',
1435 data={'status': calculated_status})
1436
1437 @LoginRequired()
1485 @LoginRequired()
1438 @NotAnonymous()
1486 @NotAnonymous()
1439 @HasRepoPermissionAnyDecorator(
1487 @HasRepoPermissionAnyDecorator(
@@ -1488,8 +1536,7 b' class RepoPullRequestsView(RepoAppView, '
1488 allowed_to_comment = PullRequestModel().check_user_comment(
1536 allowed_to_comment = PullRequestModel().check_user_comment(
1489 pull_request, self._rhodecode_user)
1537 pull_request, self._rhodecode_user)
1490 if not allowed_to_comment:
1538 if not allowed_to_comment:
1491 log.debug(
1539 log.debug('comment: forbidden because pull request is from forbidden repo')
1492 'comment: forbidden because pull request is from forbidden repo')
1493 raise HTTPForbidden()
1540 raise HTTPForbidden()
1494
1541
1495 c = self.load_default_context()
1542 c = self.load_default_context()
@@ -1518,6 +1565,7 b' class RepoPullRequestsView(RepoAppView, '
1518 pull_request, self._rhodecode_user, self.db_repo, message=text,
1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1519 auth_user=self._rhodecode_user)
1566 auth_user=self._rhodecode_user)
1520 Session().flush()
1567 Session().flush()
1568 is_inline = comment.is_inline
1521
1569
1522 PullRequestModel().trigger_pull_request_hook(
1570 PullRequestModel().trigger_pull_request_hook(
1523 pull_request, self._rhodecode_user, 'comment',
1571 pull_request, self._rhodecode_user, 'comment',
@@ -1551,6 +1599,7 b' class RepoPullRequestsView(RepoAppView, '
1551 resolves_comment_id=resolves_comment_id,
1599 resolves_comment_id=resolves_comment_id,
1552 auth_user=self._rhodecode_user
1600 auth_user=self._rhodecode_user
1553 )
1601 )
1602 is_inline = comment.is_inline
1554
1603
1555 if allowed_to_change_status:
1604 if allowed_to_change_status:
1556 # calculate old status before we change it
1605 # calculate old status before we change it
@@ -1599,6 +1648,16 b' class RepoPullRequestsView(RepoAppView, '
1599 data.update(comment.get_dict())
1648 data.update(comment.get_dict())
1600 data.update({'rendered_text': rendered_comment})
1649 data.update({'rendered_text': rendered_comment})
1601
1650
1651 comment_broadcast_channel = channelstream.comment_channel(
1652 self.db_repo_name, pull_request_obj=pull_request)
1653
1654 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1656 channelstream.comment_channelstream_push(
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1658 _('posted a new {} comment').format(comment_type),
1659 comment_data=comment_data)
1660
1602 return data
1661 return data
1603
1662
1604 @LoginRequired()
1663 @LoginRequired()
@@ -25,6 +25,7 b' from pyramid.view import view_config'
25 from rhodecode.apps._base import RepoAppView
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 from rhodecode.lib.vcs.backends.base import Reference
28 from rhodecode.model.db import Repository
29 from rhodecode.model.db import Repository
29
30
30 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
@@ -61,13 +62,28 b' class RepoReviewRulesView(RepoAppView):'
61 target_repo_name = request.GET.get('target_repo', source_repo_name)
62 target_repo_name = request.GET.get('target_repo', source_repo_name)
62 target_repo = Repository.get_by_repo_name(target_repo_name)
63 target_repo = Repository.get_by_repo_name(target_repo_name)
63
64
64 source_ref = request.GET['source_ref']
65 current_user = request.user.get_instance()
65 target_ref = request.GET['target_ref']
66
66 source_commit = source_repo.get_commit(source_ref)
67 source_commit_id = request.GET['source_ref']
67 target_commit = target_repo.get_commit(target_ref)
68 source_type = request.GET['source_ref_type']
69 source_name = request.GET['source_ref_name']
70
71 target_commit_id = request.GET['target_ref']
72 target_type = request.GET['target_ref_type']
73 target_name = request.GET['target_ref_name']
68
74
69 current_user = request.user.get_instance()
75 try:
70 review_data = get_default_reviewers_data(
76 review_data = get_default_reviewers_data(
71 current_user, source_repo, source_commit, target_repo, target_commit)
77 current_user,
78 source_repo,
79 Reference(source_type, source_name, source_commit_id),
80 target_repo,
81 Reference(target_type, target_name, target_commit_id)
82 )
83 except ValueError:
84 # No common ancestor
85 msg = "No Common ancestor found between target and source reference"
86 log.exception(msg)
87 return {'diff_info': {'error': msg}}
72
88
73 return review_data
89 return review_data
@@ -341,6 +341,10 b' def includeme(config):'
341 name='json_ext',
341 name='json_ext',
342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
343
343
344 config.add_renderer(
345 name='string_html',
346 factory='rhodecode.lib.string_renderer.html')
347
344 # include RhodeCode plugins
348 # include RhodeCode plugins
345 includes = aslist(settings.get('rhodecode.includes', []))
349 includes = aslist(settings.get('rhodecode.includes', []))
346 for inc in includes:
350 for inc in includes:
@@ -408,6 +412,7 b' def sanitize_settings_and_apply_defaults'
408 """
412 """
409
413
410 settings.setdefault('rhodecode.edition', 'Community Edition')
414 settings.setdefault('rhodecode.edition', 'Community Edition')
415 settings.setdefault('rhodecode.edition_id', 'CE')
411
416
412 if 'mako.default_filters' not in settings:
417 if 'mako.default_filters' not in settings:
413 # set custom default filters if we don't have it defined
418 # set custom default filters if we don't have it defined
@@ -113,7 +113,7 b' def _commits_as_dict(event, commit_ids, '
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
113 cs_data['permalink_url'] = RepoModel().get_commit_url(
114 repo, cs_data['raw_id'], request=event.request,
114 repo, cs_data['raw_id'], request=event.request,
115 permalink=True)
115 permalink=True)
116 urlified_message, issues_data = process_patterns(
116 urlified_message, issues_data, errors = process_patterns(
117 cs_data['message'], repo.repo_name)
117 cs_data['message'], repo.repo_name)
118 cs_data['issues'] = issues_data
118 cs_data['issues'] = issues_data
119 cs_data['message_html'] = urlify_commit_message(
119 cs_data['message_html'] = urlify_commit_message(
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -88,6 +88,9 b' ACTIONS_V1 = {'
88 'repo.pull_request.reviewer.add': '',
88 'repo.pull_request.reviewer.add': '',
89 'repo.pull_request.reviewer.delete': '',
89 'repo.pull_request.reviewer.delete': '',
90
90
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93
91 'repo.commit.strip': {'commit_id': ''},
94 'repo.commit.strip': {'commit_id': ''},
92 'repo.commit.comment.create': {'data': {}},
95 'repo.commit.comment.create': {'data': {}},
93 'repo.commit.comment.delete': {'data': {}},
96 'repo.commit.comment.delete': {'data': {}},
@@ -293,6 +293,7 b' def attach_context_attributes(context, r'
293 context.rc_config = rc_config
293 context.rc_config = rc_config
294 context.rhodecode_version = rhodecode.__version__
294 context.rhodecode_version = rhodecode.__version__
295 context.rhodecode_edition = config.get('rhodecode.edition')
295 context.rhodecode_edition = config.get('rhodecode.edition')
296 context.rhodecode_edition_id = config.get('rhodecode.edition_id')
296 # unique secret + version does not leak the version but keep consistency
297 # unique secret + version does not leak the version but keep consistency
297 context.rhodecode_version_hash = calculate_version_hash(config)
298 context.rhodecode_version_hash = calculate_version_hash(config)
298
299
@@ -225,14 +225,26 b' def write_history(config, message):'
225
225
226 def get_connection_validators(registry):
226 def get_connection_validators(registry):
227 validators = []
227 validators = []
228 for k, config in registry.rhodecode_plugins.iteritems():
228 for k, config in registry.rhodecode_plugins.items():
229 validator = config.get('channelstream', {}).get('connect_validator')
229 validator = config.get('channelstream', {}).get('connect_validator')
230 if validator:
230 if validator:
231 validators.append(validator)
231 validators.append(validator)
232 return validators
232 return validators
233
233
234
234
235 def get_channelstream_config(registry=None):
236 if not registry:
237 registry = get_current_registry()
238
239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
240 channelstream_config = rhodecode_plugins.get('channelstream', {})
241 return channelstream_config
242
243
235 def post_message(channel, message, username, registry=None):
244 def post_message(channel, message, username, registry=None):
245 channelstream_config = get_channelstream_config(registry)
246 if not channelstream_config.get('enabled'):
247 return
236
248
237 message_obj = message
249 message_obj = message
238 if isinstance(message, basestring):
250 if isinstance(message, basestring):
@@ -242,13 +254,7 b' def post_message(channel, message, usern'
242 'topic': '/notifications'
254 'topic': '/notifications'
243 }
255 }
244
256
245 if not registry:
246 registry = get_current_registry()
247
248 log.debug('Channelstream: sending notification to channel %s', channel)
257 log.debug('Channelstream: sending notification to channel %s', channel)
249 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
250 channelstream_config = rhodecode_plugins.get('channelstream', {})
251 if channelstream_config.get('enabled'):
252 payload = {
258 payload = {
253 'type': 'message',
259 'type': 'message',
254 'timestamp': datetime.datetime.utcnow(),
260 'timestamp': datetime.datetime.utcnow(),
@@ -265,3 +271,101 b' def post_message(channel, message, usern'
265 except ChannelstreamException:
271 except ChannelstreamException:
266 log.exception('Failed to send channelstream data')
272 log.exception('Failed to send channelstream data')
267 raise
273 raise
274
275
276 def _reload_link(label):
277 return (
278 '<a onclick="window.location.reload()">'
279 '<strong>{}</strong>'
280 '</a>'.format(label)
281 )
282
283
284 def pr_channel(pull_request):
285 repo_name = pull_request.target_repo.repo_name
286 pull_request_id = pull_request.pull_request_id
287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
289 return channel
290
291
292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
293 channel = None
294 if commit_obj:
295 channel = u'/repo${}$/commit/{}'.format(
296 repo_name, commit_obj.raw_id
297 )
298 elif pull_request_obj:
299 channel = u'/repo${}$/pr/{}'.format(
300 repo_name, pull_request_obj.pull_request_id
301 )
302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
303
304 return channel
305
306
307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
308 """
309 Channel push on pull request update
310 """
311 if not pr_broadcast_channel:
312 return
313
314 _ = request.translate
315
316 message = '{} {}'.format(
317 msg,
318 _reload_link(_(' Reload page to load changes')))
319
320 message_obj = {
321 'message': message,
322 'level': 'success',
323 'topic': '/notifications'
324 }
325
326 post_message(
327 pr_broadcast_channel, message_obj, user.username,
328 registry=request.registry)
329
330
331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
332 """
333 Channelstream push on comment action, on commit, or pull-request
334 """
335 if not comment_broadcast_channel:
336 return
337
338 _ = request.translate
339
340 comment_data = kwargs.pop('comment_data', {})
341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
345 user.username,
346 msg,
347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 )
350
351 message_obj = {
352 'message': message,
353 'level': 'success',
354 'topic': '/notifications'
355 }
356
357 post_message(
358 comment_broadcast_channel, message_obj, user.username,
359 registry=request.registry)
360
361 message_obj = {
362 'message': None,
363 'user': user.username,
364 'comment_id': comment_id,
365 'comment_data': comment_data,
366 'user_data': user_data,
367 'topic': '/comment'
368 }
369 post_message(
370 comment_broadcast_channel, message_obj, user.username,
371 registry=request.registry)
@@ -131,7 +131,7 b' def send_exc_email(request, exc_id, exc_'
131
131
132 # NOTE(marcink): needed for email template rendering
132 # NOTE(marcink): needed for email template rendering
133 user_id = None
133 user_id = None
134 if request:
134 if hasattr(request, 'user'):
135 user_id = request.user.user_id
135 user_id = request.user.user_id
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137
137
@@ -38,6 +38,7 b' import re'
38 import time
38 import time
39 import string
39 import string
40 import hashlib
40 import hashlib
41 import regex
41 from collections import OrderedDict
42 from collections import OrderedDict
42
43
43 import pygments
44 import pygments
@@ -1103,6 +1104,10 b' def bool2icon(value, show_at_false=True)'
1103 return HTML.tag('i', class_="icon-false", title='False')
1104 return HTML.tag('i', class_="icon-false", title='False')
1104 return HTML.tag('i')
1105 return HTML.tag('i')
1105
1106
1107
1108 def b64(inp):
1109 return base64.b64encode(inp)
1110
1106 #==============================================================================
1111 #==============================================================================
1107 # PERMS
1112 # PERMS
1108 #==============================================================================
1113 #==============================================================================
@@ -1653,7 +1658,7 b' def get_active_pattern_entries(repo_name'
1653 return active_entries
1658 return active_entries
1654
1659
1655
1660
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1661 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1657
1662
1658 allowed_link_formats = [
1663 allowed_link_formats = [
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1664 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
@@ -1670,6 +1675,7 b' def process_patterns(text_string, repo_n'
1670 active_entries = get_active_pattern_entries(repo_name)
1675 active_entries = get_active_pattern_entries(repo_name)
1671
1676
1672 issues_data = []
1677 issues_data = []
1678 errors = []
1673 new_text = text_string
1679 new_text = text_string
1674
1680
1675 log.debug('Got %s entries to process', len(active_entries))
1681 log.debug('Got %s entries to process', len(active_entries))
@@ -1687,9 +1693,11 b' def process_patterns(text_string, repo_n'
1687 pattern = entry['pat_compiled']
1693 pattern = entry['pat_compiled']
1688 else:
1694 else:
1689 try:
1695 try:
1690 pattern = re.compile(r'%s' % entry['pat'])
1696 pattern = regex.compile(r'%s' % entry['pat'])
1691 except re.error:
1697 except regex.error as e:
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1700 errors.append(regex_err)
1693 continue
1701 continue
1694
1702
1695 data_func = partial(
1703 data_func = partial(
@@ -1721,11 +1729,11 b' def process_patterns(text_string, repo_n'
1721 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1722 log.debug('processed !pr pattern')
1730 log.debug('processed !pr pattern')
1723
1731
1724 return new_text, issues_data
1732 return new_text, issues_data, errors
1725
1733
1726
1734
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1736 issues_container=None, error_container=None):
1729 """
1737 """
1730 Parses given text message and makes proper links.
1738 Parses given text message and makes proper links.
1731 issues are linked to given issue-server, and rest is a commit link
1739 issues are linked to given issue-server, and rest is a commit link
@@ -1745,12 +1753,15 b' def urlify_commit_message(commit_text, r'
1745 new_text = urlify_commits(new_text, repository)
1753 new_text = urlify_commits(new_text, repository)
1746
1754
1747 # process issue tracker patterns
1755 # process issue tracker patterns
1748 new_text, issues = process_patterns(new_text, repository or '',
1756 new_text, issues, errors = process_patterns(
1749 active_entries=active_pattern_entries)
1757 new_text, repository or '', active_entries=active_pattern_entries)
1750
1758
1751 if issues_container is not None:
1759 if issues_container is not None:
1752 issues_container.extend(issues)
1760 issues_container.extend(issues)
1753
1761
1762 if error_container is not None:
1763 error_container.extend(errors)
1764
1754 return literal(new_text)
1765 return literal(new_text)
1755
1766
1756
1767
@@ -1805,7 +1816,7 b" def render(source, renderer='rst', menti"
1805 elif renderer == 'rst':
1816 elif renderer == 'rst':
1806 if repo_name:
1817 if repo_name:
1807 # process patterns on comments if we pass in repo name
1818 # process patterns on comments if we pass in repo name
1808 source, issues = process_patterns(
1819 source, issues, errors = process_patterns(
1809 source, repo_name, link_format='rst',
1820 source, repo_name, link_format='rst',
1810 active_entries=active_pattern_entries)
1821 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1822 if issues_container is not None:
@@ -1819,7 +1830,7 b" def render(source, renderer='rst', menti"
1819 elif renderer == 'markdown':
1830 elif renderer == 'markdown':
1820 if repo_name:
1831 if repo_name:
1821 # process patterns on comments if we pass in repo name
1832 # process patterns on comments if we pass in repo name
1822 source, issues = process_patterns(
1833 source, issues, errors = process_patterns(
1823 source, repo_name, link_format='markdown',
1834 source, repo_name, link_format='markdown',
1824 active_entries=active_pattern_entries)
1835 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1836 if issues_container is not None:
@@ -57,7 +57,43 b' FILEMODE_DEFAULT = 0o100644'
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 EMPTY_COMMIT_ID = '0' * 40
59
59
60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
62
63 class Reference(_Reference):
64
65 @property
66 def branch(self):
67 if self.type == 'branch':
68 return self.name
69
70 @property
71 def bookmark(self):
72 if self.type == 'book':
73 return self.name
74
75
76 def unicode_to_reference(raw):
77 """
78 Convert a unicode (or string) to a reference object.
79 If unicode evaluates to False it returns None.
80 """
81 if raw:
82 refs = raw.split(':')
83 return Reference(*refs)
84 else:
85 return None
86
87
88 def reference_to_unicode(ref):
89 """
90 Convert a reference object to unicode.
91 If reference is None it returns None.
92 """
93 if ref:
94 return u':'.join(ref)
95 else:
96 return None
61
97
62
98
63 class MergeFailureReason(object):
99 class MergeFailureReason(object):
@@ -25,7 +25,7 b' import collections'
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@@ -383,15 +383,14 b' class ChangesetStatusModel(BaseModel):'
383 pull_request.source_repo,
383 pull_request.source_repo,
384 pull_request=pull_request,
384 pull_request=pull_request,
385 with_revisions=True)
385 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
386
389
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
390 def calculated_review_status(self, pull_request):
388
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 """
391 """
391 calculate pull request status based on reviewers, it should be a list
392 calculate pull request status based on reviewers, it should be a list
392 of two element lists.
393 of two element lists.
393
394 :param reviewers_statuses:
395 """
394 """
396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
395 reviewers = self.reviewers_statuses(pull_request)
397 return self.calculate_status(reviewers)
396 return self.calculate_status(reviewers)
@@ -399,7 +399,7 b' class CommentsModel(BaseModel):'
399 recipients += [pull_request_obj.author]
399 recipients += [pull_request_obj.author]
400
400
401 # add the reviewers to notification
401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
403
403
404 pr_target_repo = pull_request_obj.target_repo
404 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
405 pr_source_repo = pull_request_obj.source_repo
@@ -436,9 +436,8 b' class CommentsModel(BaseModel):'
436 'thread_ids': [pr_url, pr_comment_url],
436 'thread_ids': [pr_url, pr_comment_url],
437 })
437 })
438
438
439 if send_email:
439 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440
441 if send_email:
442 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
443 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
444 notification_type, **kwargs)
443 notification_type, **kwargs)
@@ -463,55 +462,11 b' class CommentsModel(BaseModel):'
463 else:
462 else:
464 action = 'repo.commit.comment.create'
463 action = 'repo.commit.comment.create'
465
464
466 comment_id = comment.comment_id
467 comment_data = comment.get_api_data()
465 comment_data = comment.get_api_data()
468
466
469 self._log_audit_action(
467 self._log_audit_action(
470 action, {'data': comment_data}, auth_user, comment)
468 action, {'data': comment_data}, auth_user, comment)
471
469
472 channel = None
473 if commit_obj:
474 repo_name = repo.repo_name
475 channel = u'/repo${}$/commit/{}'.format(
476 repo_name,
477 commit_obj.raw_id
478 )
479 elif pull_request_obj:
480 repo_name = pr_target_repo.repo_name
481 channel = u'/repo${}$/pr/{}'.format(
482 repo_name,
483 pull_request_obj.pull_request_id
484 )
485
486 if channel:
487 username = user.username
488 message = '<strong>{}</strong> {} #{}, {}'
489 message = message.format(
490 username,
491 _('posted a new comment'),
492 comment_id,
493 _('Refresh the page to see new comments.'))
494
495 message_obj = {
496 'message': message,
497 'level': 'success',
498 'topic': '/notifications'
499 }
500
501 channelstream.post_message(
502 channel, message_obj, user.username,
503 registry=get_current_registry())
504
505 message_obj = {
506 'message': None,
507 'user': username,
508 'comment_id': comment_id,
509 'topic': '/comment'
510 }
511 channelstream.post_message(
512 channel, message_obj, user.username,
513 registry=get_current_registry())
514
515 return comment
470 return comment
516
471
517 def edit(self, comment_id, text, auth_user, version):
472 def edit(self, comment_id, text, auth_user, version):
@@ -586,17 +541,20 b' class CommentsModel(BaseModel):'
586
541
587 return comment
542 return comment
588
543
589 def get_all_comments(self, repo_id, revision=None, pull_request=None):
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
590 q = ChangesetComment.query()\
545 q = ChangesetComment.query()\
591 .filter(ChangesetComment.repo_id == repo_id)
546 .filter(ChangesetComment.repo_id == repo_id)
592 if revision:
547 if revision:
593 q = q.filter(ChangesetComment.revision == revision)
548 q = q.filter(ChangesetComment.revision == revision)
594 elif pull_request:
549 elif pull_request:
595 pull_request = self.__get_pull_request(pull_request)
550 pull_request = self.__get_pull_request(pull_request)
596 q = q.filter(ChangesetComment.pull_request == pull_request)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
597 else:
552 else:
598 raise Exception('Please specify commit or pull_request')
553 raise Exception('Please specify commit or pull_request')
599 q = q.order_by(ChangesetComment.created_on)
554 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
556 return q.count()
557
600 return q.all()
558 return q.all()
601
559
602 def get_url(self, comment, request=None, permalink=False, anchor=None):
560 def get_url(self, comment, request=None, permalink=False, anchor=None):
@@ -56,7 +56,8 b' from webhelpers2.text import remove_form'
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
@@ -3773,12 +3774,12 b' class ChangesetComment(Base, BaseModel):'
3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775
3776
3776 author = relationship('User', lazy='joined')
3777 author = relationship('User', lazy='select')
3777 repo = relationship('Repository')
3778 repo = relationship('Repository')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3779 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3779 pull_request = relationship('PullRequest', lazy='joined')
3780 pull_request = relationship('PullRequest', lazy='select')
3780 pull_request_version = relationship('PullRequestVersion')
3781 pull_request_version = relationship('PullRequestVersion', lazy='select')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3782 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3782
3783
3783 @classmethod
3784 @classmethod
3784 def get_users(cls, revision=None, pull_request_id=None):
3785 def get_users(cls, revision=None, pull_request_id=None):
@@ -3983,10 +3984,10 b' class ChangesetStatus(Base, BaseModel):'
3983 version = Column('version', Integer(), nullable=False, default=0)
3984 version = Column('version', Integer(), nullable=False, default=0)
3984 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3985 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3985
3986
3986 author = relationship('User', lazy='joined')
3987 author = relationship('User', lazy='select')
3987 repo = relationship('Repository')
3988 repo = relationship('Repository', lazy='select')
3988 comment = relationship('ChangesetComment', lazy='joined')
3989 comment = relationship('ChangesetComment', lazy='select')
3989 pull_request = relationship('PullRequest', lazy='joined')
3990 pull_request = relationship('PullRequest', lazy='select')
3990
3991
3991 def __unicode__(self):
3992 def __unicode__(self):
3992 return u"<%s('%s[v%s]:%s')>" % (
3993 return u"<%s('%s[v%s]:%s')>" % (
@@ -4248,26 +4249,11 b' class _PullRequestBase(BaseModel):'
4248
4249
4249 @staticmethod
4250 @staticmethod
4250 def unicode_to_reference(raw):
4251 def unicode_to_reference(raw):
4251 """
4252 return unicode_to_reference(raw)
4252 Convert a unicode (or string) to a reference object.
4253 If unicode evaluates to False it returns None.
4254 """
4255 if raw:
4256 refs = raw.split(':')
4257 return Reference(*refs)
4258 else:
4259 return None
4260
4253
4261 @staticmethod
4254 @staticmethod
4262 def reference_to_unicode(ref):
4255 def reference_to_unicode(ref):
4263 """
4256 return reference_to_unicode(ref)
4264 Convert a reference object to unicode.
4265 If reference is None it returns None.
4266 """
4267 if ref:
4268 return u':'.join(ref)
4269 else:
4270 return None
4271
4257
4272 def get_api_data(self, with_merge_state=True):
4258 def get_api_data(self, with_merge_state=True):
4273 from rhodecode.model.pull_request import PullRequestModel
4259 from rhodecode.model.pull_request import PullRequestModel
@@ -4465,6 +4451,37 b' class PullRequest(Base, _PullRequestBase'
4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4451 from rhodecode.model.changeset_status import ChangesetStatusModel
4466 return ChangesetStatusModel().reviewers_statuses(self)
4452 return ChangesetStatusModel().reviewers_statuses(self)
4467
4453
4454 def get_pull_request_reviewers(self, role=None):
4455 qry = PullRequestReviewers.query()\
4456 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4457 if role:
4458 qry = qry.filter(PullRequestReviewers.role == role)
4459
4460 return qry.all()
4461
4462 @property
4463 def reviewers_count(self):
4464 qry = PullRequestReviewers.query()\
4465 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4466 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4467 return qry.count()
4468
4469 @property
4470 def observers_count(self):
4471 qry = PullRequestReviewers.query()\
4472 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4473 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4474 return qry.count()
4475
4476 def observers(self):
4477 qry = PullRequestReviewers.query()\
4478 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4479 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4480 .all()
4481
4482 for entry in qry:
4483 yield entry, entry.user
4484
4468 @property
4485 @property
4469 def workspace_id(self):
4486 def workspace_id(self):
4470 from rhodecode.model.pull_request import PullRequestModel
4487 from rhodecode.model.pull_request import PullRequestModel
@@ -4512,6 +4529,9 b' class PullRequestVersion(Base, _PullRequ'
4512 @property
4529 @property
4513 def reviewers(self):
4530 def reviewers(self):
4514 return self.pull_request.reviewers
4531 return self.pull_request.reviewers
4532 @property
4533 def reviewers(self):
4534 return self.pull_request.reviewers
4515
4535
4516 @property
4536 @property
4517 def versions(self):
4537 def versions(self):
@@ -4530,6 +4550,9 b' class PullRequestVersion(Base, _PullRequ'
4530 def reviewers_statuses(self):
4550 def reviewers_statuses(self):
4531 return self.pull_request.reviewers_statuses()
4551 return self.pull_request.reviewers_statuses()
4532
4552
4553 def observers(self):
4554 return self.pull_request.observers()
4555
4533
4556
4534 class PullRequestReviewers(Base, BaseModel):
4557 class PullRequestReviewers(Base, BaseModel):
4535 __tablename__ = 'pull_request_reviewers'
4558 __tablename__ = 'pull_request_reviewers'
@@ -4538,6 +4561,7 b' class PullRequestReviewers(Base, BaseMod'
4538 )
4561 )
4539 ROLE_REVIEWER = u'reviewer'
4562 ROLE_REVIEWER = u'reviewer'
4540 ROLE_OBSERVER = u'observer'
4563 ROLE_OBSERVER = u'observer'
4564 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4541
4565
4542 @hybrid_property
4566 @hybrid_property
4543 def reasons(self):
4567 def reasons(self):
@@ -4589,6 +4613,15 b' class PullRequestReviewers(Base, BaseMod'
4589
4613
4590 return user_group_data
4614 return user_group_data
4591
4615
4616 @classmethod
4617 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4618 qry = PullRequestReviewers.query()\
4619 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4620 if role:
4621 qry = qry.filter(PullRequestReviewers.role == role)
4622
4623 return qry.all()
4624
4592 def __unicode__(self):
4625 def __unicode__(self):
4593 return u"<%s('id:%s')>" % (self.__class__.__name__,
4626 return u"<%s('id:%s')>" % (self.__class__.__name__,
4594 self.pull_requests_reviewers_id)
4627 self.pull_requests_reviewers_id)
@@ -4954,16 +4987,21 b' class RepoReviewRuleUser(Base, BaseModel'
4954 __table_args__ = (
4987 __table_args__ = (
4955 base_table_args
4988 base_table_args
4956 )
4989 )
4990 ROLE_REVIEWER = u'reviewer'
4991 ROLE_OBSERVER = u'observer'
4992 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4957
4993
4958 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4994 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4959 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4995 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4996 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4961 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4997 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4998 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4962 user = relationship('User')
4999 user = relationship('User')
4963
5000
4964 def rule_data(self):
5001 def rule_data(self):
4965 return {
5002 return {
4966 'mandatory': self.mandatory
5003 'mandatory': self.mandatory,
5004 'role': self.role,
4967 }
5005 }
4968
5006
4969
5007
@@ -4974,17 +5012,22 b' class RepoReviewRuleUserGroup(Base, Base'
4974 )
5012 )
4975
5013
4976 VOTE_RULE_ALL = -1
5014 VOTE_RULE_ALL = -1
5015 ROLE_REVIEWER = u'reviewer'
5016 ROLE_OBSERVER = u'observer'
5017 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4977
5018
4978 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5019 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4979 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5020 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4980 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
5021 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4981 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5022 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5023 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4982 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5024 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4983 users_group = relationship('UserGroup')
5025 users_group = relationship('UserGroup')
4984
5026
4985 def rule_data(self):
5027 def rule_data(self):
4986 return {
5028 return {
4987 'mandatory': self.mandatory,
5029 'mandatory': self.mandatory,
5030 'role': self.role,
4988 'vote_rule': self.vote_rule
5031 'vote_rule': self.vote_rule
4989 }
5032 }
4990
5033
@@ -601,6 +601,14 b' def PullRequestForm(localizer, repo_id):'
601 reasons = All()
601 reasons = All()
602 rules = All(v.UniqueList(localizer, convert=int)())
602 rules = All(v.UniqueList(localizer, convert=int)())
603 mandatory = v.StringBoolean()
603 mandatory = v.StringBoolean()
604 role = v.String(if_missing='reviewer')
605
606 class ObserverForm(formencode.Schema):
607 user_id = v.Int(not_empty=True)
608 reasons = All()
609 rules = All(v.UniqueList(localizer, convert=int)())
610 mandatory = v.StringBoolean()
611 role = v.String(if_missing='observer')
604
612
605 class _PullRequestForm(formencode.Schema):
613 class _PullRequestForm(formencode.Schema):
606 allow_extra_fields = True
614 allow_extra_fields = True
@@ -614,6 +622,7 b' def PullRequestForm(localizer, repo_id):'
614 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
615 v.UniqueList(localizer)(not_empty=True))
623 v.UniqueList(localizer)(not_empty=True))
616 review_members = formencode.ForEach(ReviewerForm())
624 review_members = formencode.ForEach(ReviewerForm())
625 observer_members = formencode.ForEach(ObserverForm())
617 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
618 pullrequest_desc = v.UnicodeString(strip=True, required=False)
627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
619 description_renderer = v.UnicodeString(strip=True, required=False)
628 description_renderer = v.UnicodeString(strip=True, required=False)
@@ -154,28 +154,56 b' def get_diff_info('
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 commits = target_scm.compare(
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author"])
160 pre_load=["author", "date", "message"]
161 )
160
162
161 for commit in commits:
163 for commit in _compare_data:
162 user = User.get_from_cs_author(commit.author)
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
167 author=commit.author,
168 date=commit.date,
169 message=commit.message,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
172 )
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
163 if user and user not in commit_authors:
175 if user and user not in commit_authors:
164 commit_authors.append(user)
176 commit_authors.append(user)
165
177
166 # lines
178 # lines
167 if get_authors:
179 if get_authors:
180 log.debug('Calculating authors of changed files')
168 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
169
182
170 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
184
171 try:
185 try:
172 node = target_commit.get_node(fname)
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
173 except Exception:
187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
174 continue
189 continue
175
190
176 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
177 continue
192 continue
178
193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
196 author = node.last_commit.author
197 email = node.last_commit.author_email
198
199 user = User.get_from_cs_author(author)
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
205 continue
206
179 for annotation in node.annotate:
207 for annotation in node.annotate:
180 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
181 if line_no in lines:
209 if line_no in lines:
@@ -190,6 +218,8 b' def get_diff_info('
190 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
192
220
221 log.debug('Default reviewers processing finished')
222
193 return {
223 return {
194 'commits': commits,
224 'commits': commits,
195 'files': all_files_changes,
225 'files': all_files_changes,
@@ -260,10 +290,16 b' class PullRequestModel(BaseModel):'
260 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
261 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
262
292
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
298 ]
299
263 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
264 reviewer = user.user_id in [x.user_id for x in
301 return self.check_user_update(pull_request, user, api) \
265 pull_request.reviewers]
302 or self.is_user_reviewer(pull_request, user)
266 return self.check_user_update(pull_request, user, api) or reviewer
267
303
268 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
269 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
@@ -575,7 +611,7 b' class PullRequestModel(BaseModel):'
575 pull_request_display_obj, at_version
611 pull_request_display_obj, at_version
576
612
577 def create(self, created_by, source_repo, source_ref, target_repo,
613 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, title, description=None,
614 target_ref, revisions, reviewers, observers, title, description=None,
579 common_ancestor_id=None,
615 common_ancestor_id=None,
580 description_renderer=None,
616 description_renderer=None,
581 reviewer_data=None, translator=None, auth_user=None):
617 reviewer_data=None, translator=None, auth_user=None):
@@ -606,7 +642,7 b' class PullRequestModel(BaseModel):'
606 reviewer_ids = set()
642 reviewer_ids = set()
607 # members / reviewers
643 # members / reviewers
608 for reviewer_object in reviewers:
644 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
645 user_id, reasons, mandatory, role, rules = reviewer_object
610 user = self._get_user(user_id)
646 user = self._get_user(user_id)
611
647
612 # skip duplicates
648 # skip duplicates
@@ -620,6 +656,7 b' class PullRequestModel(BaseModel):'
620 reviewer.pull_request = pull_request
656 reviewer.pull_request = pull_request
621 reviewer.reasons = reasons
657 reviewer.reasons = reasons
622 reviewer.mandatory = mandatory
658 reviewer.mandatory = mandatory
659 reviewer.role = role
623
660
624 # NOTE(marcink): pick only first rule for now
661 # NOTE(marcink): pick only first rule for now
625 rule_id = list(rules)[0] if rules else None
662 rule_id = list(rules)[0] if rules else None
@@ -653,6 +690,33 b' class PullRequestModel(BaseModel):'
653 Session().add(reviewer)
690 Session().add(reviewer)
654 Session().flush()
691 Session().flush()
655
692
693 for observer_object in observers:
694 user_id, reasons, mandatory, role, rules = observer_object
695 user = self._get_user(user_id)
696
697 # skip duplicates from reviewers
698 if user.user_id in reviewer_ids:
699 continue
700
701 #reviewer_ids.add(user.user_id)
702
703 observer = PullRequestReviewers()
704 observer.user = user
705 observer.pull_request = pull_request
706 observer.reasons = reasons
707 observer.mandatory = mandatory
708 observer.role = role
709
710 # NOTE(marcink): pick only first rule for now
711 rule_id = list(rules)[0] if rules else None
712 rule = RepoReviewRule.get(rule_id) if rule_id else None
713 if rule:
714 # TODO(marcink): do we need this for observers ??
715 pass
716
717 Session().add(observer)
718 Session().flush()
719
656 # Set approval status to "Under Review" for all commits which are
720 # Set approval status to "Under Review" for all commits which are
657 # part of this pull request.
721 # part of this pull request.
658 ChangesetStatusModel().set_status(
722 ChangesetStatusModel().set_status(
@@ -678,7 +742,7 b' class PullRequestModel(BaseModel):'
678 MergeCheck.validate(
742 MergeCheck.validate(
679 pull_request, auth_user=auth_user, translator=translator)
743 pull_request, auth_user=auth_user, translator=translator)
680
744
681 self.notify_reviewers(pull_request, reviewer_ids)
745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
683
747
684 creation_data = pull_request.get_api_data(with_merge_state=False)
748 creation_data = pull_request.get_api_data(with_merge_state=False)
@@ -1204,23 +1268,25 b' class PullRequestModel(BaseModel):'
1204
1268
1205 :param pull_request: the pr to update
1269 :param pull_request: the pr to update
1206 :param reviewer_data: list of tuples
1270 :param reviewer_data: list of tuples
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1272 :param user: current use who triggers this action
1208 """
1273 """
1274
1209 pull_request = self.__get_pull_request(pull_request)
1275 pull_request = self.__get_pull_request(pull_request)
1210 if pull_request.is_closed():
1276 if pull_request.is_closed():
1211 raise ValueError('This pull request is closed')
1277 raise ValueError('This pull request is closed')
1212
1278
1213 reviewers = {}
1279 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1215 if isinstance(user_id, (int, compat.string_types)):
1281 if isinstance(user_id, (int, compat.string_types)):
1216 user_id = self._get_user(user_id).user_id
1282 user_id = self._get_user(user_id).user_id
1217 reviewers[user_id] = {
1283 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1219
1285
1220 reviewers_ids = set(reviewers.keys())
1286 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1222 .filter(PullRequestReviewers.pull_request ==
1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1223 pull_request).all()
1289
1224 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225
1291
1226 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
@@ -1241,16 +1307,19 b' class PullRequestModel(BaseModel):'
1241 reviewer.reasons = reviewers[uid]['reasons']
1307 reviewer.reasons = reviewers[uid]['reasons']
1242 # NOTE(marcink): mandatory shouldn't be changed now
1308 # NOTE(marcink): mandatory shouldn't be changed now
1243 # reviewer.mandatory = reviewers[uid]['reasons']
1309 # reviewer.mandatory = reviewers[uid]['reasons']
1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1244 Session().add(reviewer)
1312 Session().add(reviewer)
1245 added_audit_reviewers.append(reviewer.get_dict())
1313 added_audit_reviewers.append(reviewer.get_dict())
1246
1314
1247 for uid in ids_to_remove:
1315 for uid in ids_to_remove:
1248 changed = True
1316 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1250 # that prevents and fixes cases that we added the same reviewer twice.
1318 # This is an edge case that handles previous state of having the same reviewer twice.
1251 # this CAN happen due to the lack of DB checks
1319 # this CAN happen due to the lack of DB checks
1252 reviewers = PullRequestReviewers.query()\
1320 reviewers = PullRequestReviewers.query()\
1253 .filter(PullRequestReviewers.user_id == uid,
1321 .filter(PullRequestReviewers.user_id == uid,
1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1254 PullRequestReviewers.pull_request == pull_request)\
1323 PullRequestReviewers.pull_request == pull_request)\
1255 .all()
1324 .all()
1256
1325
@@ -1273,7 +1342,90 b' class PullRequestModel(BaseModel):'
1273 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 user, pull_request)
1343 user, pull_request)
1275
1344
1276 self.notify_reviewers(pull_request, ids_to_add)
1345 self.notify_reviewers(pull_request, ids_to_add, user)
1346 return ids_to_add, ids_to_remove
1347
1348 def update_observers(self, pull_request, observer_data, user):
1349 """
1350 Update the observers in the pull request
1351
1352 :param pull_request: the pr to update
1353 :param observer_data: list of tuples
1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1355 :param user: current use who triggers this action
1356 """
1357 pull_request = self.__get_pull_request(pull_request)
1358 if pull_request.is_closed():
1359 raise ValueError('This pull request is closed')
1360
1361 observers = {}
1362 for user_id, reasons, mandatory, role, rules in observer_data:
1363 if isinstance(user_id, (int, compat.string_types)):
1364 user_id = self._get_user(user_id).user_id
1365 observers[user_id] = {
1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1367
1368 observers_ids = set(observers.keys())
1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1371
1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1373
1374 ids_to_add = observers_ids.difference(current_observers_ids)
1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1376
1377 log.debug("Adding %s observer", ids_to_add)
1378 log.debug("Removing %s observer", ids_to_remove)
1379 changed = False
1380 added_audit_observers = []
1381 removed_audit_observers = []
1382
1383 for uid in ids_to_add:
1384 changed = True
1385 _usr = self._get_user(uid)
1386 observer = PullRequestReviewers()
1387 observer.user = _usr
1388 observer.pull_request = pull_request
1389 observer.reasons = observers[uid]['reasons']
1390 # NOTE(marcink): mandatory shouldn't be changed now
1391 # observer.mandatory = observer[uid]['reasons']
1392
1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1395 Session().add(observer)
1396 added_audit_observers.append(observer.get_dict())
1397
1398 for uid in ids_to_remove:
1399 changed = True
1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1401 # This is an edge case that handles previous state of having the same reviewer twice.
1402 # this CAN happen due to the lack of DB checks
1403 observers = PullRequestReviewers.query()\
1404 .filter(PullRequestReviewers.user_id == uid,
1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1406 PullRequestReviewers.pull_request == pull_request)\
1407 .all()
1408
1409 for obj in observers:
1410 added_audit_observers.append(obj.get_dict())
1411 Session().delete(obj)
1412
1413 if changed:
1414 Session().expire_all()
1415 pull_request.updated_on = datetime.datetime.now()
1416 Session().add(pull_request)
1417
1418 # finally store audit logs
1419 for user_data in added_audit_observers:
1420 self._log_audit_action(
1421 'repo.pull_request.observer.add', {'data': user_data},
1422 user, pull_request)
1423 for user_data in removed_audit_observers:
1424 self._log_audit_action(
1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1426 user, pull_request)
1427
1428 self.notify_observers(pull_request, ids_to_add, user)
1277 return ids_to_add, ids_to_remove
1429 return ids_to_add, ids_to_remove
1278
1430
1279 def get_url(self, pull_request, request=None, permalink=False):
1431 def get_url(self, pull_request, request=None, permalink=False):
@@ -1301,16 +1453,16 b' class PullRequestModel(BaseModel):'
1301 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303
1455
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1305 # notification to reviewers
1457 # notification to reviewers/observers
1306 if not reviewers_ids:
1458 if not user_ids:
1307 return
1459 return
1308
1460
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1310
1462
1311 pull_request_obj = pull_request
1463 pull_request_obj = pull_request
1312 # get the current participants of this pull request
1464 # get the current participants of this pull request
1313 recipients = reviewers_ids
1465 recipients = user_ids
1314 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315
1467
1316 pr_source_repo = pull_request_obj.source_repo
1468 pr_source_repo = pull_request_obj.source_repo
@@ -1332,8 +1484,10 b' class PullRequestModel(BaseModel):'
1332 (x.raw_id, x.message)
1484 (x.raw_id, x.message)
1333 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334
1486
1487 current_rhodecode_user = user
1335 kwargs = {
1488 kwargs = {
1336 'user': pull_request.author,
1489 'user': current_rhodecode_user,
1490 'pull_request_author': pull_request.author,
1337 'pull_request': pull_request_obj,
1491 'pull_request': pull_request_obj,
1338 'pull_request_commits': pull_request_commits,
1492 'pull_request_commits': pull_request_commits,
1339
1493
@@ -1345,6 +1499,7 b' class PullRequestModel(BaseModel):'
1345
1499
1346 'pull_request_url': pr_url,
1500 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1501 'thread_ids': [pr_url],
1502 'user_role': role
1348 }
1503 }
1349
1504
1350 # pre-generate the subject for notification itself
1505 # pre-generate the subject for notification itself
@@ -1353,7 +1508,7 b' class PullRequestModel(BaseModel):'
1353
1508
1354 # create notification objects, and emails
1509 # create notification objects, and emails
1355 NotificationModel().create(
1510 NotificationModel().create(
1356 created_by=pull_request.author,
1511 created_by=current_rhodecode_user,
1357 notification_subject=subject,
1512 notification_subject=subject,
1358 notification_body=body_plaintext,
1513 notification_body=body_plaintext,
1359 notification_type=notification_type,
1514 notification_type=notification_type,
@@ -1361,11 +1516,19 b' class PullRequestModel(BaseModel):'
1361 email_kwargs=kwargs,
1516 email_kwargs=kwargs,
1362 )
1517 )
1363
1518
1519 def notify_reviewers(self, pull_request, reviewers_ids, user):
1520 return self._notify_reviewers(pull_request, reviewers_ids,
1521 PullRequestReviewers.ROLE_REVIEWER, user)
1522
1523 def notify_observers(self, pull_request, observers_ids, user):
1524 return self._notify_reviewers(pull_request, observers_ids,
1525 PullRequestReviewers.ROLE_OBSERVER, user)
1526
1364 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1527 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1365 commit_changes, file_changes):
1528 commit_changes, file_changes):
1366
1529
1367 updating_user_id = updating_user.user_id
1530 updating_user_id = updating_user.user_id
1368 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1531 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1369 # NOTE(marcink): send notification to all other users except to
1532 # NOTE(marcink): send notification to all other users except to
1370 # person who updated the PR
1533 # person who updated the PR
1371 recipients = reviewers.difference(set([updating_user_id]))
1534 recipients = reviewers.difference(set([updating_user_id]))
@@ -1874,11 +2037,13 b' class PullRequestModel(BaseModel):'
1874 try:
2037 try:
1875 from rc_reviewers.utils import get_default_reviewers_data
2038 from rc_reviewers.utils import get_default_reviewers_data
1876 from rc_reviewers.utils import validate_default_reviewers
2039 from rc_reviewers.utils import validate_default_reviewers
2040 from rc_reviewers.utils import validate_observers
1877 except ImportError:
2041 except ImportError:
1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
2042 from rhodecode.apps.repository.utils import get_default_reviewers_data
1879 from rhodecode.apps.repository.utils import validate_default_reviewers
2043 from rhodecode.apps.repository.utils import validate_default_reviewers
2044 from rhodecode.apps.repository.utils import validate_observers
1880
2045
1881 return get_default_reviewers_data, validate_default_reviewers
2046 return get_default_reviewers_data, validate_default_reviewers, validate_observers
1882
2047
1883
2048
1884 class MergeCheck(object):
2049 class MergeCheck(object):
@@ -42,7 +42,7 b' from rhodecode.lib.exceptions import ('
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43 from rhodecode.model import BaseModel
43 from rhodecode.model import BaseModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
46 UserEmailMap, UserIpMap, UserLog)
46 UserEmailMap, UserIpMap, UserLog)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.auth_token import AuthTokenModel
@@ -96,7 +96,11 b' class UserModel(BaseModel):'
96 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
97 )
97 )
98 )
98 )
99 # sort by len to have top most matches first
100 query = query.order_by(func.length(User.username))\
101 .order_by(User.username)
99 query = query.limit(limit)
102 query = query.limit(limit)
103
100 users = query.all()
104 users = query.all()
101
105
102 _users = [
106 _users = [
@@ -21,12 +21,17 b''
21 import colander
21 import colander
22 from rhodecode.model.validation_schema import validators, preparers, types
22 from rhodecode.model.validation_schema import validators, preparers, types
23
23
24 DEFAULT_ROLE = 'reviewer'
25 VALID_ROLES = ['reviewer', 'observer']
26
24
27
25 class ReviewerSchema(colander.MappingSchema):
28 class ReviewerSchema(colander.MappingSchema):
26 username = colander.SchemaNode(types.StrOrIntType())
29 username = colander.SchemaNode(types.StrOrIntType())
27 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
30 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
28 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
31 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29 rules = colander.SchemaNode(colander.List(), missing=[])
32 rules = colander.SchemaNode(colander.List(), missing=[])
33 role = colander.SchemaNode(colander.String(), missing=DEFAULT_ROLE,
34 validator=colander.OneOf(VALID_ROLES))
30
35
31
36
32 class ReviewerListSchema(colander.SequenceSchema):
37 class ReviewerListSchema(colander.SequenceSchema):
@@ -97,6 +97,7 b''
97 <li>The server is being restarted.</li>
97 <li>The server is being restarted.</li>
98 <li>The server is overloaded.</li>
98 <li>The server is overloaded.</li>
99 <li>The link may be incorrect.</li>
99 <li>The link may be incorrect.</li>
100 <li><a onclick="window.location.reload()">Reload page</a></li>
100 </ul>
101 </ul>
101 </div>
102 </div>
102 <div class="inner-column">
103 <div class="inner-column">
@@ -374,9 +374,6 b' ul.auth_plugins {'
374 background-color: @grey6;
374 background-color: @grey6;
375 }
375 }
376
376
377 .td-status {
378 padding-left: .5em;
379 }
380 .log-container .truncate {
377 .log-container .truncate {
381 height: 2.75em;
378 height: 2.75em;
382 white-space: pre-line;
379 white-space: pre-line;
@@ -384,6 +381,10 b' ul.auth_plugins {'
384 table.rctable .user {
381 table.rctable .user {
385 padding-left: 0;
382 padding-left: 0;
386 }
383 }
384 .td-status {
385 padding: 0 0px 0px 10px;
386 width: 15px;
387 }
387 table.rctable {
388 table.rctable {
388 td.td-description,
389 td.td-description,
389 .rc-user {
390 .rc-user {
@@ -494,7 +495,8 b' ul.auth_plugins {'
494 padding-top: 10px;
495 padding-top: 10px;
495 }
496 }
496
497
497 #add_reviewer_input {
498 #add_reviewer_input,
499 #add_observer_input {
498 padding-top: 10px
500 padding-top: 10px
499 }
501 }
500
502
@@ -1700,8 +1702,33 b' table.group_members {'
1700 }
1702 }
1701
1703
1702 .reviewer_ac .ac-input {
1704 .reviewer_ac .ac-input {
1705 width: 98%;
1706 margin-bottom: 1em;
1707 }
1708
1709 .observer_ac .ac-input {
1710 width: 98%;
1711 margin-bottom: 1em;
1712 }
1713
1714 .rule-table {
1703 width: 100%;
1715 width: 100%;
1704 margin-bottom: 1em;
1716 }
1717
1718 .rule-table td {
1719
1720 }
1721
1722 .rule-table .td-role {
1723 width: 100px
1724 }
1725
1726 .rule-table .td-mandatory {
1727 width: 100px
1728 }
1729
1730 .rule-table .td-group-votes {
1731 width: 150px
1705 }
1732 }
1706
1733
1707 .compare_view_commits tr{
1734 .compare_view_commits tr{
@@ -2635,6 +2662,7 b' h3.files_location{'
2635 li {
2662 li {
2636 list-style-type: none
2663 list-style-type: none
2637 }
2664 }
2665
2638 }
2666 }
2639
2667
2640 .grid-filter-box-icon {
2668 .grid-filter-box-icon {
@@ -477,23 +477,3 b''
477 }
477 }
478
478
479 }
479 }
480
481 .rctable.repo_summary {
482 border: 1px solid #eaeaea;
483 border-radius: 2px;
484 border-collapse: inherit;
485 border-bottom: 0;
486
487 th {
488 background: @grey7;
489 border-bottom: 0;
490 }
491
492 td {
493 border-color: #eaeaea;
494 }
495
496 td.td-status {
497 padding: 0 0 0 10px;
498 }
499 }
@@ -4,6 +4,25 b''
4 // see style guide documentation for guidelines.
4 // see style guide documentation for guidelines.
5
5
6 // TABLES
6 // TABLES
7 table.rctable.table-bordered {
8 border: 1px solid #eaeaea;
9 border-radius: 2px;
10 border-collapse: inherit;
11 border-bottom: 0;
12
13 th {
14 background: @grey7;
15 border-bottom: 0;
16 }
17
18 td {
19 border-color: #eaeaea;
20 }
21
22 td.td-status {
23 padding: 0 0 0 10px;
24 }
25 }
7
26
8 .rctable,
27 .rctable,
9 table.rctable,
28 table.rctable,
@@ -306,12 +325,14 b' table.dataTable {'
306 }
325 }
307 }
326 }
308 }
327 }
328
309 .rctable.audit-log {
329 .rctable.audit-log {
310 td {
330 td {
311 vertical-align: top;
331 vertical-align: top;
312 }
332 }
313 }
333 }
314
334
335
315 // TRUNCATING
336 // TRUNCATING
316 // TODO: lisaq: should this possibly be moved out of tables.less?
337 // TODO: lisaq: should this possibly be moved out of tables.less?
317 // for truncated text
338 // for truncated text
@@ -426,15 +447,6 b' table.keyboard-mappings {'
426 }
447 }
427 }
448 }
428
449
429 // Pull Request List Table
430 #pull_request_list_table.dataTable {
431
432 //TODO: lisa: This needs to be removed once the description is adjusted
433 // for using an expand_commit button (see issue 765)
434 td {
435 vertical-align: middle;
436 }
437 }
438
450
439 // Settings (no border)
451 // Settings (no border)
440 table.rctable.dl-settings {
452 table.rctable.dl-settings {
@@ -484,9 +496,6 b' table.trending_language_tbl {'
484
496
485 // Changesets
497 // Changesets
486 #changesets.rctable {
498 #changesets.rctable {
487 th {
488 padding: 0 1em 0.65em 0;
489 }
490
499
491 // td must be fixed height for graph
500 // td must be fixed height for graph
492 td {
501 td {
@@ -344,6 +344,10 b' mark,'
344 width: 200px;
344 width: 200px;
345 }
345 }
346
346
347 #obj_count {
348 line-height: 34px;
349 }
350
347 }
351 }
348
352
349 #readme .title {
353 #readme .title {
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Delete',
35 'Delete': 'Delete',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Stop following this repository',
106 'Stop following this repository': 'Stop following this repository',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Submitting...',
108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Löschen',
35 'Delete': 'Löschen',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Stop following this repository',
106 'Stop following this repository': 'Stop following this repository',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Submitting...',
108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Delete',
35 'Delete': 'Delete',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Stop following this repository',
106 'Stop following this repository': 'Stop following this repository',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Submitting...',
108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Delete',
35 'Delete': 'Delete',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Stop following this repository',
106 'Stop following this repository': 'Stop following this repository',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Submitting...',
108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Supprimer',
35 'Delete': 'Supprimer',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Arrêter de suivre ce dépôt',
106 'Stop following this repository': 'Arrêter de suivre ce dépôt',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Envoi…',
108 'Submitting...': 'Envoi…',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Aggiungi un altro commento',
13 'Add another comment': 'Aggiungi un altro commento',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Elimina',
35 'Delete': 'Elimina',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'Nessun risultato',
70 'No results': 'Nessun risultato',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Smetti di seguire il repository',
106 'Stop following this repository': 'Smetti di seguire il repository',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Inoltro...',
108 'Submitting...': 'Inoltro...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'Al momento non ci sono richieste di PULL che richiedono il tuo intervento',
114 'There are currently no open pull requests requiring your participation.': 'Al momento non ci sono richieste di PULL che richiedono il tuo intervento',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': '別のコメントを追加',
13 'Add another comment': '別のコメントを追加',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': '選択したステータス ({0}) を元にコメントが自動的に設定されます...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': '選択したステータス ({0}) を元にコメントが自動的に設定されます...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': '削除',
35 'Delete': '削除',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'まだリポジトリグループがありません。',
68 'No repository groups available yet.': 'まだリポジトリグループがありません。',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': '結果がありません',
70 'No results': '結果がありません',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'まだタグがありません。',
73 'No tags available yet.': 'まだタグがありません。',
69 'No user groups available yet.': 'まだユーザーグループがありません。',
74 'No user groups available yet.': 'まだユーザーグループがありません。',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'このリポジトリのフォローをやめる',
106 'Stop following this repository': 'このリポジトリのフォローをやめる',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': '送信中...',
108 'Submitting...': '送信中...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -1,5 +1,7 b''
1 // AUTO GENERATED FILE FOR Babel JS-GETTEXT EXTRACTORS, DO NOT CHANGE
1 // AUTO GENERATED FILE FOR Babel JS-GETTEXT EXTRACTORS, DO NOT CHANGE
2 _gettext('(from usergroup {0})');
2 _gettext('(from usergroup {0})');
3 _gettext('<strong>, and {0} file</strong> changed.');
4 _gettext('<strong>, and {0} files</strong> changed.');
3 _gettext('<strong>{0} file</strong> changed, ');
5 _gettext('<strong>{0} file</strong> changed, ');
4 _gettext('<strong>{0} files</strong> changed, ');
6 _gettext('<strong>{0} files</strong> changed, ');
5 _gettext('Add another comment');
7 _gettext('Add another comment');
@@ -21,6 +23,8 b''
21 _gettext('Comment body was not changed.');
23 _gettext('Comment body was not changed.');
22 _gettext('Comment text will be set automatically based on currently selected status ({0}) ...');
24 _gettext('Comment text will be set automatically based on currently selected status ({0}) ...');
23 _gettext('Commit Authors are not allowed to be a reviewer.');
25 _gettext('Commit Authors are not allowed to be a reviewer.');
26 _gettext('Compare summary: <strong>{0} commit</strong>');
27 _gettext('Compare summary: <strong>{0} commits</strong>');
24 _gettext('Context file: ');
28 _gettext('Context file: ');
25 _gettext('Delete');
29 _gettext('Delete');
26 _gettext('Delete this comment?');
30 _gettext('Delete this comment?');
@@ -58,6 +62,7 b''
58 _gettext('No repository groups available yet.');
62 _gettext('No repository groups available yet.');
59 _gettext('No repository groups present.');
63 _gettext('No repository groups present.');
60 _gettext('No results');
64 _gettext('No results');
65 _gettext('No review rules set.');
61 _gettext('No ssh keys available yet.');
66 _gettext('No ssh keys available yet.');
62 _gettext('No tags available yet.');
67 _gettext('No tags available yet.');
63 _gettext('No user groups available yet.');
68 _gettext('No user groups available yet.');
@@ -95,11 +100,13 b''
95 _gettext('Stop following this repository');
100 _gettext('Stop following this repository');
96 _gettext('Stopped watching this repository');
101 _gettext('Stopped watching this repository');
97 _gettext('Submitting...');
102 _gettext('Submitting...');
103 _gettext('Switch target repository with the source.');
98 _gettext('Switch to chat');
104 _gettext('Switch to chat');
99 _gettext('Switch to comment');
105 _gettext('Switch to comment');
100 _gettext('TODO comment');
106 _gettext('TODO comment');
101 _gettext('TODO from comment {0} was fixed.');
107 _gettext('TODO from comment {0} was fixed.');
102 _gettext('There are currently no open pull requests requiring your participation.');
108 _gettext('There are currently no open pull requests requiring your participation.');
109 _gettext('There are no commits to merge.');
103 _gettext('There is a later version of file tree available. Click {0} to create a file at the latest tree.');
110 _gettext('There is a later version of file tree available. Click {0} to create a file at the latest tree.');
104 _gettext('There is an existing path `{0}` at this commit.');
111 _gettext('There is an existing path `{0}` at this commit.');
105 _gettext('This pull requests will consist of <strong>{0} commit</strong>.');
112 _gettext('This pull requests will consist of <strong>{0} commit</strong>.');
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Dodaj kolejny komentarz',
13 'Add another comment': 'Dodaj kolejny komentarz',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Usuń',
35 'Delete': 'Usuń',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Zakończyć obserwację tego repozytorium',
106 'Stop following this repository': 'Zakończyć obserwację tego repozytorium',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Przesyłanie...',
108 'Submitting...': 'Przesyłanie...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Adicionar outro comentário',
13 'Add another comment': 'Adicionar outro comentário',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Excluir',
35 'Delete': 'Excluir',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Parar de seguir este repositório',
106 'Stop following this repository': 'Parar de seguir este repositório',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Enviando...',
108 'Submitting...': 'Enviando...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Добавить другой комментарий',
13 'Add another comment': 'Добавить другой комментарий',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': 'Удалить',
35 'Delete': 'Удалить',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': 'Отменить наблюдение за репозиторием',
106 'Stop following this repository': 'Отменить наблюдение за репозиторием',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': 'Применение...',
108 'Submitting...': 'Применение...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 //JS translations map
6 //JS translations map
7 var _TM = {
7 var _TM = {
8 '(from usergroup {0})': '(from usergroup {0})',
8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 'Add another comment': 'Add another comment',
13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 'Comment body was not changed.': 'Comment body was not changed.',
29 'Comment body was not changed.': 'Comment body was not changed.',
28 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 'Context file: ': 'Context file: ',
34 'Context file: ': 'Context file: ',
31 'Delete': '删除',
35 'Delete': '删除',
32 'Delete this comment?': 'Delete this comment?',
36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 'No repository groups available yet.': 'No repository groups available yet.',
68 'No repository groups available yet.': 'No repository groups available yet.',
65 'No repository groups present.': 'No repository groups present.',
69 'No repository groups present.': 'No repository groups present.',
66 'No results': 'No results',
70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 'No ssh keys available yet.': 'No ssh keys available yet.',
72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 'No tags available yet.': 'No tags available yet.',
73 'No tags available yet.': 'No tags available yet.',
69 'No user groups available yet.': 'No user groups available yet.',
74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 'Stop following this repository': '停止关注该版本库',
106 'Stop following this repository': '停止关注该版本库',
102 'Stopped watching this repository': 'Stopped watching this repository',
107 'Stopped watching this repository': 'Stopped watching this repository',
103 'Submitting...': '提交中……',
108 'Submitting...': '提交中……',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 'Switch to chat': 'Switch to chat',
110 'Switch to chat': 'Switch to chat',
105 'Switch to comment': 'Switch to comment',
111 'Switch to comment': 'Switch to comment',
106 'TODO comment': 'TODO comment',
112 'TODO comment': 'TODO comment',
107 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -75,8 +75,8 b' var CommitsController = function () {'
75 height: height,
75 height: height,
76 x_step: x_step,
76 x_step: x_step,
77 y_step: 42,
77 y_step: 42,
78 dotRadius: 3.5,
78 dotRadius: 3.8,
79 lineWidth: 2.5
79 lineWidth: 2.8
80 };
80 };
81
81
82 var prevCommitsData = this.$graphCanvas.data('commits') || [];
82 var prevCommitsData = this.$graphCanvas.data('commits') || [];
@@ -98,11 +98,12 b' var CommitsController = function () {'
98
98
99 this.setLabelText(edgeData);
99 this.setLabelText(edgeData);
100
100
101 var padding = 90;
101 // main padding from top, aligns the first dot graph
102 var padding = 100;
102 if (prev_link) {
103 if (prev_link) {
103 padding += 34;
104 padding += 34;
105 }
104
106
105 }
106 $('#graph_nodes').css({'padding-top': padding});
107 $('#graph_nodes').css({'padding-top': padding});
107
108
108 $.each($('.message.truncate'), function(idx, value) {
109 $.each($('.message.truncate'), function(idx, value) {
@@ -94,21 +94,26 b' var getTitleAndDescription = function(so'
94 };
94 };
95
95
96
96
97 ReviewersController = function () {
97 window.ReviewersController = function () {
98 var self = this;
98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 this.$reviewRulesContainer = $('#review_rules');
100 this.$reviewRulesContainer = $('#review_rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
102 this.$userRule = $('.pr-user-rule-container');
102 this.forbidReviewUsers = undefined;
103 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
105
104 this.currentRequest = null;
106 this.currentRequest = null;
105 this.diffData = null;
107 this.diffData = null;
106 this.enabledRules = [];
108 this.enabledRules = [];
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
107
112
108 //dummy handler, we might register our own later
113 //dummy handler, we might register our own later
109 this.diffDataHandler = function(data){};
114 this.diffDataHandler = function (data) {};
110
115
111 this.defaultForbidReviewUsers = function () {
116 this.defaultForbidUsers = function () {
112 return [
117 return [
113 {
118 {
114 'username': 'default',
119 'username': 'default',
@@ -117,6 +122,9 b' ReviewersController = function () {'
117 ];
122 ];
118 };
123 };
119
124
125 // init default forbidden users
126 this.forbidUsers = this.defaultForbidUsers();
127
120 this.hideReviewRules = function () {
128 this.hideReviewRules = function () {
121 self.$reviewRulesContainer.hide();
129 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
130 $(self.$userRule.selector).hide();
@@ -133,11 +141,40 b' ReviewersController = function () {'
133 return '<div>- {0}</div>'.format(ruleText)
141 return '<div>- {0}</div>'.format(ruleText)
134 };
142 };
135
143
144 this.increaseCounter = function(role) {
145 if (role === self.ROLE_REVIEWER) {
146 var $elem = $('#reviewers-cnt')
147 var cnt = parseInt($elem.data('count') || 0)
148 cnt +=1
149 $elem.html(cnt);
150 $elem.data('count', cnt);
151 }
152 else if (role === self.ROLE_OBSERVER) {
153 var $elem = $('#observers-cnt');
154 var cnt = parseInt($elem.data('count') || 0)
155 cnt +=1
156 $elem.html(cnt);
157 $elem.data('count', cnt);
158 }
159 }
160
161 this.resetCounter = function () {
162 var $elem = $('#reviewers-cnt');
163
164 $elem.data('count', 0);
165 $elem.html(0);
166
167 var $elem = $('#observers-cnt');
168
169 $elem.data('count', 0);
170 $elem.html(0);
171 }
172
136 this.loadReviewRules = function (data) {
173 this.loadReviewRules = function (data) {
137 self.diffData = data;
174 self.diffData = data;
138
175
139 // reset forbidden Users
176 // reset forbidden Users
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
177 this.forbidUsers = self.defaultForbidUsers();
141
178
142 // reset state of review rules
179 // reset state of review rules
143 self.$rulesList.html('');
180 self.$rulesList.html('');
@@ -148,7 +185,7 b' ReviewersController = function () {'
148 self.addRule(
185 self.addRule(
149 _gettext('All reviewers must vote.'))
186 _gettext('All reviewers must vote.'))
150 );
187 );
151 return self.forbidReviewUsers
188 return self.forbidUsers
152 }
189 }
153
190
154 if (data.rules.voting !== undefined) {
191 if (data.rules.voting !== undefined) {
@@ -195,7 +232,7 b' ReviewersController = function () {'
195 }
232 }
196
233
197 if (data.rules.forbid_author_to_review) {
234 if (data.rules.forbid_author_to_review) {
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
235 self.forbidUsers.push(data.rules_data.pr_author);
199 self.$rulesList.append(
236 self.$rulesList.append(
200 self.addRule(
237 self.addRule(
201 _gettext('Author is not allowed to be a reviewer.'))
238 _gettext('Author is not allowed to be a reviewer.'))
@@ -206,9 +243,8 b' ReviewersController = function () {'
206
243
207 if (data.rules_data.forbidden_users) {
244 if (data.rules_data.forbidden_users) {
208 $.each(data.rules_data.forbidden_users, function (index, member_data) {
245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
209 self.forbidReviewUsers.push(member_data)
246 self.forbidUsers.push(member_data)
210 });
247 });
211
212 }
248 }
213
249
214 self.$rulesList.append(
250 self.$rulesList.append(
@@ -223,9 +259,31 b' ReviewersController = function () {'
223 _gettext('No review rules set.'))
259 _gettext('No review rules set.'))
224 }
260 }
225
261
226 return self.forbidReviewUsers
262 return self.forbidUsers
227 };
263 };
228
264
265 this.emptyTables = function () {
266 self.emptyReviewersTable();
267 self.emptyObserversTable();
268
269 // Also reset counters.
270 self.resetCounter();
271 }
272
273 this.emptyReviewersTable = function (withText) {
274 self.$reviewMembers.empty();
275 if (withText !== undefined) {
276 self.$reviewMembers.html(withText)
277 }
278 };
279
280 this.emptyObserversTable = function (withText) {
281 self.$observerMembers.empty();
282 if (withText !== undefined) {
283 self.$observerMembers.html(withText)
284 }
285 }
286
229 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230
288
231 if (self.currentRequest) {
289 if (self.currentRequest) {
@@ -233,19 +291,21 b' ReviewersController = function () {'
233 self.currentRequest.abort();
291 self.currentRequest.abort();
234 }
292 }
235
293
236 $('.calculate-reviewers').show();
294 self.$loadingIndicator.show();
237 // reset reviewer members
295
238 self.$reviewMembers.empty();
296 // reset reviewer/observe members
297 self.emptyTables();
239
298
240 prButtonLock(true, null, 'reviewers');
299 prButtonLock(true, null, 'reviewers');
241 $('#user').hide(); // hide user autocomplete before load
300 $('#user').hide(); // hide user autocomplete before load
301 $('#observer').hide(); //hide observer autocomplete before load
242
302
243 // lock PR button, so we cannot send PR before it's calculated
303 // lock PR button, so we cannot send PR before it's calculated
244 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245
305
246 if (sourceRef.length !== 3 || targetRef.length !== 3) {
306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 // don't load defaults in case we're missing some refs...
307 // don't load defaults in case we're missing some refs...
248 $('.calculate-reviewers').hide();
308 self.$loadingIndicator.hide();
249 return
309 return
250 }
310 }
251
311
@@ -253,9 +313,13 b' ReviewersController = function () {'
253 {
313 {
254 'repo_name': templateContext.repo_name,
314 'repo_name': templateContext.repo_name,
255 'source_repo': sourceRepo,
315 'source_repo': sourceRepo,
316 'source_ref_type': sourceRef[0],
317 'source_ref_name': sourceRef[1],
256 'source_ref': sourceRef[2],
318 'source_ref': sourceRef[2],
257 'target_repo': targetRepo,
319 'target_repo': targetRepo,
258 'target_ref': targetRef[2]
320 'target_ref': targetRef[2],
321 'target_ref_type': sourceRef[0],
322 'target_ref_name': sourceRef[1]
259 });
323 });
260
324
261 self.currentRequest = $.ajax({
325 self.currentRequest = $.ajax({
@@ -268,15 +332,23 b' ReviewersController = function () {'
268
332
269 // review rules
333 // review rules
270 self.loadReviewRules(data);
334 self.loadReviewRules(data);
271 self.handleDiffData(data["diff_info"]);
335 var diffHandled = self.handleDiffData(data["diff_info"]);
336 if (diffHandled === false) {
337 return
338 }
272
339
273 for (var i = 0; i < data.reviewers.length; i++) {
340 for (var i = 0; i < data.reviewers.length; i++) {
274 var reviewer = data.reviewers[i];
341 var reviewer = data.reviewers[i];
275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
342 // load reviewer rules from the repo data
343 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
276 }
344 }
277 $('.calculate-reviewers').hide();
345
346
347 self.$loadingIndicator.hide();
278 prButtonLock(false, null, 'reviewers');
348 prButtonLock(false, null, 'reviewers');
279 $('#user').show(); // show user autocomplete after load
349
350 $('#user').show(); // show user autocomplete before load
351 $('#observer').show(); // show observer autocomplete before load
280
352
281 var commitElements = data["diff_info"]['commits'];
353 var commitElements = data["diff_info"]['commits'];
282
354
@@ -292,7 +364,7 b' ReviewersController = function () {'
292
364
293 },
365 },
294 error: function (jqXHR, textStatus, errorThrown) {
366 error: function (jqXHR, textStatus, errorThrown) {
295 var prefix = "Loading diff and reviewers failed\n"
367 var prefix = "Loading diff and reviewers/observers failed\n"
296 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
368 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
297 ajaxErrorSwal(message);
369 ajaxErrorSwal(message);
298 }
370 }
@@ -301,7 +373,7 b' ReviewersController = function () {'
301 };
373 };
302
374
303 // check those, refactor
375 // check those, refactor
304 this.removeReviewMember = function (reviewer_id, mark_delete) {
376 this.removeMember = function (reviewer_id, mark_delete) {
305 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
377 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
306
378
307 if (typeof (mark_delete) === undefined) {
379 if (typeof (mark_delete) === undefined) {
@@ -312,6 +384,7 b' ReviewersController = function () {'
312 if (reviewer) {
384 if (reviewer) {
313 // now delete the input
385 // now delete the input
314 $('#reviewer_{0} input'.format(reviewer_id)).remove();
386 $('#reviewer_{0} input'.format(reviewer_id)).remove();
387 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
315 // mark as to-delete
388 // mark as to-delete
316 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
389 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
317 obj.addClass('to-delete');
390 obj.addClass('to-delete');
@@ -322,27 +395,26 b' ReviewersController = function () {'
322 }
395 }
323 };
396 };
324
397
325 this.reviewMemberEntry = function () {
398 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
326
399
327 };
328
329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
330 var id = reviewer_obj.user_id;
400 var id = reviewer_obj.user_id;
331 var username = reviewer_obj.username;
401 var username = reviewer_obj.username;
332
402
333 var reasons = reasons || [];
403 reasons = reasons || [];
334 var mandatory = mandatory || false;
404 mandatory = mandatory || false;
405 role = role || self.ROLE_REVIEWER
335
406
336 // register IDS to check if we don't have this ID already in
407 // register current set IDS to check if we don't have this ID already in
408 // and prevent duplicates
337 var currentIds = [];
409 var currentIds = [];
338
410
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
411 $.each($('.reviewer_entry'), function (index, value) {
340 currentIds.push($(value).data('reviewerUserId'))
412 currentIds.push($(value).data('reviewerUserId'))
341 })
413 })
342
414
343 var userAllowedReview = function (userId) {
415 var userAllowedReview = function (userId) {
344 var allowed = true;
416 var allowed = true;
345 $.each(self.forbidReviewUsers, function (index, member_data) {
417 $.each(self.forbidUsers, function (index, member_data) {
346 if (parseInt(userId) === member_data['user_id']) {
418 if (parseInt(userId) === member_data['user_id']) {
347 allowed = false;
419 allowed = false;
348 return false // breaks the loop
420 return false // breaks the loop
@@ -352,6 +424,7 b' ReviewersController = function () {'
352 };
424 };
353
425
354 var userAllowed = userAllowedReview(id);
426 var userAllowed = userAllowedReview(id);
427
355 if (!userAllowed) {
428 if (!userAllowed) {
356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
429 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
357 } else {
430 } else {
@@ -359,11 +432,13 b' ReviewersController = function () {'
359 var alreadyReviewer = currentIds.indexOf(id) != -1;
432 var alreadyReviewer = currentIds.indexOf(id) != -1;
360
433
361 if (alreadyReviewer) {
434 if (alreadyReviewer) {
362 alert(_gettext('User `{0}` already in reviewers').format(username));
435 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
363 } else {
436 } else {
437
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
438 var reviewerEntry = renderTemplate('reviewMemberEntry', {
365 'member': reviewer_obj,
439 'member': reviewer_obj,
366 'mandatory': mandatory,
440 'mandatory': mandatory,
441 'role': role,
367 'reasons': reasons,
442 'reasons': reasons,
368 'allowed_to_update': true,
443 'allowed_to_update': true,
369 'review_status': 'not_reviewed',
444 'review_status': 'not_reviewed',
@@ -372,20 +447,36 b' ReviewersController = function () {'
372 'create': true,
447 'create': true,
373 'rule_show': true,
448 'rule_show': true,
374 })
449 })
450
451 if (role === self.ROLE_REVIEWER) {
375 $(self.$reviewMembers.selector).append(reviewerEntry);
452 $(self.$reviewMembers.selector).append(reviewerEntry);
453 self.increaseCounter(self.ROLE_REVIEWER);
454 $('#reviewer-empty-msg').remove()
455 }
456 else if (role === self.ROLE_OBSERVER) {
457 $(self.$observerMembers.selector).append(reviewerEntry);
458 self.increaseCounter(self.ROLE_OBSERVER);
459 $('#observer-empty-msg').remove();
460 }
461
376 tooltipActivate();
462 tooltipActivate();
377 }
463 }
378 }
464 }
379
465
380 };
466 };
381
467
382 this.updateReviewers = function (repo_name, pull_request_id) {
468 this.updateReviewers = function (repo_name, pull_request_id, role) {
469 if (role === 'reviewer') {
383 var postData = $('#reviewers input').serialize();
470 var postData = $('#reviewers input').serialize();
384 _updatePullRequest(repo_name, pull_request_id, postData);
471 _updatePullRequest(repo_name, pull_request_id, postData);
472 } else if (role === 'observer') {
473 var postData = $('#observers input').serialize();
474 _updatePullRequest(repo_name, pull_request_id, postData);
475 }
385 };
476 };
386
477
387 this.handleDiffData = function (data) {
478 this.handleDiffData = function (data) {
388 self.diffDataHandler(data)
479 return self.diffDataHandler(data)
389 }
480 }
390 };
481 };
391
482
@@ -449,23 +540,14 b' var editPullRequest = function(repo_name'
449
540
450
541
451 /**
542 /**
452 * Reviewer autocomplete
543 * autocomplete handler for reviewers/observers
453 */
544 */
454 var ReviewerAutoComplete = function(inputId) {
545 var autoCompleteHandler = function (inputId, controller, role) {
455 $(inputId).autocomplete({
546
456 serviceUrl: pyroutes.url('user_autocomplete_data'),
547 return function (element, data) {
457 minChars:2,
458 maxHeight:400,
459 deferRequestBy: 300, //miliseconds
460 showNoSuggestionNotice: true,
461 tabDisabled: true,
462 autoSelectFirst: true,
463 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
464 formatResult: autocompleteFormatResult,
465 lookupFilter: autocompleteFilterResult,
466 onSelect: function(element, data) {
467 var mandatory = false;
548 var mandatory = false;
468 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
549 var reasons = [_gettext('added manually by "{0}"').format(
550 templateContext.rhodecode_user.username)];
469
551
470 // add whole user groups
552 // add whole user groups
471 if (data.value_type == 'user_group') {
553 if (data.value_type == 'user_group') {
@@ -477,7 +559,7 b' var ReviewerAutoComplete = function(inpu'
477 reviewer['gravatar_link'] = member_data['icon_link'];
559 reviewer['gravatar_link'] = member_data['icon_link'];
478 reviewer['user_link'] = member_data['profile_link'];
560 reviewer['user_link'] = member_data['profile_link'];
479 reviewer['rules'] = [];
561 reviewer['rules'] = [];
480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
562 controller.addMember(reviewer, reasons, mandatory, role);
481 })
563 })
482 }
564 }
483 // add single user
565 // add single user
@@ -487,14 +569,71 b' var ReviewerAutoComplete = function(inpu'
487 reviewer['gravatar_link'] = data['icon_link'];
569 reviewer['gravatar_link'] = data['icon_link'];
488 reviewer['user_link'] = data['profile_link'];
570 reviewer['user_link'] = data['profile_link'];
489 reviewer['rules'] = [];
571 reviewer['rules'] = [];
490 reviewersController.addReviewMember(reviewer, reasons, mandatory);
572 controller.addMember(reviewer, reasons, mandatory, role);
491 }
573 }
492
574
493 $(inputId).val('');
575 $(inputId).val('');
494 }
576 }
577 }
578
579 /**
580 * Reviewer autocomplete
581 */
582 var ReviewerAutoComplete = function (inputId, controller) {
583 var self = this;
584 self.controller = controller;
585 self.inputId = inputId;
586 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
587
588 $(inputId).autocomplete({
589 serviceUrl: pyroutes.url('user_autocomplete_data'),
590 minChars: 2,
591 maxHeight: 400,
592 deferRequestBy: 300, //miliseconds
593 showNoSuggestionNotice: true,
594 tabDisabled: true,
595 autoSelectFirst: true,
596 params: {
597 user_id: templateContext.rhodecode_user.user_id,
598 user_groups: true,
599 user_groups_expand: true,
600 skip_default_user: true
601 },
602 formatResult: autocompleteFormatResult,
603 lookupFilter: autocompleteFilterResult,
604 onSelect: handler
495 });
605 });
496 };
606 };
497
607
608 /**
609 * Observers autocomplete
610 */
611 var ObserverAutoComplete = function(inputId, controller) {
612 var self = this;
613 self.controller = controller;
614 self.inputId = inputId;
615 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
616
617 $(inputId).autocomplete({
618 serviceUrl: pyroutes.url('user_autocomplete_data'),
619 minChars: 2,
620 maxHeight: 400,
621 deferRequestBy: 300, //miliseconds
622 showNoSuggestionNotice: true,
623 tabDisabled: true,
624 autoSelectFirst: true,
625 params: {
626 user_id: templateContext.rhodecode_user.user_id,
627 user_groups: true,
628 user_groups_expand: true,
629 skip_default_user: true
630 },
631 formatResult: autocompleteFormatResult,
632 lookupFilter: autocompleteFilterResult,
633 onSelect: handler
634 });
635 }
636
498
637
499 window.VersionController = function () {
638 window.VersionController = function () {
500 var self = this;
639 var self = this;
@@ -504,7 +643,7 b' window.VersionController = function () {'
504
643
505 this.adjustRadioSelectors = function (curNode) {
644 this.adjustRadioSelectors = function (curNode) {
506 var getVal = function (item) {
645 var getVal = function (item) {
507 if (item == 'latest') {
646 if (item === 'latest') {
508 return Number.MAX_SAFE_INTEGER
647 return Number.MAX_SAFE_INTEGER
509 }
648 }
510 else {
649 else {
@@ -663,6 +802,7 b' window.UpdatePrController = function () '
663 };
802 };
664 };
803 };
665
804
805
666 /**
806 /**
667 * Reviewer display panel
807 * Reviewer display panel
668 */
808 */
@@ -673,6 +813,7 b' window.ReviewersPanel = {'
673 removeButtons: null,
813 removeButtons: null,
674 reviewRules: null,
814 reviewRules: null,
675 setReviewers: null,
815 setReviewers: null,
816 controller: null,
676
817
677 setSelectors: function () {
818 setSelectors: function () {
678 var self = this;
819 var self = this;
@@ -682,17 +823,18 b' window.ReviewersPanel = {'
682 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
823 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
683 },
824 },
684
825
685 init: function (reviewRules, setReviewers) {
826 init: function (controller, reviewRules, setReviewers) {
686 var self = this;
827 var self = this;
687 self.setSelectors();
828 self.setSelectors();
688
829
689 this.reviewRules = reviewRules;
830 self.controller = controller;
690 this.setReviewers = setReviewers;
831 self.reviewRules = reviewRules;
832 self.setReviewers = setReviewers;
691
833
692 this.editButton.on('click', function (e) {
834 self.editButton.on('click', function (e) {
693 self.edit();
835 self.edit();
694 });
836 });
695 this.closeButton.on('click', function (e) {
837 self.closeButton.on('click', function (e) {
696 self.close();
838 self.close();
697 self.renderReviewers();
839 self.renderReviewers();
698 });
840 });
@@ -702,14 +844,26 b' window.ReviewersPanel = {'
702 },
844 },
703
845
704 renderReviewers: function () {
846 renderReviewers: function () {
847 var self = this;
705
848
706 $('#review_members').html('')
849 if (self.setReviewers.reviewers === undefined) {
707 $.each(this.setReviewers.reviewers, function (key, val) {
850 return
851 }
852 if (self.setReviewers.reviewers.length === 0) {
853 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
854 return
855 }
856
857 self.controller.emptyReviewersTable();
858
859 $.each(self.setReviewers.reviewers, function (key, val) {
860
708 var member = val;
861 var member = val;
709
862 if (member.role === self.controller.ROLE_REVIEWER) {
710 var entry = renderTemplate('reviewMemberEntry', {
863 var entry = renderTemplate('reviewMemberEntry', {
711 'member': member,
864 'member': member,
712 'mandatory': member.mandatory,
865 'mandatory': member.mandatory,
866 'role': member.role,
713 'reasons': member.reasons,
867 'reasons': member.reasons,
714 'allowed_to_update': member.allowed_to_update,
868 'allowed_to_update': member.allowed_to_update,
715 'review_status': member.review_status,
869 'review_status': member.review_status,
@@ -718,10 +872,106 b' window.ReviewersPanel = {'
718 'create': false
872 'create': false
719 });
873 });
720
874
721 $('#review_members').append(entry)
875 $(self.controller.$reviewMembers.selector).append(entry)
876 }
722 });
877 });
878
723 tooltipActivate();
879 tooltipActivate();
880 },
724
881
882 edit: function (event) {
883 var self = this;
884 self.editButton.hide();
885 self.closeButton.show();
886 self.addButton.show();
887 $(self.removeButtons.selector).css('visibility', 'visible');
888 // review rules
889 self.controller.loadReviewRules(this.reviewRules);
890 },
891
892 close: function (event) {
893 var self = this;
894 this.editButton.show();
895 this.closeButton.hide();
896 this.addButton.hide();
897 $(this.removeButtons.selector).css('visibility', 'hidden');
898 // hide review rules
899 self.controller.hideReviewRules();
900 }
901 };
902
903 /**
904 * Reviewer display panel
905 */
906 window.ObserversPanel = {
907 editButton: null,
908 closeButton: null,
909 addButton: null,
910 removeButtons: null,
911 reviewRules: null,
912 setReviewers: null,
913 controller: null,
914
915 setSelectors: function () {
916 var self = this;
917 self.editButton = $('#open_edit_observers');
918 self.closeButton =$('#close_edit_observers');
919 self.addButton = $('#add_observer');
920 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
921 },
922
923 init: function (controller, reviewRules, setReviewers) {
924 var self = this;
925 self.setSelectors();
926
927 self.controller = controller;
928 self.reviewRules = reviewRules;
929 self.setReviewers = setReviewers;
930
931 self.editButton.on('click', function (e) {
932 self.edit();
933 });
934 self.closeButton.on('click', function (e) {
935 self.close();
936 self.renderObservers();
937 });
938
939 self.renderObservers();
940
941 },
942
943 renderObservers: function () {
944 var self = this;
945 if (self.setReviewers.observers === undefined) {
946 return
947 }
948 if (self.setReviewers.observers.length === 0) {
949 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
950 return
951 }
952
953 self.controller.emptyObserversTable();
954
955 $.each(self.setReviewers.observers, function (key, val) {
956 var member = val;
957 if (member.role === self.controller.ROLE_OBSERVER) {
958 var entry = renderTemplate('reviewMemberEntry', {
959 'member': member,
960 'mandatory': member.mandatory,
961 'role': member.role,
962 'reasons': member.reasons,
963 'allowed_to_update': member.allowed_to_update,
964 'review_status': member.review_status,
965 'review_status_label': member.review_status_label,
966 'user_group': member.user_group,
967 'create': false
968 });
969
970 $(self.controller.$observerMembers.selector).append(entry)
971 }
972 });
973
974 tooltipActivate();
725 },
975 },
726
976
727 edit: function (event) {
977 edit: function (event) {
@@ -729,8 +979,6 b' window.ReviewersPanel = {'
729 this.closeButton.show();
979 this.closeButton.show();
730 this.addButton.show();
980 this.addButton.show();
731 $(this.removeButtons.selector).css('visibility', 'visible');
981 $(this.removeButtons.selector).css('visibility', 'visible');
732 // review rules
733 reviewersController.loadReviewRules(this.reviewRules);
734 },
982 },
735
983
736 close: function (event) {
984 close: function (event) {
@@ -738,12 +986,56 b' window.ReviewersPanel = {'
738 this.closeButton.hide();
986 this.closeButton.hide();
739 this.addButton.hide();
987 this.addButton.hide();
740 $(this.removeButtons.selector).css('visibility', 'hidden');
988 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
989 }
742 reviewersController.hideReviewRules()
990
991 };
992
993 window.PRDetails = {
994 editButton: null,
995 closeButton: null,
996 deleteButton: null,
997 viewFields: null,
998 editFields: null,
999
1000 setSelectors: function () {
1001 var self = this;
1002 self.editButton = $('#open_edit_pullrequest')
1003 self.closeButton = $('#close_edit_pullrequest')
1004 self.deleteButton = $('#delete_pullrequest')
1005 self.viewFields = $('#pr-desc, #pr-title')
1006 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
1007 },
1008
1009 init: function () {
1010 var self = this;
1011 self.setSelectors();
1012 self.editButton.on('click', function (e) {
1013 self.edit();
1014 });
1015 self.closeButton.on('click', function (e) {
1016 self.view();
1017 });
1018 },
1019
1020 edit: function (event) {
1021 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1022 this.viewFields.hide();
1023 this.editButton.hide();
1024 this.deleteButton.hide();
1025 this.closeButton.show();
1026 this.editFields.show();
1027 cmInstance.refresh();
1028 },
1029
1030 view: function (event) {
1031 this.editButton.show();
1032 this.deleteButton.show();
1033 this.editFields.hide();
1034 this.closeButton.hide();
1035 this.viewFields.show();
743 }
1036 }
744 };
1037 };
745
1038
746
747 /**
1039 /**
748 * OnLine presence using channelstream
1040 * OnLine presence using channelstream
749 */
1041 */
@@ -813,17 +1105,14 b' window.refreshComments = function (versi'
813 $.each($('.comment'), function (idx, element) {
1105 $.each($('.comment'), function (idx, element) {
814 currentIDs.push($(element).data('commentId'));
1106 currentIDs.push($(element).data('commentId'));
815 });
1107 });
816 var data = {"comments[]": currentIDs};
1108 var data = {"comments": currentIDs};
817
1109
818 var $targetElem = $('.comments-content-table');
1110 var $targetElem = $('.comments-content-table');
819 $targetElem.css('opacity', 0.3);
1111 $targetElem.css('opacity', 0.3);
820 $targetElem.load(
1112
821 loadUrl, data, function (responseText, textStatus, jqXHR) {
1113 var success = function (data) {
822 if (jqXHR.status !== 200) {
823 return false;
824 }
825 var $counterElem = $('#comments-count');
1114 var $counterElem = $('#comments-count');
826 var newCount = $(responseText).data('counter');
1115 var newCount = $(data).data('counter');
827 if (newCount !== undefined) {
1116 if (newCount !== undefined) {
828 var callback = function () {
1117 var callback = function () {
829 $counterElem.animate({'opacity': 1.00}, 200)
1118 $counterElem.animate({'opacity': 1.00}, 200)
@@ -833,9 +1122,12 b' window.refreshComments = function (versi'
833 }
1122 }
834
1123
835 $targetElem.css('opacity', 1);
1124 $targetElem.css('opacity', 1);
1125 $targetElem.html(data);
836 tooltipActivate();
1126 tooltipActivate();
837 }
1127 }
838 );
1128
1129 ajaxPOST(loadUrl, data, success, null, {})
1130
839 }
1131 }
840
1132
841 window.refreshTODOs = function (version) {
1133 window.refreshTODOs = function (version) {
@@ -858,16 +1150,13 b' window.refreshTODOs = function (version)'
858 currentIDs.push($(element).data('commentId'));
1150 currentIDs.push($(element).data('commentId'));
859 });
1151 });
860
1152
861 var data = {"comments[]": currentIDs};
1153 var data = {"comments": currentIDs};
862 var $targetElem = $('.todos-content-table');
1154 var $targetElem = $('.todos-content-table');
863 $targetElem.css('opacity', 0.3);
1155 $targetElem.css('opacity', 0.3);
864 $targetElem.load(
1156
865 loadUrl, data, function (responseText, textStatus, jqXHR) {
1157 var success = function (data) {
866 if (jqXHR.status !== 200) {
867 return false;
868 }
869 var $counterElem = $('#todos-count')
1158 var $counterElem = $('#todos-count')
870 var newCount = $(responseText).data('counter');
1159 var newCount = $(data).data('counter');
871 if (newCount !== undefined) {
1160 if (newCount !== undefined) {
872 var callback = function () {
1161 var callback = function () {
873 $counterElem.animate({'opacity': 1.00}, 200)
1162 $counterElem.animate({'opacity': 1.00}, 200)
@@ -877,9 +1166,12 b' window.refreshTODOs = function (version)'
877 }
1166 }
878
1167
879 $targetElem.css('opacity', 1);
1168 $targetElem.css('opacity', 1);
1169 $targetElem.html(data);
880 tooltipActivate();
1170 tooltipActivate();
881 }
1171 }
882 );
1172
1173 ajaxPOST(loadUrl, data, success, null, {})
1174
883 }
1175 }
884
1176
885 window.refreshAllComments = function (version) {
1177 window.refreshAllComments = function (version) {
@@ -888,3 +1180,12 b' window.refreshAllComments = function (ve'
888 refreshComments(version);
1180 refreshComments(version);
889 refreshTODOs(version);
1181 refreshTODOs(version);
890 };
1182 };
1183
1184 window.sidebarComment = function (commentId) {
1185 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1186 if (!jsonData) {
1187 return 'Failed to load comment {0}'.format(commentId)
1188 }
1189 var funcData = JSON.parse(atob(jsonData));
1190 return renderTemplate('sideBarCommentHovercard', funcData)
1191 };
@@ -57,15 +57,18 b' var ajaxGET = function (url, success, fa'
57 return request;
57 return request;
58 };
58 };
59
59
60 var ajaxPOST = function (url, postData, success, failure) {
60 var ajaxPOST = function (url, postData, success, failure, options) {
61 var sUrl = url;
61
62 var postData = toQueryString(postData);
62 var ajaxSettings = $.extend({
63 var request = $.ajax({
64 type: 'POST',
63 type: 'POST',
65 url: sUrl,
64 url: url,
66 data: postData,
65 data: toQueryString(postData),
67 headers: {'X-PARTIAL-XHR': true}
66 headers: {'X-PARTIAL-XHR': true}
68 })
67 }, options);
68
69 var request = $.ajax(
70 ajaxSettings
71 )
69 .done(function (data) {
72 .done(function (data) {
70 success(data);
73 success(data);
71 })
74 })
@@ -126,7 +129,8 b' function formatErrorMessage(jqXHR, textS'
126 } else if (errorThrown === 'abort') {
129 } else if (errorThrown === 'abort') {
127 return (prefix + 'Ajax request aborted.');
130 return (prefix + 'Ajax request aborted.');
128 } else {
131 } else {
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
132 var errInfo = 'Uncaught Error. code: {0}\n'.format(jqXHR.status)
133 return (prefix + errInfo + jqXHR.responseText);
130 }
134 }
131 }
135 }
132
136
@@ -33,7 +33,7 b''
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
34 </div>
35 <div class="panel-body panel-body-min-height">
35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="display"></table>
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 </div>
37 </div>
38 </div>
38 </div>
39
39
@@ -58,16 +58,10 b''
58
58
59 dom: 'rtp',
59 dom: 'rtp',
60 pageLength: ${c.visual.dashboard_items},
60 pageLength: ${c.visual.dashboard_items},
61 order: [[2, "desc"]],
61 order: [[1, "desc"]],
62 columns: [
62 columns: [
63 {
63 {
64 data: {
64 data: {
65 "_": "target_repo",
66 "sort": "target_repo"
67 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
68 },
69 {
70 data: {
71 "_": "status",
65 "_": "status",
72 "sort": "status"
66 "sort": "status"
73 }, title: "", className: "td-status", orderable: false
67 }, title: "", className: "td-status", orderable: false
@@ -101,7 +95,13 b''
101 "_": "updated_on",
95 "_": "updated_on",
102 "sort": "updated_on_raw"
96 "sort": "updated_on_raw"
103 }, title: "${_('Last Update')}", className: "td-time"
97 }, title: "${_('Last Update')}", className: "td-time"
104 }
98 },
99 {
100 data: {
101 "_": "target_repo",
102 "sort": "target_repo"
103 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
104 },
105 ],
105 ],
106 language: {
106 language: {
107 paginate: DEFAULT_GRID_PAGINATION,
107 paginate: DEFAULT_GRID_PAGINATION,
@@ -785,15 +785,15 b''
785
785
786 - Prefix query to allow special search:
786 - Prefix query to allow special search:
787
787
788 user:admin, to search for usernames, always global
788 <strong>user:</strong>admin, to search for usernames, always global
789
789
790 user_group:devops, to search for user groups, always global
790 <strong>user_group:</strong>devops, to search for user groups, always global
791
791
792 pr:303, to search for pull request number, title, or description, always global
792 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
793
793
794 commit:efced4, to search for commits, scoped to repositories or groups
794 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
795
795
796 file:models.py, to search for file paths, scoped to repositories or groups
796 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
797
797
798 % if c.template_context['search_context']['repo_id']:
798 % if c.template_context['search_context']['repo_id']:
799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
@@ -16,8 +16,8 b' examples = ['
16 ),
16 ),
17
17
18 (
18 (
19 'Redmine',
19 'Tickets with #123 (Redmine etc)',
20 '(^#|\s#)(?P<issue_id>\d+)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>\d+)',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
22 ''
22 ''
23 ),
23 ),
@@ -38,14 +38,15 b' examples = ['
38
38
39 (
39 (
40 'JIRA - All tickets',
40 'JIRA - All tickets',
41 '(^|\s\w+-\d+)',
41 # official JIRA ticket pattern
42 'https://myjira.com/browse/${id}',
42 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>[A-Z]{1,6}-(?:[1-9][0-9]{0,7}))',
43 'https://myjira.com/browse/${issue_id}',
43 ''
44 ''
44 ),
45 ),
45
46
46 (
47 (
47 'JIRA - Project (JRA)',
48 'JIRA - Single project (JRA-XXXXXXXX)',
48 '(?:(^|\s)(?P<issue_id>(?:JRA-|JRA-)(?:\d+)))',
49 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>JRA-(?:[1-9][0-9]{0,7}))',
49 'https://myjira.com/${issue_id}',
50 'https://myjira.com/${issue_id}',
50 ''
51 ''
51 ),
52 ),
@@ -275,12 +276,18 b' examples = ['
275 <div class='textarea-full'>
276 <div class='textarea-full'>
276 <textarea id="test_pattern_data" rows="12">
277 <textarea id="test_pattern_data" rows="12">
277 This is an example text for testing issue tracker patterns.
278 This is an example text for testing issue tracker patterns.
278 This commit fixes ticket #451 and ticket #910.
279 This commit fixes ticket #451 and ticket #910, reference for JRA-401.
279 Following tickets will get mentioned:
280 The following tickets will get mentioned:
280 #123
281 #123
281 #456
282 #456 and PROJ-101
282 JRA-123
283 JRA-123 and #123
283 JRA-456
284 PROJ-456
285
286 [my artifact](http://something.com/JRA-1234-build.zip)
287
288 - #1001
289 - JRA-998
290
284 Open a pull request !101 to contribute !
291 Open a pull request !101 to contribute!
285 Added tag v1.3.0 for commit 0f3b629be725
292 Added tag v1.3.0 for commit 0f3b629be725
286
293
@@ -89,36 +89,41 b''
89 if is_pr:
89 if is_pr:
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 %>
91 %>
92
92 ## new comments, since refresh
93 <script type="text/javascript">
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 // closure function helper
94 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
95 var sidebarComment${comment_obj.comment_id} = function() {
95 !
96 return renderTemplate('sideBarCommentHovercard', {
96 </div>
97 version_info: "${version_info}",
98 file_name: "${comment_obj.f_path}",
99 line_no: "${comment_obj.line_no}",
100 outdated: ${h.json.dumps(comment_obj.outdated)},
101 inline: ${h.json.dumps(comment_obj.is_inline)},
102 is_todo: ${h.json.dumps(comment_obj.is_todo)},
103 created_on: "${h.format_date(comment_obj.created_on)}",
104 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
105 review_status: "${(comment_obj.review_status or '')}"
106 })
107 }
108 </script>
109
110 % if comment_obj.outdated:
111 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 % elif comment_obj.is_inline:
113 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
114 % else:
115 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
116 % endif
97 % endif
117
98
118 ## NEW, since refresh
99 <%
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
100 data = h.json.dumps({
120 <span class="tag">NEW</span>
101 'comment_id': comment_obj.comment_id,
121 % endif
102 'version_info': version_info,
103 'file_name': comment_obj.f_path,
104 'line_no': comment_obj.line_no,
105 'outdated': comment_obj.outdated,
106 'inline': comment_obj.is_inline,
107 'is_todo': comment_obj.is_todo,
108 'created_on': h.format_date(comment_obj.created_on),
109 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
110 'review_status': (comment_obj.review_status or '')
111 })
112
113 if comment_obj.outdated:
114 icon = 'icon-comment-toggle'
115 elif comment_obj.is_inline:
116 icon = 'icon-code'
117 else:
118 icon = 'icon-comment'
119 %>
120
121 <i id="commentHovercard${comment_obj.comment_id}"
122 class="${icon} tooltip-hovercard"
123 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
124 data-comment-json-b64='${h.b64(data)}'>
125 </i>
126
122 </td>
127 </td>
123
128
124 <td class="td-todo-gravatar">
129 <td class="td-todo-gravatar">
@@ -30,11 +30,21 b''
30 </ul>
30 </ul>
31 %endif
31 %endif
32 %if c.has_references:
32 %if c.has_references:
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('bookmarks')}
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 %endif
44 %endif
36 </div>
45 </div>
37 <table id="obj_list_table" class="display"></table>
46
47 <table id="obj_list_table" class="rctable table-bordered"></table>
38 </div>
48 </div>
39
49
40
50
@@ -43,7 +53,9 b''
43
53
44 var get_datatable_count = function(){
54 var get_datatable_count = function(){
45 var api = $('#obj_list_table').dataTable().api();
55 var api = $('#obj_list_table').dataTable().api();
46 $('#obj_count').text(api.page.info().recordsDisplay);
56 var total = api.page.info().recordsDisplay
57 var _text = _ngettext('{0} bookmark', '{0} bookmarks', total).format(total);
58 $('#obj_count').text(_text);
47 };
59 };
48
60
49 // object list
61 // object list
@@ -30,11 +30,20 b''
30 </ul>
30 </ul>
31 %endif
31 %endif
32 %if c.has_references:
32 %if c.has_references:
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('branches')}
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 %endif
44 %endif
36 </div>
45 </div>
37 <table id="obj_list_table" class="display"></table>
46 <table id="obj_list_table" class="rctable table-bordered"></table>
38 </div>
47 </div>
39
48
40 <script type="text/javascript">
49 <script type="text/javascript">
@@ -42,7 +51,10 b''
42
51
43 var get_datatable_count = function(){
52 var get_datatable_count = function(){
44 var api = $('#obj_list_table').dataTable().api();
53 var api = $('#obj_list_table').dataTable().api();
45 $('#obj_count').text(api.page.info().recordsDisplay);
54 var total = api.page.info().recordsDisplay
55 var _text = _ngettext('{0} branch', '{0} branches', total).format(total);
56
57 $('#obj_count').text(_text);
46 };
58 };
47
59
48 // object list
60 // object list
@@ -187,12 +187,12 b''
187 <div class="sidebar-element clear-both">
187 <div class="sidebar-element clear-both">
188 <% vote_title = _ungettext(
188 <% vote_title = _ungettext(
189 'Status calculated based on votes from {} reviewer',
189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
190 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 %>
191 %>
192
192
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
195 ${c.reviewers_count}
196 </div>
196 </div>
197 </div>
197 </div>
198
198
@@ -420,7 +420,8 b''
420 e.preventDefault();
420 e.preventDefault();
421 });
421 });
422
422
423 ReviewersPanel.init(null, setReviewersData);
423 reviewersController = new ReviewersController();
424 ReviewersPanel.init(reviewersController, null, setReviewersData);
424
425
425 var channel = '${c.commit_broadcast_channel}';
426 var channel = '${c.commit_broadcast_channel}';
426 new ReviewerPresenceController(channel)
427 new ReviewerPresenceController(channel)
@@ -99,7 +99,7 b''
99 <div id="graph_content" class="graph_full_width">
99 <div id="graph_content" class="graph_full_width">
100
100
101 <div class="table">
101 <div class="table">
102 <table id="changesets" class="rctable">
102 <table id="changesets" class="rctable table-bordered">
103 <tr>
103 <tr>
104 ## checkbox
104 ## checkbox
105 <th colspan="4">
105 <th colspan="4">
@@ -379,14 +379,15 b''
379 </%def>
379 </%def>
380
380
381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
382 <code>
382 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
383 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
383
384 % if short:
384 % if short:
385 !${pull_request_id}
385 !${pull_request_id}
386 % else:
386 % else:
387 ${_('Pull request !{}').format(pull_request_id)}
387 ${_('Pull request !{}').format(pull_request_id)}
388 % endif
388 % endif
389
389 </a>
390 </code>
390 % if state not in ['created']:
391 % if state not in ['created']:
391 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
392 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
392 % endif
393 % endif
@@ -394,7 +395,6 b''
394 % if is_wip:
395 % if is_wip:
395 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
396 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
396 % endif
397 % endif
397 </a>
398 </%def>
398 </%def>
399
399
400 <%def name="pullrequest_updated_on(updated_on)">
400 <%def name="pullrequest_updated_on(updated_on)">
@@ -149,7 +149,7 b''
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeMember(70, true)" style="visibility: hidden;">
153 <i class="icon-remove"></i>
153 <i class="icon-remove"></i>
154 </div>
154 </div>
155 </li>
155 </li>
@@ -66,9 +66,18 b" var data_hovercard_url = pyroutes.url('h"
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
67
68 <td style="width: 20px">
68 <td style="width: 20px">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
70 <i class="icon-eye" style="color: #0ac878"></i>
71 </div>
72 <% if (role === 'reviewer') { %>
69 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 <i class="icon-circle review-status-<%= review_status %>"></i>
74 <i class="icon-circle review-status-<%= review_status %>"></i>
71 </div>
75 </div>
76 <% } else if (role === 'observer') { %>
77 <div class="tooltip" title="Observer without voting right.">
78 <i class="icon-circle-thin"></i>
79 </div>
80 <% } %>
72 </td>
81 </td>
73
82
74 <td>
83 <td>
@@ -84,9 +93,6 b" var data_hovercard_url = pyroutes.url('h"
84 'gravatar_url': member.gravatar_link
93 'gravatar_url': member.gravatar_link
85 })
94 })
86 %>
95 %>
87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 <i class="icon-eye" style="color: #0ac878"></i>
89 </span>
90 </div>
96 </div>
91 </td>
97 </td>
92
98
@@ -108,7 +114,7 b" var data_hovercard_url = pyroutes.url('h"
108 <% } else { %>
114 <% } else { %>
109 <td style="text-align: right;width: 10px;">
115 <td style="text-align: right;width: 10px;">
110 <% if (allowed_to_update) { %>
116 <% if (allowed_to_update) { %>
111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 <i class="icon-remove"></i>
118 <i class="icon-remove"></i>
113 </div>
119 </div>
114 <% } %>
120 <% } %>
@@ -117,7 +123,7 b" var data_hovercard_url = pyroutes.url('h"
117
123
118 </tr>
124 </tr>
119
125
120 <tr>
126 <tr id="reviewer_<%= member.user_id %>_rules">
121 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 <input type="hidden" name="__start__" value="reviewer:mapping">
128 <input type="hidden" name="__start__" value="reviewer:mapping">
123
129
@@ -149,6 +155,7 b" var data_hovercard_url = pyroutes.url('h"
149
155
150 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
152
159
153 <input type="hidden" name="__end__" value="reviewer:mapping">
160 <input type="hidden" name="__end__" value="reviewer:mapping">
154 </td>
161 </td>
@@ -11,6 +11,9 b' data = {'
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title,
12 }
12 }
13
13
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
15 %>
18 %>
16
19
@@ -34,6 +37,7 b' data = {'
34 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
36 }
39 }
40
37 %>
41 %>
38
42
39 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
@@ -51,7 +55,7 b' data = {'
51
55
52 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
53 - ${h.short_id(commit_id)}
57 - ${h.short_id(commit_id)}
54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
55
59
56 % endfor
60 % endfor
57
61
@@ -78,19 +82,23 b' data = {'
78 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
79 <tr>
83 <tr>
80 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 <div style="margin: 0; font-weight: bold">
85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
92 % else:
83 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
84 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
85 ${_('requested a')}
95 ${_('requested a')}
86 <a href="${pull_request_url}" style="${base.link_css()}">
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
87 ${_('pull request review.').format(**data) }
88 </a>
89 </div>
97 </div>
98 % endif
90 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
91 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 </div>
101 </div>
93
94 </td>
102 </td>
95 </tr>
103 </tr>
96
104
@@ -10,7 +10,7 b''
10 default_landing_ref = c.commit.raw_id
10 default_landing_ref = c.commit.raw_id
11 %>
11 %>
12 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
12 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
13 <table class="code-browser rctable repo_summary">
13 <table class="code-browser rctable table-bordered">
14 <thead>
14 <thead>
15 <tr>
15 <tr>
16 <th>${_('Name')}</th>
16 <th>${_('Name')}</th>
@@ -112,7 +112,7 b''
112 ## REVIEWERS
112 ## REVIEWERS
113 <div class="field">
113 <div class="field">
114 <div class="label label-textarea">
114 <div class="label label-textarea">
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 </div>
116 </div>
117 <div class="content">
117 <div class="content">
118 ## REVIEW RULES
118 ## REVIEW RULES
@@ -125,18 +125,40 b''
125 </div>
125 </div>
126 </div>
126 </div>
127
127
128 ## REVIEWERS
128 ## REVIEWERS / OBSERVERS
129 <div class="reviewers-title">
129 <div class="reviewers-title">
130 <div class="pr-details-title">
130
131 ${_('Pull request reviewers')}
131 <ul class="nav-links clearfix">
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
132
133 </div>
133 ## TAB1 MANDATORY REVIEWERS
134 </div>
134 <li class="active">
135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 Reviewers
137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 </a>
139 </li>
140
141 ## TAB2 OBSERVERS
142 <li class="">
143 <a id="observers-btn" href="#showObservers" tabindex="-1">
144 Observers
145 <span id="observers-cnt" data-count="0" class="menulink-counter">0</span>
146 </a>
147 </li>
148
149 </ul>
150
151 ## TAB1 MANDATORY REVIEWERS
152 <div id="reviewers-container">
153 <span class="calculate-reviewers">
154 <h4>${_('loading...')}</h4>
155 </span>
156
135 <div id="reviewers" class="pr-details-content reviewers">
157 <div id="reviewers" class="pr-details-content reviewers">
136 ## members goes here, filled via JS based on initial selection !
158 ## members goes here, filled via JS based on initial selection !
137 <input type="hidden" name="__start__" value="review_members:sequence">
159 <input type="hidden" name="__start__" value="review_members:sequence">
138 <table id="review_members" class="group_members">
160 <table id="review_members" class="group_members">
139 ## This content is loaded via JS and ReviewersPanel
161 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
140 </table>
162 </table>
141 <input type="hidden" name="__end__" value="review_members:sequence">
163 <input type="hidden" name="__end__" value="review_members:sequence">
142
164
@@ -149,6 +171,39 b''
149
171
150 </div>
172 </div>
151 </div>
173 </div>
174
175 ## TAB2 OBSERVERS
176 <div id="observers-container" style="display: none">
177 <span class="calculate-reviewers">
178 <h4>${_('loading...')}</h4>
179 </span>
180 % if c.rhodecode_edition_id == 'EE':
181 <div id="observers" class="pr-details-content observers">
182 ## members goes here, filled via JS based on initial selection !
183 <input type="hidden" name="__start__" value="observer_members:sequence">
184 <table id="observer_members" class="group_members">
185 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
186 </table>
187 <input type="hidden" name="__end__" value="observer_members:sequence">
188
189 <div id="add_observer_input" class='ac'>
190 <div class="observer_ac">
191 ${h.text('observer', class_='ac-input', placeholder=_('Add observer or observer group'))}
192 <div id="observers_container"></div>
193 </div>
194 </div>
195 </div>
196 % else:
197 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
198 <p>
199 Pull request observers allows adding users who don't need to leave mandatory votes, but need to be aware about certain changes.
200 </p>
201 % endif
202 </div>
203
204 </div>
205
206 </div>
152 </div>
207 </div>
153
208
154 ## SUBMIT
209 ## SUBMIT
@@ -248,12 +303,19 b''
248
303
249 var originalOption = data.element;
304 var originalOption = data.element;
250 return prefix + escapeMarkup(data.text);
305 return prefix + escapeMarkup(data.text);
251 };formatSelection:
306 };
252
307
253 // custom code mirror
308 // custom code mirror
254 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
309 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
255
310
256 var diffDataHandler = function(data) {
311 var diffDataHandler = function(data) {
312 if (data['error'] !== undefined) {
313 var noCommitsMsg = '<span class="alert-text-error">{0}</span>'.format(data['error']);
314 prButtonLock(true, noCommitsMsg, 'compare');
315 //make both panels equal
316 $('.target-panel').height($('.source-panel').height())
317 return false
318 }
257
319
258 var commitElements = data['commits'];
320 var commitElements = data['commits'];
259 var files = data['files'];
321 var files = data['files'];
@@ -307,8 +369,10 b''
307 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
369 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
308 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
370 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
309
371
372
310 $.each(commitElements, function(idx, value) {
373 $.each(commitElements, function(idx, value) {
311 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
374 var commit_id = value["commit_id"]
375 msg += '<input type="hidden" name="revisions" value="{0}">'.format(commit_id);
312 });
376 });
313
377
314 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
378 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
@@ -338,8 +402,8 b''
338 }
402 }
339
403
340 //make both panels equal
404 //make both panels equal
341 $('.target-panel').height($('.source-panel').height())
405 $('.target-panel').height($('.source-panel').height());
342
406 return true
343 };
407 };
344
408
345 reviewersController = new ReviewersController();
409 reviewersController = new ReviewersController();
@@ -465,8 +529,7 b''
465 queryTargetRefs(initialData, query)
529 queryTargetRefs(initialData, query)
466 },
530 },
467 initSelection: initRefSelection()
531 initSelection: initRefSelection()
468 }
532 });
469 );
470
533
471 var sourceRepoSelect2 = Select2Box($sourceRepo, {
534 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 query: function(query) {}
535 query: function(query) {}
@@ -543,12 +606,44 b''
543 $sourceRef.select2('val', '${c.default_source_ref}');
606 $sourceRef.select2('val', '${c.default_source_ref}');
544
607
545
608
546 // default reviewers
609 // default reviewers / observers
547 reviewersController.loadDefaultReviewers(
610 reviewersController.loadDefaultReviewers(
548 sourceRepo(), sourceRef(), targetRepo(), targetRef());
611 sourceRepo(), sourceRef(), targetRepo(), targetRef());
549 % endif
612 % endif
550
613
551 ReviewerAutoComplete('#user');
614 ReviewerAutoComplete('#user', reviewersController);
615 ObserverAutoComplete('#observer', reviewersController);
616
617 // TODO, move this to another handler
618
619 var $reviewersBtn = $('#reviewers-btn');
620 var $reviewersContainer = $('#reviewers-container');
621
622 var $observersBtn = $('#observers-btn')
623 var $observersContainer = $('#observers-container');
624
625 $reviewersBtn.on('click', function (e) {
626
627 $observersContainer.hide();
628 $reviewersContainer.show();
629
630 $observersBtn.parent().removeClass('active');
631 $reviewersBtn.parent().addClass('active');
632 e.preventDefault();
633
634 })
635
636 $observersBtn.on('click', function (e) {
637
638 $reviewersContainer.hide();
639 $observersContainer.show();
640
641 $reviewersBtn.parent().removeClass('active');
642 $observersBtn.parent().addClass('active');
643 e.preventDefault();
644
645 })
646
552 });
647 });
553 </script>
648 </script>
554
649
@@ -556,12 +556,12 b''
556 <div class="sidebar-element clear-both">
556 <div class="sidebar-element clear-both">
557 <% vote_title = _ungettext(
557 <% vote_title = _ungettext(
558 'Status calculated based on votes from {} reviewer',
558 'Status calculated based on votes from {} reviewer',
559 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
559 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
560 %>
560 %>
561
561
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
564 ${c.reviewers_count}
565 </div>
565 </div>
566
566
567 ## REVIEW RULES
567 ## REVIEW RULES
@@ -609,13 +609,13 b''
609 <div id="add_reviewer" class="ac" style="display: none;">
609 <div id="add_reviewer" class="ac" style="display: none;">
610 %if c.allowed_to_update:
610 %if c.allowed_to_update:
611 % if not c.forbid_adding_reviewers:
611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 <div id="reviewers_container"></div>
614 <div id="reviewers_container"></div>
615 </div>
615 </div>
616 % endif
616 % endif
617 <div class="pull-right">
617 <div class="pull-right" style="margin-bottom: 15px">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 </div>
619 </div>
620 %endif
620 %endif
621 </div>
621 </div>
@@ -623,23 +623,59 b''
623 </div>
623 </div>
624 </div>
624 </div>
625
625
626 ## ## OBSERVERS
626 ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
627 % if c.rhodecode_edition_id == 'EE':
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
628 <div class="sidebar-element clear-both">
629 ## <i class="icon-eye"></i>
629 <% vote_title = _ungettext(
630 ## 0
630 '{} observer without voting right.',
631 ## </div>
631 '{} observers without voting right.', c.observers_count).format(c.observers_count)
632 ##
632 %>
633 ## <div class="right-sidebar-expanded-state pr-details-title">
633
634 ## <span class="sidebar-heading">
634 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
635 ## <i class="icon-eye"></i>
635 <i class="icon-circle-thin"></i>
636 ## ${_('Observers')}
636 ${c.observers_count}
637 ## </span>
637 </div>
638 ## </div>
638
639 ## <div class="right-sidebar-expanded-state pr-details-content">
639 <div class="right-sidebar-expanded-state pr-details-title">
640 ## No observers
640 <span class="tooltip sidebar-heading" title="${vote_title}">
641 ## </div>
641 <i class="icon-circle-thin"></i>
642 ## </div>
642 ${_('Observers')}
643 </span>
644 %if c.allowed_to_update:
645 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
646 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
647 %endif
648 </div>
649
650 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
651 ## members redering block
652 <input type="hidden" name="__start__" value="observer_members:sequence">
653
654 <table id="observer_members" class="group_members">
655 ## This content is loaded via JS and ReviewersPanel
656 </table>
657
658 <input type="hidden" name="__end__" value="observer_members:sequence">
659 ## end members redering block
660
661 %if not c.pull_request.is_closed():
662 <div id="add_observer" class="ac" style="display: none;">
663 %if c.allowed_to_update:
664 % if not c.forbid_adding_reviewers or 1:
665 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
666 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
667 <div id="observers_container"></div>
668 </div>
669 % endif
670 <div class="pull-right" style="margin-bottom: 15px">
671 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
672 </div>
673 %endif
674 </div>
675 %endif
676 </div>
677 </div>
678 % endif
643
679
644 ## TODOs
680 ## TODOs
645 <div class="sidebar-element clear-both">
681 <div class="sidebar-element clear-both">
@@ -757,7 +793,7 b''
757
793
758 <tr><td><code>${_('In pull request description')}:</code></td></tr>
794 <tr><td><code>${_('In pull request description')}:</code></td></tr>
759 % if c.referenced_desc_issues:
795 % if c.referenced_desc_issues:
760 % for ticket_dict in c.referenced_desc_issues:
796 % for ticket_dict in sorted(c.referenced_desc_issues):
761 <tr>
797 <tr>
762 <td>
798 <td>
763 <a href="${ticket_dict.get('url')}">
799 <a href="${ticket_dict.get('url')}">
@@ -776,7 +812,7 b''
776
812
777 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
813 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
778 % if c.referenced_commit_issues:
814 % if c.referenced_commit_issues:
779 % for ticket_dict in c.referenced_commit_issues:
815 % for ticket_dict in sorted(c.referenced_commit_issues):
780 <tr>
816 <tr>
781 <td>
817 <td>
782 <a href="${ticket_dict.get('url')}">
818 <a href="${ticket_dict.get('url')}">
@@ -815,6 +851,7 b' updateController = new UpdatePrControlle'
815
851
816 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
852 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
853 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
854 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
818
855
819 (function () {
856 (function () {
820 "use strict";
857 "use strict";
@@ -822,44 +859,9 b' window.setReviewersData = ${c.pull_reque'
822 // custom code mirror
859 // custom code mirror
823 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
860 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
824
861
825 var PRDetails = {
826 editButton: $('#open_edit_pullrequest'),
827 closeButton: $('#close_edit_pullrequest'),
828 deleteButton: $('#delete_pullrequest'),
829 viewFields: $('#pr-desc, #pr-title'),
830 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
831
832 init: function () {
833 var that = this;
834 this.editButton.on('click', function (e) {
835 that.edit();
836 });
837 this.closeButton.on('click', function (e) {
838 that.view();
839 });
840 },
841
842 edit: function (event) {
843 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
844 this.viewFields.hide();
845 this.editButton.hide();
846 this.deleteButton.hide();
847 this.closeButton.show();
848 this.editFields.show();
849 cmInstance.refresh();
850 },
851
852 view: function (event) {
853 this.editButton.show();
854 this.deleteButton.show();
855 this.editFields.hide();
856 this.closeButton.hide();
857 this.viewFields.show();
858 }
859 };
860
861 PRDetails.init();
862 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
863 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
864 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
863
865
864 window.showOutdated = function (self) {
866 window.showOutdated = function (self) {
865 $('.comment-inline.comment-outdated').show();
867 $('.comment-inline.comment-outdated').show();
@@ -929,12 +931,17 b' window.setReviewersData = ${c.pull_reque'
929 title, description, renderer);
931 title, description, renderer);
930 });
932 });
931
933
932 $('#update_pull_request').on('click', function (e) {
934 var $updateButtons = $('#update_reviewers,#update_observers');
933 $(this).attr('disabled', 'disabled');
935 $updateButtons.on('click', function (e) {
934 $(this).addClass('disabled');
936 var role = $(this).data('role');
935 $(this).html(_gettext('Saving...'));
937 $updateButtons.attr('disabled', 'disabled');
938 $updateButtons.addClass('disabled');
939 $updateButtons.html(_gettext('Saving...'));
936 reviewersController.updateReviewers(
940 reviewersController.updateReviewers(
937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
941 templateContext.repo_name,
942 templateContext.pull_request_data.pull_request_id,
943 role
944 );
938 });
945 });
939
946
940 // fixing issue with caches on firefox
947 // fixing issue with caches on firefox
@@ -978,7 +985,8 b' window.setReviewersData = ${c.pull_reque'
978 refreshMergeChecks();
985 refreshMergeChecks();
979 };
986 };
980
987
981 ReviewerAutoComplete('#user');
988 ReviewerAutoComplete('#user', reviewersController);
989 ObserverAutoComplete('#observer', reviewersController);
982
990
983 })();
991 })();
984
992
@@ -61,7 +61,7 b''
61 </div>
61 </div>
62
62
63 <div class="main-content-full-width">
63 <div class="main-content-full-width">
64 <table id="pull_request_list_table" class="display"></table>
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 </div>
65 </div>
66
66
67 </div>
67 </div>
@@ -1,11 +1,11 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 %if c.repo_commits:
3 %if c.repo_commits:
4 <table class="rctable repo_summary table_disp">
4 <table class="rctable table-bordered">
5 <tr>
5 <tr>
6
6
7 <th class="status"></th>
7 <th class="status"></th>
8 <th>${_('Commit')}</th>
8 <th></th>
9 <th>${_('Commit message')}</th>
9 <th>${_('Commit message')}</th>
10 <th>${_('Age')}</th>
10 <th>${_('Age')}</th>
11 <th>${_('Author')}</th>
11 <th>${_('Author')}</th>
@@ -30,11 +30,20 b''
30 </ul>
30 </ul>
31 %endif
31 %endif
32 %if c.has_references:
32 %if c.has_references:
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('tags')}
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 %endif
44 %endif
36 </div>
45 </div>
37 <table id="obj_list_table" class="display"></table>
46 <table id="obj_list_table" class="rctable table-bordered"></table>
38 </div>
47 </div>
39
48
40
49
@@ -43,7 +52,10 b''
43
52
44 var get_datatable_count = function(){
53 var get_datatable_count = function(){
45 var api = $('#obj_list_table').dataTable().api();
54 var api = $('#obj_list_table').dataTable().api();
46 $('#obj_count').text(api.page.info().recordsDisplay);
55 var total = api.page.info().recordsDisplay
56 var _text = _ngettext('{0} tag', '{0} tags', total).format(total);
57
58 $('#obj_count').text(_text);
47 };
59 };
48
60
49 // object list
61 // object list
@@ -121,7 +121,7 b' def test_extract_issues(backend, text_st'
121
121
122 with mock.patch.object(IssueTrackerSettingsModel,
122 with mock.patch.object(IssueTrackerSettingsModel,
123 'get_settings', get_settings_mock):
123 'get_settings', get_settings_mock):
124 text, issues = helpers.process_patterns(text_string, repo.repo_name)
124 text, issues, errors = helpers.process_patterns(text_string, repo.repo_name)
125
125
126 expected = copy.deepcopy(expected)
126 expected = copy.deepcopy(expected)
127 for item in expected:
127 for item in expected:
@@ -159,7 +159,7 b' def test_process_patterns_repo(backend, '
159
159
160 with mock.patch.object(IssueTrackerSettingsModel,
160 with mock.patch.object(IssueTrackerSettingsModel,
161 'get_settings', get_settings_mock):
161 'get_settings', get_settings_mock):
162 processed_text, issues = helpers.process_patterns(
162 processed_text, issues, error = helpers.process_patterns(
163 text_string, repo.repo_name, link_format)
163 text_string, repo.repo_name, link_format)
164
164
165 assert processed_text == expected_text.format(repo=repo.repo_name)
165 assert processed_text == expected_text.format(repo=repo.repo_name)
@@ -186,7 +186,7 b' def test_process_patterns_no_repo(text_s'
186
186
187 with mock.patch.object(IssueTrackerSettingsModel,
187 with mock.patch.object(IssueTrackerSettingsModel,
188 'get_global_settings', get_settings_mock):
188 'get_global_settings', get_settings_mock):
189 processed_text, issues = helpers.process_patterns(
189 processed_text, issues, errors = helpers.process_patterns(
190 text_string, '')
190 text_string, '')
191
191
192 assert processed_text == expected_text
192 assert processed_text == expected_text
@@ -211,7 +211,7 b' def test_process_patterns_non_existent_r'
211
211
212 with mock.patch.object(IssueTrackerSettingsModel,
212 with mock.patch.object(IssueTrackerSettingsModel,
213 'get_global_settings', get_settings_mock):
213 'get_global_settings', get_settings_mock):
214 processed_text, issues = helpers.process_patterns(
214 processed_text, issues, errors = helpers.process_patterns(
215 text_string, 'do-not-exist')
215 text_string, 'do-not-exist')
216
216
217 assert processed_text == expected_text
217 assert processed_text == expected_text
@@ -23,7 +23,7 b' import collections'
23
23
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User, PullRequestReviewers
27 from rhodecode.model.notification import EmailNotificationModel
27 from rhodecode.model.notification import EmailNotificationModel
28
28
29
29
@@ -52,7 +52,8 b' def test_render_email(app, http_host_onl'
52 assert 'Email Body' in body
52 assert 'Email Body' in body
53
53
54
54
55 def test_render_pr_email(app, user_admin):
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
56 ref = collections.namedtuple(
57 ref = collections.namedtuple(
57 'Ref', 'name, type')('fxies123', 'book')
58 'Ref', 'name, type')('fxies123', 'book')
58
59
@@ -75,13 +76,17 b' def test_render_pr_email(app, user_admin'
75 'pull_request_source_repo_url': 'x',
76 'pull_request_source_repo_url': 'x',
76
77
77 'pull_request_url': 'http://localhost/pr1',
78 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
78 }
80 }
79
81
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
82 subject, body, body_plaintext = EmailNotificationModel().render_email(
81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
82
84
83 # subject
85 # subject
86 if role == PullRequestReviewers.ROLE_REVIEWER:
84 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
85
90
86
91
87 def test_render_pr_update_email(app, user_admin):
92 def test_render_pr_update_email(app, user_admin):
@@ -122,7 +122,7 b' class TestPullRequestModel(object):'
122
122
123 def test_get_awaiting_my_review(self, pull_request):
123 def test_get_awaiting_my_review(self, pull_request):
124 PullRequestModel().update_reviewers(
124 PullRequestModel().update_reviewers(
125 pull_request, [(pull_request.author, ['author'], False, [])],
125 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
126 pull_request.author)
126 pull_request.author)
127 Session().commit()
127 Session().commit()
128
128
@@ -133,7 +133,7 b' class TestPullRequestModel(object):'
133
133
134 def test_count_awaiting_my_review(self, pull_request):
134 def test_count_awaiting_my_review(self, pull_request):
135 PullRequestModel().update_reviewers(
135 PullRequestModel().update_reviewers(
136 pull_request, [(pull_request.author, ['author'], False, [])],
136 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
137 pull_request.author)
137 pull_request.author)
138 Session().commit()
138 Session().commit()
139
139
@@ -43,8 +43,8 b' from rhodecode.lib.utils2 import Attribu'
43 from rhodecode.model.changeset_status import ChangesetStatusModel
43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.db import (
45 from rhodecode.model.db import (
46 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
46 PullRequest, PullRequestReviewers, Repository, RhodeCodeSetting, ChangesetStatus,
47 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
47 RepoGroup, UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
@@ -968,7 +968,7 b' class PRTestUtility(object):'
968 def create_pull_request(
968 def create_pull_request(
969 self, commits=None, target_head=None, source_head=None,
969 self, commits=None, target_head=None, source_head=None,
970 revisions=None, approved=False, author=None, mergeable=False,
970 revisions=None, approved=False, author=None, mergeable=False,
971 enable_notifications=True, name_suffix=u'', reviewers=None,
971 enable_notifications=True, name_suffix=u'', reviewers=None, observers=None,
972 title=u"Test", description=u"Description"):
972 title=u"Test", description=u"Description"):
973 self.set_mergeable(mergeable)
973 self.set_mergeable(mergeable)
974 if not enable_notifications:
974 if not enable_notifications:
@@ -1005,6 +1005,7 b' class PRTestUtility(object):'
1005 'target_ref': self._default_branch_reference(target_head),
1005 'target_ref': self._default_branch_reference(target_head),
1006 'revisions': [self.commit_ids[r] for r in revisions],
1006 'revisions': [self.commit_ids[r] for r in revisions],
1007 'reviewers': reviewers or self._get_reviewers(),
1007 'reviewers': reviewers or self._get_reviewers(),
1008 'observers': observers or self._get_observers(),
1008 'title': title,
1009 'title': title,
1009 'description': description,
1010 'description': description,
1010 }
1011 }
@@ -1037,9 +1038,15 b' class PRTestUtility(object):'
1037 return reference
1038 return reference
1038
1039
1039 def _get_reviewers(self):
1040 def _get_reviewers(self):
1041 role = PullRequestReviewers.ROLE_REVIEWER
1040 return [
1042 return [
1041 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
1043 (TEST_USER_REGULAR_LOGIN, ['default1'], False, role, []),
1042 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1044 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, role, []),
1045 ]
1046
1047 def _get_observers(self):
1048 return [
1049
1043 ]
1050 ]
1044
1051
1045 def update_source_repository(self, head=None):
1052 def update_source_repository(self, head=None):
General Comments 0
You need to be logged in to leave comments. Login now