##// END OF EJS Templates
pull-request: code cleanup...
marcink -
r1979:f2f8f5ce default
parent child Browse files
Show More
@@ -1,179 +1,179 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 from rhodecode.apps.admin.navigation import NavigationRegistry
22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 from rhodecode.config.routing import ADMIN_PREFIX
23 from rhodecode.config.routing import ADMIN_PREFIX
24 from rhodecode.lib.utils2 import str2bool
24 from rhodecode.lib.utils2 import str2bool
25
25
26
26
27 def admin_routes(config):
27 def admin_routes(config):
28 """
28 """
29 Admin prefixed routes
29 Admin prefixed routes
30 """
30 """
31
31
32 config.add_route(
32 config.add_route(
33 name='admin_audit_logs',
33 name='admin_audit_logs',
34 pattern='/audit_logs')
34 pattern='/audit_logs')
35
35
36 config.add_route(
36 config.add_route(
37 name='pull_requests_global_0', # backward compat
37 name='pull_requests_global_0', # backward compat
38 pattern='/pull_requests/{pull_request_id:[0-9]+}')
38 pattern='/pull_requests/{pull_request_id:\d+}')
39 config.add_route(
39 config.add_route(
40 name='pull_requests_global_1', # backward compat
40 name='pull_requests_global_1', # backward compat
41 pattern='/pull-requests/{pull_request_id:[0-9]+}')
41 pattern='/pull-requests/{pull_request_id:\d+}')
42 config.add_route(
42 config.add_route(
43 name='pull_requests_global',
43 name='pull_requests_global',
44 pattern='/pull-request/{pull_request_id:[0-9]+}')
44 pattern='/pull-request/{pull_request_id:\d+}')
45
45
46 config.add_route(
46 config.add_route(
47 name='admin_settings_open_source',
47 name='admin_settings_open_source',
48 pattern='/settings/open_source')
48 pattern='/settings/open_source')
49 config.add_route(
49 config.add_route(
50 name='admin_settings_vcs_svn_generate_cfg',
50 name='admin_settings_vcs_svn_generate_cfg',
51 pattern='/settings/vcs/svn_generate_cfg')
51 pattern='/settings/vcs/svn_generate_cfg')
52
52
53 config.add_route(
53 config.add_route(
54 name='admin_settings_system',
54 name='admin_settings_system',
55 pattern='/settings/system')
55 pattern='/settings/system')
56 config.add_route(
56 config.add_route(
57 name='admin_settings_system_update',
57 name='admin_settings_system_update',
58 pattern='/settings/system/updates')
58 pattern='/settings/system/updates')
59
59
60 config.add_route(
60 config.add_route(
61 name='admin_settings_sessions',
61 name='admin_settings_sessions',
62 pattern='/settings/sessions')
62 pattern='/settings/sessions')
63 config.add_route(
63 config.add_route(
64 name='admin_settings_sessions_cleanup',
64 name='admin_settings_sessions_cleanup',
65 pattern='/settings/sessions/cleanup')
65 pattern='/settings/sessions/cleanup')
66
66
67 config.add_route(
67 config.add_route(
68 name='admin_settings_process_management',
68 name='admin_settings_process_management',
69 pattern='/settings/process_management')
69 pattern='/settings/process_management')
70 config.add_route(
70 config.add_route(
71 name='admin_settings_process_management_signal',
71 name='admin_settings_process_management_signal',
72 pattern='/settings/process_management/signal')
72 pattern='/settings/process_management/signal')
73
73
74 # global permissions
74 # global permissions
75
75
76 config.add_route(
76 config.add_route(
77 name='admin_permissions_application',
77 name='admin_permissions_application',
78 pattern='/permissions/application')
78 pattern='/permissions/application')
79 config.add_route(
79 config.add_route(
80 name='admin_permissions_application_update',
80 name='admin_permissions_application_update',
81 pattern='/permissions/application/update')
81 pattern='/permissions/application/update')
82
82
83 config.add_route(
83 config.add_route(
84 name='admin_permissions_global',
84 name='admin_permissions_global',
85 pattern='/permissions/global')
85 pattern='/permissions/global')
86 config.add_route(
86 config.add_route(
87 name='admin_permissions_global_update',
87 name='admin_permissions_global_update',
88 pattern='/permissions/global/update')
88 pattern='/permissions/global/update')
89
89
90 config.add_route(
90 config.add_route(
91 name='admin_permissions_object',
91 name='admin_permissions_object',
92 pattern='/permissions/object')
92 pattern='/permissions/object')
93 config.add_route(
93 config.add_route(
94 name='admin_permissions_object_update',
94 name='admin_permissions_object_update',
95 pattern='/permissions/object/update')
95 pattern='/permissions/object/update')
96
96
97 config.add_route(
97 config.add_route(
98 name='admin_permissions_ips',
98 name='admin_permissions_ips',
99 pattern='/permissions/ips')
99 pattern='/permissions/ips')
100
100
101 config.add_route(
101 config.add_route(
102 name='admin_permissions_overview',
102 name='admin_permissions_overview',
103 pattern='/permissions/overview')
103 pattern='/permissions/overview')
104
104
105 config.add_route(
105 config.add_route(
106 name='admin_permissions_auth_token_access',
106 name='admin_permissions_auth_token_access',
107 pattern='/permissions/auth_token_access')
107 pattern='/permissions/auth_token_access')
108
108
109 # users admin
109 # users admin
110 config.add_route(
110 config.add_route(
111 name='users',
111 name='users',
112 pattern='/users')
112 pattern='/users')
113
113
114 config.add_route(
114 config.add_route(
115 name='users_data',
115 name='users_data',
116 pattern='/users_data')
116 pattern='/users_data')
117
117
118 # user auth tokens
118 # user auth tokens
119 config.add_route(
119 config.add_route(
120 name='edit_user_auth_tokens',
120 name='edit_user_auth_tokens',
121 pattern='/users/{user_id:\d+}/edit/auth_tokens')
121 pattern='/users/{user_id:\d+}/edit/auth_tokens')
122 config.add_route(
122 config.add_route(
123 name='edit_user_auth_tokens_add',
123 name='edit_user_auth_tokens_add',
124 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
124 pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
125 config.add_route(
125 config.add_route(
126 name='edit_user_auth_tokens_delete',
126 name='edit_user_auth_tokens_delete',
127 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
127 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
128
128
129 # user emails
129 # user emails
130 config.add_route(
130 config.add_route(
131 name='edit_user_emails',
131 name='edit_user_emails',
132 pattern='/users/{user_id:\d+}/edit/emails')
132 pattern='/users/{user_id:\d+}/edit/emails')
133 config.add_route(
133 config.add_route(
134 name='edit_user_emails_add',
134 name='edit_user_emails_add',
135 pattern='/users/{user_id:\d+}/edit/emails/new')
135 pattern='/users/{user_id:\d+}/edit/emails/new')
136 config.add_route(
136 config.add_route(
137 name='edit_user_emails_delete',
137 name='edit_user_emails_delete',
138 pattern='/users/{user_id:\d+}/edit/emails/delete')
138 pattern='/users/{user_id:\d+}/edit/emails/delete')
139
139
140 # user IPs
140 # user IPs
141 config.add_route(
141 config.add_route(
142 name='edit_user_ips',
142 name='edit_user_ips',
143 pattern='/users/{user_id:\d+}/edit/ips')
143 pattern='/users/{user_id:\d+}/edit/ips')
144 config.add_route(
144 config.add_route(
145 name='edit_user_ips_add',
145 name='edit_user_ips_add',
146 pattern='/users/{user_id:\d+}/edit/ips/new')
146 pattern='/users/{user_id:\d+}/edit/ips/new')
147 config.add_route(
147 config.add_route(
148 name='edit_user_ips_delete',
148 name='edit_user_ips_delete',
149 pattern='/users/{user_id:\d+}/edit/ips/delete')
149 pattern='/users/{user_id:\d+}/edit/ips/delete')
150
150
151 # user groups management
151 # user groups management
152 config.add_route(
152 config.add_route(
153 name='edit_user_groups_management',
153 name='edit_user_groups_management',
154 pattern='/users/{user_id:\d+}/edit/groups_management')
154 pattern='/users/{user_id:\d+}/edit/groups_management')
155
155
156 config.add_route(
156 config.add_route(
157 name='edit_user_groups_management_updates',
157 name='edit_user_groups_management_updates',
158 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
158 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
159
159
160 # user audit logs
160 # user audit logs
161 config.add_route(
161 config.add_route(
162 name='edit_user_audit_logs',
162 name='edit_user_audit_logs',
163 pattern='/users/{user_id:\d+}/edit/audit')
163 pattern='/users/{user_id:\d+}/edit/audit')
164
164
165
165
166 def includeme(config):
166 def includeme(config):
167 settings = config.get_settings()
167 settings = config.get_settings()
168
168
169 # Create admin navigation registry and add it to the pyramid registry.
169 # Create admin navigation registry and add it to the pyramid registry.
170 labs_active = str2bool(settings.get('labs_settings_active', False))
170 labs_active = str2bool(settings.get('labs_settings_active', False))
171 navigation_registry = NavigationRegistry(labs_active=labs_active)
171 navigation_registry = NavigationRegistry(labs_active=labs_active)
172 config.registry.registerUtility(navigation_registry)
172 config.registry.registerUtility(navigation_registry)
173
173
174 # main admin routes
174 # main admin routes
175 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
175 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
176 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
176 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
177
177
178 # Scan module for configuration decorators.
178 # Scan module for configuration decorators.
179 config.scan()
179 config.scan()
@@ -1,63 +1,65 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.model.db import PullRequest
30 from rhodecode.model.db import PullRequest
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class AdminMainView(BaseAppView):
36 class AdminMainView(BaseAppView):
37
37
38 @LoginRequired()
38 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
39 @HasPermissionAllDecorator('hg.admin')
40 @view_config(
40 @view_config(
41 route_name='admin_home', request_method='GET')
41 route_name='admin_home', request_method='GET')
42 def admin_main(self):
42 def admin_main(self):
43 # redirect _admin to audit logs...
43 # redirect _admin to audit logs...
44 raise HTTPFound(h.route_path('admin_audit_logs'))
44 raise HTTPFound(h.route_path('admin_audit_logs'))
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
50 def pull_requests(self):
50 def pull_requests(self):
51 """
51 """
52 Global redirect for Pull Requests
52 Global redirect for Pull Requests
53
53
54 :param pull_request_id: id of pull requests in the system
54 :param pull_request_id: id of pull requests in the system
55 """
55 """
56
56
57 pull_request_id = self.request.matchdict.get('pull_request_id')
57 pull_request = PullRequest.get_or_404(
58 pull_request = PullRequest.get_or_404(pull_request_id)
58 self.request.matchdict['pull_request_id'])
59 pull_request_id = pull_request.pull_request_id
60
59 repo_name = pull_request.target_repo.repo_name
61 repo_name = pull_request.target_repo.repo_name
60
62
61 raise HTTPFound(
63 raise HTTPFound(
62 h.route_path('pullrequest_show', repo_name=repo_name,
64 h.route_path('pullrequest_show', repo_name=repo_name,
63 pull_request_id=pull_request_id))
65 pull_request_id=pull_request_id))
@@ -1,150 +1,150 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.utils2 import safe_unicode, safe_str
23 from rhodecode.lib.utils2 import safe_unicode, safe_str
24 from rhodecode.model.db import Repository
24 from rhodecode.model.db import Repository
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
27 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.utils import repo_on_filesystem
29 from rhodecode.tests.utils import repo_on_filesystem
30
30
31 fixture = Fixture()
31 fixture = Fixture()
32
32
33
33
34 def route_path(name, params=None, **kwargs):
34 def route_path(name, params=None, **kwargs):
35 import urllib
35 import urllib
36
36
37 base_url = {
37 base_url = {
38 'repo_summary_explicit': '/{repo_name}/summary',
38 'repo_summary_explicit': '/{repo_name}/summary',
39 'repo_summary': '/{repo_name}',
39 'repo_summary': '/{repo_name}',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
45
45
46 }[name].format(**kwargs)
46 }[name].format(**kwargs)
47
47
48 if params:
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
50 return base_url
50 return base_url
51
51
52
52
53 @pytest.mark.usefixtures('autologin_user', 'app')
53 @pytest.mark.usefixtures('autologin_user', 'app')
54 class TestAdminRepoSettingsAdvanced(object):
54 class TestAdminRepoSettingsAdvanced(object):
55
55
56 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
56 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
57 repo = backend.repo
57 repo = backend.repo
58 response = self.app.get(
58 response = self.app.get(
59 route_path('edit_repo_advanced', repo_name=backend.repo_name))
59 route_path('edit_repo_advanced', repo_name=backend.repo_name))
60 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
60 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
61 response.mustcontain(no=[opt])
61 response.mustcontain(no=[opt])
62
62
63 def test_set_fork_of_target_repo(
63 def test_set_fork_of_target_repo(
64 self, autologin_user, backend, csrf_token):
64 self, autologin_user, backend, csrf_token):
65 target_repo = 'target_%s' % backend.alias
65 target_repo = 'target_%s' % backend.alias
66 fixture.create_repo(target_repo, repo_type=backend.alias)
66 fixture.create_repo(target_repo, repo_type=backend.alias)
67 repo2 = Repository.get_by_repo_name(target_repo)
67 repo2 = Repository.get_by_repo_name(target_repo)
68 response = self.app.post(
68 response = self.app.post(
69 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
69 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
70 params={'id_fork_of': repo2.repo_id,
70 params={'id_fork_of': repo2.repo_id,
71 'csrf_token': csrf_token})
71 'csrf_token': csrf_token})
72 repo = Repository.get_by_repo_name(backend.repo_name)
72 repo = Repository.get_by_repo_name(backend.repo_name)
73 repo2 = Repository.get_by_repo_name(target_repo)
73 repo2 = Repository.get_by_repo_name(target_repo)
74 assert_session_flash(
74 assert_session_flash(
75 response,
75 response,
76 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
76 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
77
77
78 assert repo.fork == repo2
78 assert repo.fork == repo2
79 response = response.follow()
79 response = response.follow()
80 # check if given repo is selected
80 # check if given repo is selected
81
81
82 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
82 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
83 route_path('repo_summary', repo_name=repo2.repo_name),
83 route_path('repo_summary', repo_name=repo2.repo_name),
84 repo2.repo_name)
84 repo2.repo_name)
85
85
86 response.mustcontain(opt)
86 response.mustcontain(opt)
87
87
88 fixture.destroy_repo(target_repo, forks='detach')
88 fixture.destroy_repo(target_repo, forks='detach')
89
89
90 @pytest.mark.backends("hg", "git")
90 @pytest.mark.backends("hg", "git")
91 def test_set_fork_of_other_type_repo(
91 def test_set_fork_of_other_type_repo(
92 self, autologin_user, backend, csrf_token):
92 self, autologin_user, backend, csrf_token):
93 TARGET_REPO_MAP = {
93 TARGET_REPO_MAP = {
94 'git': {
94 'git': {
95 'type': 'hg',
95 'type': 'hg',
96 'repo_name': HG_REPO},
96 'repo_name': HG_REPO},
97 'hg': {
97 'hg': {
98 'type': 'git',
98 'type': 'git',
99 'repo_name': GIT_REPO},
99 'repo_name': GIT_REPO},
100 }
100 }
101 target_repo = TARGET_REPO_MAP[backend.alias]
101 target_repo = TARGET_REPO_MAP[backend.alias]
102
102
103 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
103 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
104 response = self.app.post(
104 response = self.app.post(
105 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
105 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
106 params={'id_fork_of': repo2.repo_id,
106 params={'id_fork_of': repo2.repo_id,
107 'csrf_token': csrf_token})
107 'csrf_token': csrf_token})
108 assert_session_flash(
108 assert_session_flash(
109 response,
109 response,
110 'Cannot set repository as fork of repository with other type')
110 'Cannot set repository as fork of repository with other type')
111
111
112 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
112 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
113 # mark it as None
113 # mark it as None
114 response = self.app.post(
114 response = self.app.post(
115 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
115 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
116 params={'id_fork_of': None, '_method': 'put',
116 params={'id_fork_of': None,
117 'csrf_token': csrf_token})
117 'csrf_token': csrf_token})
118 assert_session_flash(
118 assert_session_flash(
119 response,
119 response,
120 'Marked repo %s as fork of %s'
120 'Marked repo %s as fork of %s'
121 % (backend.repo_name, "Nothing"))
121 % (backend.repo_name, "Nothing"))
122 assert backend.repo.fork is None
122 assert backend.repo.fork is None
123
123
124 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
124 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
125 repo = Repository.get_by_repo_name(backend.repo_name)
125 repo = Repository.get_by_repo_name(backend.repo_name)
126 response = self.app.post(
126 response = self.app.post(
127 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
127 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
128 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
128 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
129 assert_session_flash(
129 assert_session_flash(
130 response, 'An error occurred during this operation')
130 response, 'An error occurred during this operation')
131
131
132 @pytest.mark.parametrize(
132 @pytest.mark.parametrize(
133 "suffix",
133 "suffix",
134 ['', u'Δ…Δ™Ε‚' , '123'],
134 ['', u'Δ…Δ™Ε‚' , '123'],
135 ids=no_newline_id_generator)
135 ids=no_newline_id_generator)
136 def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
136 def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
137 repo = backend.create_repo(name_suffix=suffix)
137 repo = backend.create_repo(name_suffix=suffix)
138 repo_name = repo.repo_name
138 repo_name = repo.repo_name
139 repo_name_str = safe_str(repo.repo_name)
139 repo_name_str = safe_str(repo.repo_name)
140
140
141 response = self.app.post(
141 response = self.app.post(
142 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
142 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
143 params={'csrf_token': csrf_token})
143 params={'csrf_token': csrf_token})
144 assert_session_flash(response,
144 assert_session_flash(response,
145 u'Deleted repository `{}`'.format(repo_name))
145 u'Deleted repository `{}`'.format(repo_name))
146 response.follow()
146 response.follow()
147
147
148 # check if repo was deleted from db
148 # check if repo was deleted from db
149 assert RepoModel().get_by_repo_name(repo_name) is None
149 assert RepoModel().get_by_repo_name(repo_name) is None
150 assert not repo_on_filesystem(repo_name_str)
150 assert not repo_on_filesystem(repo_name_str)
@@ -1,1181 +1,1186 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import peppercorn
25 import peppercorn
26 from pyramid.httpexceptions import (
26 from pyramid.httpexceptions import (
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 from pyramid.view import view_config
28 from pyramid.view import view_config
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
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 (func, or_, PullRequest, PullRequestVersion,
45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 ChangesetComment, ChangesetStatus, Repository)
46 ChangesetComment, ChangesetStatus, Repository)
47 from rhodecode.model.forms import PullRequestForm
47 from rhodecode.model.forms import PullRequestForm
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 from rhodecode.model.scm import ScmModel
50 from rhodecode.model.scm import ScmModel
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56
56
57 def load_default_context(self):
57 def load_default_context(self):
58 c = self._get_local_tmpl_context(include_app_defaults=True)
58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 c.repo_info = self.db_repo
60 c.repo_info = self.db_repo
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 self._register_global_c(c)
63 self._register_global_c(c)
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'data_table/_dt_elements.mako')
72 'data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176
176
177 # additional filters
177 # additional filters
178 req_get = self.request.GET
178 req_get = self.request.GET
179 source = str2bool(req_get.get('source'))
179 source = str2bool(req_get.get('source'))
180 closed = str2bool(req_get.get('closed'))
180 closed = str2bool(req_get.get('closed'))
181 my = str2bool(req_get.get('my'))
181 my = str2bool(req_get.get('my'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184
184
185 filter_type = 'awaiting_review' if awaiting_review \
185 filter_type = 'awaiting_review' if awaiting_review \
186 else 'awaiting_my_review' if awaiting_my_review \
186 else 'awaiting_my_review' if awaiting_my_review \
187 else None
187 else None
188
188
189 opened_by = None
189 opened_by = None
190 if my:
190 if my:
191 opened_by = [self._rhodecode_user.user_id]
191 opened_by = [self._rhodecode_user.user_id]
192
192
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 if closed:
194 if closed:
195 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
196
196
197 data = self._get_pull_requests_list(
197 data = self._get_pull_requests_list(
198 repo_name=self.db_repo_name, source=source,
198 repo_name=self.db_repo_name, source=source,
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200
200
201 return data
201 return data
202
202
203 def _get_pr_version(self, pull_request_id, version=None):
203 def _get_pr_version(self, pull_request_id, version=None):
204 at_version = None
204 at_version = None
205
205
206 if version and version == 'latest':
206 if version and version == 'latest':
207 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_obj = pull_request_ver
208 pull_request_obj = pull_request_ver
209 _org_pull_request_obj = pull_request_obj
209 _org_pull_request_obj = pull_request_obj
210 at_version = 'latest'
210 at_version = 'latest'
211 elif version:
211 elif version:
212 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_obj = pull_request_ver
213 pull_request_obj = pull_request_ver
214 _org_pull_request_obj = pull_request_ver.pull_request
214 _org_pull_request_obj = pull_request_ver.pull_request
215 at_version = pull_request_ver.pull_request_version_id
215 at_version = pull_request_ver.pull_request_version_id
216 else:
216 else:
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 pull_request_id)
218 pull_request_id)
219
219
220 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_obj, _org_pull_request_obj)
221 pull_request_obj, _org_pull_request_obj)
222
222
223 return _org_pull_request_obj, pull_request_obj, \
223 return _org_pull_request_obj, pull_request_obj, \
224 pull_request_display_obj, at_version
224 pull_request_display_obj, at_version
225
225
226 def _get_diffset(self, source_repo_name, source_repo,
226 def _get_diffset(self, source_repo_name, source_repo,
227 source_ref_id, target_ref_id,
227 source_ref_id, target_ref_id,
228 target_commit, source_commit, diff_limit, fulldiff,
228 target_commit, source_commit, diff_limit, fulldiff,
229 file_limit, display_inline_comments):
229 file_limit, display_inline_comments):
230
230
231 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
232 source_repo, source_ref_id, target_ref_id)
232 source_repo, source_ref_id, target_ref_id)
233
233
234 diff_processor = diffs.DiffProcessor(
234 diff_processor = diffs.DiffProcessor(
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
237
237
238 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
239
239
240 def _node_getter(commit):
240 def _node_getter(commit):
241 def get_node(fname):
241 def get_node(fname):
242 try:
242 try:
243 return commit.get_node(fname)
243 return commit.get_node(fname)
244 except NodeDoesNotExistError:
244 except NodeDoesNotExistError:
245 return None
245 return None
246
246
247 return get_node
247 return get_node
248
248
249 diffset = codeblocks.DiffSet(
249 diffset = codeblocks.DiffSet(
250 repo_name=self.db_repo_name,
250 repo_name=self.db_repo_name,
251 source_repo_name=source_repo_name,
251 source_repo_name=source_repo_name,
252 source_node_getter=_node_getter(target_commit),
252 source_node_getter=_node_getter(target_commit),
253 target_node_getter=_node_getter(source_commit),
253 target_node_getter=_node_getter(source_commit),
254 comments=display_inline_comments
254 comments=display_inline_comments
255 )
255 )
256 diffset = diffset.render_patchset(
256 diffset = diffset.render_patchset(
257 _parsed, target_commit.raw_id, source_commit.raw_id)
257 _parsed, target_commit.raw_id, source_commit.raw_id)
258
258
259 return diffset
259 return diffset
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
267 def pull_request_show(self):
268 pull_request_id = self.request.matchdict.get('pull_request_id')
268 pull_request_id = self.request.matchdict['pull_request_id']
269
269
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 version = self.request.GET.get('version')
272 version = self.request.GET.get('version')
273 from_version = self.request.GET.get('from_version') or version
273 from_version = self.request.GET.get('from_version') or version
274 merge_checks = self.request.GET.get('merge_checks')
274 merge_checks = self.request.GET.get('merge_checks')
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276
276
277 (pull_request_latest,
277 (pull_request_latest,
278 pull_request_at_ver,
278 pull_request_at_ver,
279 pull_request_display_obj,
279 pull_request_display_obj,
280 at_version) = self._get_pr_version(
280 at_version) = self._get_pr_version(
281 pull_request_id, version=version)
281 pull_request_id, version=version)
282 pr_closed = pull_request_latest.is_closed()
282 pr_closed = pull_request_latest.is_closed()
283
283
284 if pr_closed and (version or from_version):
284 if pr_closed and (version or from_version):
285 # not allow to browse versions
285 # not allow to browse versions
286 raise HTTPFound(h.route_path(
286 raise HTTPFound(h.route_path(
287 'pullrequest_show', repo_name=self.db_repo_name,
287 'pullrequest_show', repo_name=self.db_repo_name,
288 pull_request_id=pull_request_id))
288 pull_request_id=pull_request_id))
289
289
290 versions = pull_request_display_obj.versions()
290 versions = pull_request_display_obj.versions()
291
291
292 c.at_version = at_version
292 c.at_version = at_version
293 c.at_version_num = (at_version
293 c.at_version_num = (at_version
294 if at_version and at_version != 'latest'
294 if at_version and at_version != 'latest'
295 else None)
295 else None)
296 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_num, versions)
297 c.at_version_num, versions)
298
298
299 (prev_pull_request_latest,
299 (prev_pull_request_latest,
300 prev_pull_request_at_ver,
300 prev_pull_request_at_ver,
301 prev_pull_request_display_obj,
301 prev_pull_request_display_obj,
302 prev_at_version) = self._get_pr_version(
302 prev_at_version) = self._get_pr_version(
303 pull_request_id, version=from_version)
303 pull_request_id, version=from_version)
304
304
305 c.from_version = prev_at_version
305 c.from_version = prev_at_version
306 c.from_version_num = (prev_at_version
306 c.from_version_num = (prev_at_version
307 if prev_at_version and prev_at_version != 'latest'
307 if prev_at_version and prev_at_version != 'latest'
308 else None)
308 else None)
309 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_num, versions)
310 c.from_version_num, versions)
311
311
312 # define if we're in COMPARE mode or VIEW at version mode
312 # define if we're in COMPARE mode or VIEW at version mode
313 compare = at_version != prev_at_version
313 compare = at_version != prev_at_version
314
314
315 # pull_requests repo_name we opened it against
315 # pull_requests repo_name we opened it against
316 # ie. target_repo must match
316 # ie. target_repo must match
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 raise HTTPNotFound()
318 raise HTTPNotFound()
319
319
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 pull_request_at_ver)
321 pull_request_at_ver)
322
322
323 c.pull_request = pull_request_display_obj
323 c.pull_request = pull_request_display_obj
324 c.pull_request_latest = pull_request_latest
324 c.pull_request_latest = pull_request_latest
325
325
326 if compare or (at_version and not at_version == 'latest'):
326 if compare or (at_version and not at_version == 'latest'):
327 c.allowed_to_change_status = False
327 c.allowed_to_change_status = False
328 c.allowed_to_update = False
328 c.allowed_to_update = False
329 c.allowed_to_merge = False
329 c.allowed_to_merge = False
330 c.allowed_to_delete = False
330 c.allowed_to_delete = False
331 c.allowed_to_comment = False
331 c.allowed_to_comment = False
332 c.allowed_to_close = False
332 c.allowed_to_close = False
333 else:
333 else:
334 can_change_status = PullRequestModel().check_user_change_status(
334 can_change_status = PullRequestModel().check_user_change_status(
335 pull_request_at_ver, self._rhodecode_user)
335 pull_request_at_ver, self._rhodecode_user)
336 c.allowed_to_change_status = can_change_status and not pr_closed
336 c.allowed_to_change_status = can_change_status and not pr_closed
337
337
338 c.allowed_to_update = PullRequestModel().check_user_update(
338 c.allowed_to_update = PullRequestModel().check_user_update(
339 pull_request_latest, self._rhodecode_user) and not pr_closed
339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 pull_request_latest, self._rhodecode_user) and not pr_closed
341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 pull_request_latest, self._rhodecode_user) and not pr_closed
343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 c.allowed_to_comment = not pr_closed
344 c.allowed_to_comment = not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346
346
347 c.forbid_adding_reviewers = False
347 c.forbid_adding_reviewers = False
348 c.forbid_author_to_review = False
348 c.forbid_author_to_review = False
349 c.forbid_commit_author_to_review = False
349 c.forbid_commit_author_to_review = False
350
350
351 if pull_request_latest.reviewer_data and \
351 if pull_request_latest.reviewer_data and \
352 'rules' in pull_request_latest.reviewer_data:
352 'rules' in pull_request_latest.reviewer_data:
353 rules = pull_request_latest.reviewer_data['rules'] or {}
353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 try:
354 try:
355 c.forbid_adding_reviewers = rules.get(
355 c.forbid_adding_reviewers = rules.get(
356 'forbid_adding_reviewers')
356 'forbid_adding_reviewers')
357 c.forbid_author_to_review = rules.get(
357 c.forbid_author_to_review = rules.get(
358 'forbid_author_to_review')
358 'forbid_author_to_review')
359 c.forbid_commit_author_to_review = rules.get(
359 c.forbid_commit_author_to_review = rules.get(
360 'forbid_commit_author_to_review')
360 'forbid_commit_author_to_review')
361 except Exception:
361 except Exception:
362 pass
362 pass
363
363
364 # check merge capabilities
364 # check merge capabilities
365 _merge_check = MergeCheck.validate(
365 _merge_check = MergeCheck.validate(
366 pull_request_latest, user=self._rhodecode_user)
366 pull_request_latest, user=self._rhodecode_user)
367 c.pr_merge_errors = _merge_check.error_details
367 c.pr_merge_errors = _merge_check.error_details
368 c.pr_merge_possible = not _merge_check.failed
368 c.pr_merge_possible = not _merge_check.failed
369 c.pr_merge_message = _merge_check.merge_msg
369 c.pr_merge_message = _merge_check.merge_msg
370
370
371 c.pull_request_review_status = _merge_check.review_status
371 c.pull_request_review_status = _merge_check.review_status
372 if merge_checks:
372 if merge_checks:
373 self.request.override_renderer = \
373 self.request.override_renderer = \
374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 return self._get_template_context(c)
375 return self._get_template_context(c)
376
376
377 comments_model = CommentsModel()
377 comments_model = CommentsModel()
378
378
379 # reviewers and statuses
379 # reviewers and statuses
380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382
382
383 # GENERAL COMMENTS with versions #
383 # GENERAL COMMENTS with versions #
384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 q = q.order_by(ChangesetComment.comment_id.asc())
385 q = q.order_by(ChangesetComment.comment_id.asc())
386 general_comments = q
386 general_comments = q
387
387
388 # pick comments we want to render at current version
388 # pick comments we want to render at current version
389 c.comment_versions = comments_model.aggregate_comments(
389 c.comment_versions = comments_model.aggregate_comments(
390 general_comments, versions, c.at_version_num)
390 general_comments, versions, c.at_version_num)
391 c.comments = c.comment_versions[c.at_version_num]['until']
391 c.comments = c.comment_versions[c.at_version_num]['until']
392
392
393 # INLINE COMMENTS with versions #
393 # INLINE COMMENTS with versions #
394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 q = q.order_by(ChangesetComment.comment_id.asc())
395 q = q.order_by(ChangesetComment.comment_id.asc())
396 inline_comments = q
396 inline_comments = q
397
397
398 c.inline_versions = comments_model.aggregate_comments(
398 c.inline_versions = comments_model.aggregate_comments(
399 inline_comments, versions, c.at_version_num, inline=True)
399 inline_comments, versions, c.at_version_num, inline=True)
400
400
401 # inject latest version
401 # inject latest version
402 latest_ver = PullRequest.get_pr_display_object(
402 latest_ver = PullRequest.get_pr_display_object(
403 pull_request_latest, pull_request_latest)
403 pull_request_latest, pull_request_latest)
404
404
405 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
406
406
407 # if we use version, then do not show later comments
407 # if we use version, then do not show later comments
408 # than current version
408 # than current version
409 display_inline_comments = collections.defaultdict(
409 display_inline_comments = collections.defaultdict(
410 lambda: collections.defaultdict(list))
410 lambda: collections.defaultdict(list))
411 for co in inline_comments:
411 for co in inline_comments:
412 if c.at_version_num:
412 if c.at_version_num:
413 # pick comments that are at least UPTO given version, so we
413 # pick comments that are at least UPTO given version, so we
414 # don't render comments for higher version
414 # don't render comments for higher version
415 should_render = co.pull_request_version_id and \
415 should_render = co.pull_request_version_id and \
416 co.pull_request_version_id <= c.at_version_num
416 co.pull_request_version_id <= c.at_version_num
417 else:
417 else:
418 # showing all, for 'latest'
418 # showing all, for 'latest'
419 should_render = True
419 should_render = True
420
420
421 if should_render:
421 if should_render:
422 display_inline_comments[co.f_path][co.line_no].append(co)
422 display_inline_comments[co.f_path][co.line_no].append(co)
423
423
424 # load diff data into template context, if we use compare mode then
424 # load diff data into template context, if we use compare mode then
425 # diff is calculated based on changes between versions of PR
425 # diff is calculated based on changes between versions of PR
426
426
427 source_repo = pull_request_at_ver.source_repo
427 source_repo = pull_request_at_ver.source_repo
428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429
429
430 target_repo = pull_request_at_ver.target_repo
430 target_repo = pull_request_at_ver.target_repo
431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432
432
433 if compare:
433 if compare:
434 # in compare switch the diff base to latest commit from prev version
434 # in compare switch the diff base to latest commit from prev version
435 target_ref_id = prev_pull_request_display_obj.revisions[0]
435 target_ref_id = prev_pull_request_display_obj.revisions[0]
436
436
437 # despite opening commits for bookmarks/branches/tags, we always
437 # despite opening commits for bookmarks/branches/tags, we always
438 # convert this to rev to prevent changes after bookmark or branch change
438 # convert this to rev to prevent changes after bookmark or branch change
439 c.source_ref_type = 'rev'
439 c.source_ref_type = 'rev'
440 c.source_ref = source_ref_id
440 c.source_ref = source_ref_id
441
441
442 c.target_ref_type = 'rev'
442 c.target_ref_type = 'rev'
443 c.target_ref = target_ref_id
443 c.target_ref = target_ref_id
444
444
445 c.source_repo = source_repo
445 c.source_repo = source_repo
446 c.target_repo = target_repo
446 c.target_repo = target_repo
447
447
448 c.commit_ranges = []
448 c.commit_ranges = []
449 source_commit = EmptyCommit()
449 source_commit = EmptyCommit()
450 target_commit = EmptyCommit()
450 target_commit = EmptyCommit()
451 c.missing_requirements = False
451 c.missing_requirements = False
452
452
453 source_scm = source_repo.scm_instance()
453 source_scm = source_repo.scm_instance()
454 target_scm = target_repo.scm_instance()
454 target_scm = target_repo.scm_instance()
455
455
456 # try first shadow repo, fallback to regular repo
456 # try first shadow repo, fallback to regular repo
457 try:
457 try:
458 commits_source_repo = pull_request_latest.get_shadow_repo()
458 commits_source_repo = pull_request_latest.get_shadow_repo()
459 except Exception:
459 except Exception:
460 log.debug('Failed to get shadow repo', exc_info=True)
460 log.debug('Failed to get shadow repo', exc_info=True)
461 commits_source_repo = source_scm
461 commits_source_repo = source_scm
462
462
463 c.commits_source_repo = commits_source_repo
463 c.commits_source_repo = commits_source_repo
464 commit_cache = {}
464 commit_cache = {}
465 try:
465 try:
466 pre_load = ["author", "branch", "date", "message"]
466 pre_load = ["author", "branch", "date", "message"]
467 show_revs = pull_request_at_ver.revisions
467 show_revs = pull_request_at_ver.revisions
468 for rev in show_revs:
468 for rev in show_revs:
469 comm = commits_source_repo.get_commit(
469 comm = commits_source_repo.get_commit(
470 commit_id=rev, pre_load=pre_load)
470 commit_id=rev, pre_load=pre_load)
471 c.commit_ranges.append(comm)
471 c.commit_ranges.append(comm)
472 commit_cache[comm.raw_id] = comm
472 commit_cache[comm.raw_id] = comm
473
473
474 # Order here matters, we first need to get target, and then
474 # Order here matters, we first need to get target, and then
475 # the source
475 # the source
476 target_commit = commits_source_repo.get_commit(
476 target_commit = commits_source_repo.get_commit(
477 commit_id=safe_str(target_ref_id))
477 commit_id=safe_str(target_ref_id))
478
478
479 source_commit = commits_source_repo.get_commit(
479 source_commit = commits_source_repo.get_commit(
480 commit_id=safe_str(source_ref_id))
480 commit_id=safe_str(source_ref_id))
481
481
482 except CommitDoesNotExistError:
482 except CommitDoesNotExistError:
483 log.warning(
483 log.warning(
484 'Failed to get commit from `{}` repo'.format(
484 'Failed to get commit from `{}` repo'.format(
485 commits_source_repo), exc_info=True)
485 commits_source_repo), exc_info=True)
486 except RepositoryRequirementError:
486 except RepositoryRequirementError:
487 log.warning(
487 log.warning(
488 'Failed to get all required data from repo', exc_info=True)
488 'Failed to get all required data from repo', exc_info=True)
489 c.missing_requirements = True
489 c.missing_requirements = True
490
490
491 c.ancestor = None # set it to None, to hide it from PR view
491 c.ancestor = None # set it to None, to hide it from PR view
492
492
493 try:
493 try:
494 ancestor_id = source_scm.get_common_ancestor(
494 ancestor_id = source_scm.get_common_ancestor(
495 source_commit.raw_id, target_commit.raw_id, target_scm)
495 source_commit.raw_id, target_commit.raw_id, target_scm)
496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 except Exception:
497 except Exception:
498 c.ancestor_commit = None
498 c.ancestor_commit = None
499
499
500 c.statuses = source_repo.statuses(
500 c.statuses = source_repo.statuses(
501 [x.raw_id for x in c.commit_ranges])
501 [x.raw_id for x in c.commit_ranges])
502
502
503 # auto collapse if we have more than limit
503 # auto collapse if we have more than limit
504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 c.compare_mode = compare
506 c.compare_mode = compare
507
507
508 # diff_limit is the old behavior, will cut off the whole diff
508 # diff_limit is the old behavior, will cut off the whole diff
509 # if the limit is applied otherwise will just hide the
509 # if the limit is applied otherwise will just hide the
510 # big files from the front-end
510 # big files from the front-end
511 diff_limit = c.visual.cut_off_limit_diff
511 diff_limit = c.visual.cut_off_limit_diff
512 file_limit = c.visual.cut_off_limit_file
512 file_limit = c.visual.cut_off_limit_file
513
513
514 c.missing_commits = False
514 c.missing_commits = False
515 if (c.missing_requirements
515 if (c.missing_requirements
516 or isinstance(source_commit, EmptyCommit)
516 or isinstance(source_commit, EmptyCommit)
517 or source_commit == target_commit):
517 or source_commit == target_commit):
518
518
519 c.missing_commits = True
519 c.missing_commits = True
520 else:
520 else:
521
521
522 c.diffset = self._get_diffset(
522 c.diffset = self._get_diffset(
523 c.source_repo.repo_name, commits_source_repo,
523 c.source_repo.repo_name, commits_source_repo,
524 source_ref_id, target_ref_id,
524 source_ref_id, target_ref_id,
525 target_commit, source_commit,
525 target_commit, source_commit,
526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527
527
528 c.limited_diff = c.diffset.limited_diff
528 c.limited_diff = c.diffset.limited_diff
529
529
530 # calculate removed files that are bound to comments
530 # calculate removed files that are bound to comments
531 comment_deleted_files = [
531 comment_deleted_files = [
532 fname for fname in display_inline_comments
532 fname for fname in display_inline_comments
533 if fname not in c.diffset.file_stats]
533 if fname not in c.diffset.file_stats]
534
534
535 c.deleted_files_comments = collections.defaultdict(dict)
535 c.deleted_files_comments = collections.defaultdict(dict)
536 for fname, per_line_comments in display_inline_comments.items():
536 for fname, per_line_comments in display_inline_comments.items():
537 if fname in comment_deleted_files:
537 if fname in comment_deleted_files:
538 c.deleted_files_comments[fname]['stats'] = 0
538 c.deleted_files_comments[fname]['stats'] = 0
539 c.deleted_files_comments[fname]['comments'] = list()
539 c.deleted_files_comments[fname]['comments'] = list()
540 for lno, comments in per_line_comments.items():
540 for lno, comments in per_line_comments.items():
541 c.deleted_files_comments[fname]['comments'].extend(
541 c.deleted_files_comments[fname]['comments'].extend(
542 comments)
542 comments)
543
543
544 # this is a hack to properly display links, when creating PR, the
544 # this is a hack to properly display links, when creating PR, the
545 # compare view and others uses different notation, and
545 # compare view and others uses different notation, and
546 # compare_commits.mako renders links based on the target_repo.
546 # compare_commits.mako renders links based on the target_repo.
547 # We need to swap that here to generate it properly on the html side
547 # We need to swap that here to generate it properly on the html side
548 c.target_repo = c.source_repo
548 c.target_repo = c.source_repo
549
549
550 c.commit_statuses = ChangesetStatus.STATUSES
550 c.commit_statuses = ChangesetStatus.STATUSES
551
551
552 c.show_version_changes = not pr_closed
552 c.show_version_changes = not pr_closed
553 if c.show_version_changes:
553 if c.show_version_changes:
554 cur_obj = pull_request_at_ver
554 cur_obj = pull_request_at_ver
555 prev_obj = prev_pull_request_at_ver
555 prev_obj = prev_pull_request_at_ver
556
556
557 old_commit_ids = prev_obj.revisions
557 old_commit_ids = prev_obj.revisions
558 new_commit_ids = cur_obj.revisions
558 new_commit_ids = cur_obj.revisions
559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 old_commit_ids, new_commit_ids)
560 old_commit_ids, new_commit_ids)
561 c.commit_changes_summary = commit_changes
561 c.commit_changes_summary = commit_changes
562
562
563 # calculate the diff for commits between versions
563 # calculate the diff for commits between versions
564 c.commit_changes = []
564 c.commit_changes = []
565 mark = lambda cs, fw: list(
565 mark = lambda cs, fw: list(
566 h.itertools.izip_longest([], cs, fillvalue=fw))
566 h.itertools.izip_longest([], cs, fillvalue=fw))
567 for c_type, raw_id in mark(commit_changes.added, 'a') \
567 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 + mark(commit_changes.removed, 'r') \
568 + mark(commit_changes.removed, 'r') \
569 + mark(commit_changes.common, 'c'):
569 + mark(commit_changes.common, 'c'):
570
570
571 if raw_id in commit_cache:
571 if raw_id in commit_cache:
572 commit = commit_cache[raw_id]
572 commit = commit_cache[raw_id]
573 else:
573 else:
574 try:
574 try:
575 commit = commits_source_repo.get_commit(raw_id)
575 commit = commits_source_repo.get_commit(raw_id)
576 except CommitDoesNotExistError:
576 except CommitDoesNotExistError:
577 # in case we fail extracting still use "dummy" commit
577 # in case we fail extracting still use "dummy" commit
578 # for display in commit diff
578 # for display in commit diff
579 commit = h.AttributeDict(
579 commit = h.AttributeDict(
580 {'raw_id': raw_id,
580 {'raw_id': raw_id,
581 'message': 'EMPTY or MISSING COMMIT'})
581 'message': 'EMPTY or MISSING COMMIT'})
582 c.commit_changes.append([c_type, commit])
582 c.commit_changes.append([c_type, commit])
583
583
584 # current user review statuses for each version
584 # current user review statuses for each version
585 c.review_versions = {}
585 c.review_versions = {}
586 if self._rhodecode_user.user_id in allowed_reviewers:
586 if self._rhodecode_user.user_id in allowed_reviewers:
587 for co in general_comments:
587 for co in general_comments:
588 if co.author.user_id == self._rhodecode_user.user_id:
588 if co.author.user_id == self._rhodecode_user.user_id:
589 # each comment has a status change
589 # each comment has a status change
590 status = co.status_change
590 status = co.status_change
591 if status:
591 if status:
592 _ver_pr = status[0].comment.pull_request_version_id
592 _ver_pr = status[0].comment.pull_request_version_id
593 c.review_versions[_ver_pr] = status[0]
593 c.review_versions[_ver_pr] = status[0]
594
594
595 return self._get_template_context(c)
595 return self._get_template_context(c)
596
596
597 def assure_not_empty_repo(self):
597 def assure_not_empty_repo(self):
598 _ = self.request.translate
598 _ = self.request.translate
599
599
600 try:
600 try:
601 self.db_repo.scm_instance().get_commit()
601 self.db_repo.scm_instance().get_commit()
602 except EmptyRepositoryError:
602 except EmptyRepositoryError:
603 h.flash(h.literal(_('There are no commits yet')),
603 h.flash(h.literal(_('There are no commits yet')),
604 category='warning')
604 category='warning')
605 raise HTTPFound(
605 raise HTTPFound(
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607
607
608 @LoginRequired()
608 @LoginRequired()
609 @NotAnonymous()
609 @NotAnonymous()
610 @HasRepoPermissionAnyDecorator(
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
612 @view_config(
613 route_name='pullrequest_new', request_method='GET',
613 route_name='pullrequest_new', request_method='GET',
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 def pull_request_new(self):
615 def pull_request_new(self):
616 _ = self.request.translate
616 _ = self.request.translate
617 c = self.load_default_context()
617 c = self.load_default_context()
618
618
619 self.assure_not_empty_repo()
619 self.assure_not_empty_repo()
620 source_repo = self.db_repo
620 source_repo = self.db_repo
621
621
622 commit_id = self.request.GET.get('commit')
622 commit_id = self.request.GET.get('commit')
623 branch_ref = self.request.GET.get('branch')
623 branch_ref = self.request.GET.get('branch')
624 bookmark_ref = self.request.GET.get('bookmark')
624 bookmark_ref = self.request.GET.get('bookmark')
625
625
626 try:
626 try:
627 source_repo_data = PullRequestModel().generate_repo_data(
627 source_repo_data = PullRequestModel().generate_repo_data(
628 source_repo, commit_id=commit_id,
628 source_repo, commit_id=commit_id,
629 branch=branch_ref, bookmark=bookmark_ref)
629 branch=branch_ref, bookmark=bookmark_ref)
630 except CommitDoesNotExistError as e:
630 except CommitDoesNotExistError as e:
631 log.exception(e)
631 log.exception(e)
632 h.flash(_('Commit does not exist'), 'error')
632 h.flash(_('Commit does not exist'), 'error')
633 raise HTTPFound(
633 raise HTTPFound(
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635
635
636 default_target_repo = source_repo
636 default_target_repo = source_repo
637
637
638 if source_repo.parent:
638 if source_repo.parent:
639 parent_vcs_obj = source_repo.parent.scm_instance()
639 parent_vcs_obj = source_repo.parent.scm_instance()
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 # change default if we have a parent repo
641 # change default if we have a parent repo
642 default_target_repo = source_repo.parent
642 default_target_repo = source_repo.parent
643
643
644 target_repo_data = PullRequestModel().generate_repo_data(
644 target_repo_data = PullRequestModel().generate_repo_data(
645 default_target_repo)
645 default_target_repo)
646
646
647 selected_source_ref = source_repo_data['refs']['selected_ref']
647 selected_source_ref = source_repo_data['refs']['selected_ref']
648
648
649 title_source_ref = selected_source_ref.split(':', 2)[1]
649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 c.default_title = PullRequestModel().generate_pullrequest_title(
650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 source=source_repo.repo_name,
651 source=source_repo.repo_name,
652 source_ref=title_source_ref,
652 source_ref=title_source_ref,
653 target=default_target_repo.repo_name
653 target=default_target_repo.repo_name
654 )
654 )
655
655
656 c.default_repo_data = {
656 c.default_repo_data = {
657 'source_repo_name': source_repo.repo_name,
657 'source_repo_name': source_repo.repo_name,
658 'source_refs_json': json.dumps(source_repo_data),
658 'source_refs_json': json.dumps(source_repo_data),
659 'target_repo_name': default_target_repo.repo_name,
659 'target_repo_name': default_target_repo.repo_name,
660 'target_refs_json': json.dumps(target_repo_data),
660 'target_refs_json': json.dumps(target_repo_data),
661 }
661 }
662 c.default_source_ref = selected_source_ref
662 c.default_source_ref = selected_source_ref
663
663
664 return self._get_template_context(c)
664 return self._get_template_context(c)
665
665
666 @LoginRequired()
666 @LoginRequired()
667 @NotAnonymous()
667 @NotAnonymous()
668 @HasRepoPermissionAnyDecorator(
668 @HasRepoPermissionAnyDecorator(
669 'repository.read', 'repository.write', 'repository.admin')
669 'repository.read', 'repository.write', 'repository.admin')
670 @view_config(
670 @view_config(
671 route_name='pullrequest_repo_refs', request_method='GET',
671 route_name='pullrequest_repo_refs', request_method='GET',
672 renderer='json_ext', xhr=True)
672 renderer='json_ext', xhr=True)
673 def pull_request_repo_refs(self):
673 def pull_request_repo_refs(self):
674 target_repo_name = self.request.matchdict['target_repo_name']
674 target_repo_name = self.request.matchdict['target_repo_name']
675 repo = Repository.get_by_repo_name(target_repo_name)
675 repo = Repository.get_by_repo_name(target_repo_name)
676 if not repo:
676 if not repo:
677 raise HTTPNotFound()
677 raise HTTPNotFound()
678 return PullRequestModel().generate_repo_data(repo)
678 return PullRequestModel().generate_repo_data(repo)
679
679
680 @LoginRequired()
680 @LoginRequired()
681 @NotAnonymous()
681 @NotAnonymous()
682 @HasRepoPermissionAnyDecorator(
682 @HasRepoPermissionAnyDecorator(
683 'repository.read', 'repository.write', 'repository.admin')
683 'repository.read', 'repository.write', 'repository.admin')
684 @view_config(
684 @view_config(
685 route_name='pullrequest_repo_destinations', request_method='GET',
685 route_name='pullrequest_repo_destinations', request_method='GET',
686 renderer='json_ext', xhr=True)
686 renderer='json_ext', xhr=True)
687 def pull_request_repo_destinations(self):
687 def pull_request_repo_destinations(self):
688 _ = self.request.translate
688 _ = self.request.translate
689 filter_query = self.request.GET.get('query')
689 filter_query = self.request.GET.get('query')
690
690
691 query = Repository.query() \
691 query = Repository.query() \
692 .order_by(func.length(Repository.repo_name)) \
692 .order_by(func.length(Repository.repo_name)) \
693 .filter(
693 .filter(
694 or_(Repository.repo_name == self.db_repo.repo_name,
694 or_(Repository.repo_name == self.db_repo.repo_name,
695 Repository.fork_id == self.db_repo.repo_id))
695 Repository.fork_id == self.db_repo.repo_id))
696
696
697 if filter_query:
697 if filter_query:
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 query = query.filter(
699 query = query.filter(
700 Repository.repo_name.ilike(ilike_expression))
700 Repository.repo_name.ilike(ilike_expression))
701
701
702 add_parent = False
702 add_parent = False
703 if self.db_repo.parent:
703 if self.db_repo.parent:
704 if filter_query in self.db_repo.parent.repo_name:
704 if filter_query in self.db_repo.parent.repo_name:
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 add_parent = True
707 add_parent = True
708
708
709 limit = 20 - 1 if add_parent else 20
709 limit = 20 - 1 if add_parent else 20
710 all_repos = query.limit(limit).all()
710 all_repos = query.limit(limit).all()
711 if add_parent:
711 if add_parent:
712 all_repos += [self.db_repo.parent]
712 all_repos += [self.db_repo.parent]
713
713
714 repos = []
714 repos = []
715 for obj in ScmModel().get_repos(all_repos):
715 for obj in ScmModel().get_repos(all_repos):
716 repos.append({
716 repos.append({
717 'id': obj['name'],
717 'id': obj['name'],
718 'text': obj['name'],
718 'text': obj['name'],
719 'type': 'repo',
719 'type': 'repo',
720 'obj': obj['dbrepo']
720 'obj': obj['dbrepo']
721 })
721 })
722
722
723 data = {
723 data = {
724 'more': False,
724 'more': False,
725 'results': [{
725 'results': [{
726 'text': _('Repositories'),
726 'text': _('Repositories'),
727 'children': repos
727 'children': repos
728 }] if repos else []
728 }] if repos else []
729 }
729 }
730 return data
730 return data
731
731
732 @LoginRequired()
732 @LoginRequired()
733 @NotAnonymous()
733 @NotAnonymous()
734 @HasRepoPermissionAnyDecorator(
734 @HasRepoPermissionAnyDecorator(
735 'repository.read', 'repository.write', 'repository.admin')
735 'repository.read', 'repository.write', 'repository.admin')
736 @CSRFRequired()
736 @CSRFRequired()
737 @view_config(
737 @view_config(
738 route_name='pullrequest_create', request_method='POST',
738 route_name='pullrequest_create', request_method='POST',
739 renderer=None)
739 renderer=None)
740 def pull_request_create(self):
740 def pull_request_create(self):
741 _ = self.request.translate
741 _ = self.request.translate
742 self.assure_not_empty_repo()
742 self.assure_not_empty_repo()
743
743
744 controls = peppercorn.parse(self.request.POST.items())
744 controls = peppercorn.parse(self.request.POST.items())
745
745
746 try:
746 try:
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 except formencode.Invalid as errors:
748 except formencode.Invalid as errors:
749 if errors.error_dict.get('revisions'):
749 if errors.error_dict.get('revisions'):
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 elif errors.error_dict.get('pullrequest_title'):
751 elif errors.error_dict.get('pullrequest_title'):
752 msg = _('Pull request requires a title with min. 3 chars')
752 msg = _('Pull request requires a title with min. 3 chars')
753 else:
753 else:
754 msg = _('Error creating pull request: {}').format(errors)
754 msg = _('Error creating pull request: {}').format(errors)
755 log.exception(msg)
755 log.exception(msg)
756 h.flash(msg, 'error')
756 h.flash(msg, 'error')
757
757
758 # would rather just go back to form ...
758 # would rather just go back to form ...
759 raise HTTPFound(
759 raise HTTPFound(
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761
761
762 source_repo = _form['source_repo']
762 source_repo = _form['source_repo']
763 source_ref = _form['source_ref']
763 source_ref = _form['source_ref']
764 target_repo = _form['target_repo']
764 target_repo = _form['target_repo']
765 target_ref = _form['target_ref']
765 target_ref = _form['target_ref']
766 commit_ids = _form['revisions'][::-1]
766 commit_ids = _form['revisions'][::-1]
767
767
768 # find the ancestor for this pr
768 # find the ancestor for this pr
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771
771
772 source_scm = source_db_repo.scm_instance()
772 source_scm = source_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
774
774
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777
777
778 ancestor = source_scm.get_common_ancestor(
778 ancestor = source_scm.get_common_ancestor(
779 source_commit.raw_id, target_commit.raw_id, target_scm)
779 source_commit.raw_id, target_commit.raw_id, target_scm)
780
780
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783
783
784 pullrequest_title = _form['pullrequest_title']
784 pullrequest_title = _form['pullrequest_title']
785 title_source_ref = source_ref.split(':', 2)[1]
785 title_source_ref = source_ref.split(':', 2)[1]
786 if not pullrequest_title:
786 if not pullrequest_title:
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 source=source_repo,
788 source=source_repo,
789 source_ref=title_source_ref,
789 source_ref=title_source_ref,
790 target=target_repo
790 target=target_repo
791 )
791 )
792
792
793 description = _form['pullrequest_desc']
793 description = _form['pullrequest_desc']
794
794
795 get_default_reviewers_data, validate_default_reviewers = \
795 get_default_reviewers_data, validate_default_reviewers = \
796 PullRequestModel().get_reviewer_functions()
796 PullRequestModel().get_reviewer_functions()
797
797
798 # recalculate reviewers logic, to make sure we can validate this
798 # recalculate reviewers logic, to make sure we can validate this
799 reviewer_rules = get_default_reviewers_data(
799 reviewer_rules = get_default_reviewers_data(
800 self._rhodecode_db_user, source_db_repo,
800 self._rhodecode_db_user, source_db_repo,
801 source_commit, target_db_repo, target_commit)
801 source_commit, target_db_repo, target_commit)
802
802
803 given_reviewers = _form['review_members']
803 given_reviewers = _form['review_members']
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805
805
806 try:
806 try:
807 pull_request = PullRequestModel().create(
807 pull_request = PullRequestModel().create(
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 target_ref, commit_ids, reviewers, pullrequest_title,
809 target_ref, commit_ids, reviewers, pullrequest_title,
810 description, reviewer_rules
810 description, reviewer_rules
811 )
811 )
812 Session().commit()
812 Session().commit()
813 h.flash(_('Successfully opened new pull request'),
813 h.flash(_('Successfully opened new pull request'),
814 category='success')
814 category='success')
815 except Exception as e:
815 except Exception as e:
816 msg = _('Error occurred during creation of this pull request.')
816 msg = _('Error occurred during creation of this pull request.')
817 log.exception(msg)
817 log.exception(msg)
818 h.flash(msg, category='error')
818 h.flash(msg, category='error')
819 raise HTTPFound(
819 raise HTTPFound(
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821
821
822 raise HTTPFound(
822 raise HTTPFound(
823 h.route_path('pullrequest_show', repo_name=target_repo,
823 h.route_path('pullrequest_show', repo_name=target_repo,
824 pull_request_id=pull_request.pull_request_id))
824 pull_request_id=pull_request.pull_request_id))
825
825
826 @LoginRequired()
826 @LoginRequired()
827 @NotAnonymous()
827 @NotAnonymous()
828 @HasRepoPermissionAnyDecorator(
828 @HasRepoPermissionAnyDecorator(
829 'repository.read', 'repository.write', 'repository.admin')
829 'repository.read', 'repository.write', 'repository.admin')
830 @CSRFRequired()
830 @CSRFRequired()
831 @view_config(
831 @view_config(
832 route_name='pullrequest_update', request_method='POST',
832 route_name='pullrequest_update', request_method='POST',
833 renderer='json_ext')
833 renderer='json_ext')
834 def pull_request_update(self):
834 def pull_request_update(self):
835 pull_request_id = self.request.matchdict['pull_request_id']
835 pull_request = PullRequest.get_or_404(
836 pull_request = PullRequest.get_or_404(pull_request_id)
836 self.request.matchdict['pull_request_id'])
837
837
838 # only owner or admin can update it
838 # only owner or admin can update it
839 allowed_to_update = PullRequestModel().check_user_update(
839 allowed_to_update = PullRequestModel().check_user_update(
840 pull_request, self._rhodecode_user)
840 pull_request, self._rhodecode_user)
841 if allowed_to_update:
841 if allowed_to_update:
842 controls = peppercorn.parse(self.request.POST.items())
842 controls = peppercorn.parse(self.request.POST.items())
843
843
844 if 'review_members' in controls:
844 if 'review_members' in controls:
845 self._update_reviewers(
845 self._update_reviewers(
846 pull_request_id, controls['review_members'],
846 pull_request, controls['review_members'],
847 pull_request.reviewer_data)
847 pull_request.reviewer_data)
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 self._update_commits(pull_request)
849 self._update_commits(pull_request)
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 self._edit_pull_request(pull_request)
851 self._edit_pull_request(pull_request)
852 else:
852 else:
853 raise HTTPBadRequest()
853 raise HTTPBadRequest()
854 return True
854 return True
855 raise HTTPForbidden()
855 raise HTTPForbidden()
856
856
857 def _edit_pull_request(self, pull_request):
857 def _edit_pull_request(self, pull_request):
858 _ = self.request.translate
858 _ = self.request.translate
859 try:
859 try:
860 PullRequestModel().edit(
860 PullRequestModel().edit(
861 pull_request, self.request.POST.get('title'),
861 pull_request, self.request.POST.get('title'),
862 self.request.POST.get('description'), self._rhodecode_user)
862 self.request.POST.get('description'), self._rhodecode_user)
863 except ValueError:
863 except ValueError:
864 msg = _(u'Cannot update closed pull requests.')
864 msg = _(u'Cannot update closed pull requests.')
865 h.flash(msg, category='error')
865 h.flash(msg, category='error')
866 return
866 return
867 else:
867 else:
868 Session().commit()
868 Session().commit()
869
869
870 msg = _(u'Pull request title & description updated.')
870 msg = _(u'Pull request title & description updated.')
871 h.flash(msg, category='success')
871 h.flash(msg, category='success')
872 return
872 return
873
873
874 def _update_commits(self, pull_request):
874 def _update_commits(self, pull_request):
875 _ = self.request.translate
875 _ = self.request.translate
876 resp = PullRequestModel().update_commits(pull_request)
876 resp = PullRequestModel().update_commits(pull_request)
877
877
878 if resp.executed:
878 if resp.executed:
879
879
880 if resp.target_changed and resp.source_changed:
880 if resp.target_changed and resp.source_changed:
881 changed = 'target and source repositories'
881 changed = 'target and source repositories'
882 elif resp.target_changed and not resp.source_changed:
882 elif resp.target_changed and not resp.source_changed:
883 changed = 'target repository'
883 changed = 'target repository'
884 elif not resp.target_changed and resp.source_changed:
884 elif not resp.target_changed and resp.source_changed:
885 changed = 'source repository'
885 changed = 'source repository'
886 else:
886 else:
887 changed = 'nothing'
887 changed = 'nothing'
888
888
889 msg = _(
889 msg = _(
890 u'Pull request updated to "{source_commit_id}" with '
890 u'Pull request updated to "{source_commit_id}" with '
891 u'{count_added} added, {count_removed} removed commits. '
891 u'{count_added} added, {count_removed} removed commits. '
892 u'Source of changes: {change_source}')
892 u'Source of changes: {change_source}')
893 msg = msg.format(
893 msg = msg.format(
894 source_commit_id=pull_request.source_ref_parts.commit_id,
894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 count_added=len(resp.changes.added),
895 count_added=len(resp.changes.added),
896 count_removed=len(resp.changes.removed),
896 count_removed=len(resp.changes.removed),
897 change_source=changed)
897 change_source=changed)
898 h.flash(msg, category='success')
898 h.flash(msg, category='success')
899
899
900 channel = '/repo${}$/pr/{}'.format(
900 channel = '/repo${}$/pr/{}'.format(
901 pull_request.target_repo.repo_name,
901 pull_request.target_repo.repo_name,
902 pull_request.pull_request_id)
902 pull_request.pull_request_id)
903 message = msg + (
903 message = msg + (
904 ' - <a onclick="window.location.reload()">'
904 ' - <a onclick="window.location.reload()">'
905 '<strong>{}</strong></a>'.format(_('Reload page')))
905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 channelstream.post_message(
906 channelstream.post_message(
907 channel, message, self._rhodecode_user.username,
907 channel, message, self._rhodecode_user.username,
908 registry=self.request.registry)
908 registry=self.request.registry)
909 else:
909 else:
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 warning_reasons = [
911 warning_reasons = [
912 UpdateFailureReason.NO_CHANGE,
912 UpdateFailureReason.NO_CHANGE,
913 UpdateFailureReason.WRONG_REF_TYPE,
913 UpdateFailureReason.WRONG_REF_TYPE,
914 ]
914 ]
915 category = 'warning' if resp.reason in warning_reasons else 'error'
915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 h.flash(msg, category=category)
916 h.flash(msg, category=category)
917
917
918 @LoginRequired()
918 @LoginRequired()
919 @NotAnonymous()
919 @NotAnonymous()
920 @HasRepoPermissionAnyDecorator(
920 @HasRepoPermissionAnyDecorator(
921 'repository.read', 'repository.write', 'repository.admin')
921 'repository.read', 'repository.write', 'repository.admin')
922 @CSRFRequired()
922 @CSRFRequired()
923 @view_config(
923 @view_config(
924 route_name='pullrequest_merge', request_method='POST',
924 route_name='pullrequest_merge', request_method='POST',
925 renderer='json_ext')
925 renderer='json_ext')
926 def pull_request_merge(self):
926 def pull_request_merge(self):
927 """
927 """
928 Merge will perform a server-side merge of the specified
928 Merge will perform a server-side merge of the specified
929 pull request, if the pull request is approved and mergeable.
929 pull request, if the pull request is approved and mergeable.
930 After successful merging, the pull request is automatically
930 After successful merging, the pull request is automatically
931 closed, with a relevant comment.
931 closed, with a relevant comment.
932 """
932 """
933 pull_request_id = self.request.matchdict['pull_request_id']
933 pull_request = PullRequest.get_or_404(
934 pull_request = PullRequest.get_or_404(pull_request_id)
934 self.request.matchdict['pull_request_id'])
935
935
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 merge_possible = not check.failed
937 merge_possible = not check.failed
938
938
939 for err_type, error_msg in check.errors:
939 for err_type, error_msg in check.errors:
940 h.flash(error_msg, category=err_type)
940 h.flash(error_msg, category=err_type)
941
941
942 if merge_possible:
942 if merge_possible:
943 log.debug("Pre-conditions checked, trying to merge.")
943 log.debug("Pre-conditions checked, trying to merge.")
944 extras = vcs_operation_context(
944 extras = vcs_operation_context(
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 username=self._rhodecode_db_user.username, action='push',
946 username=self._rhodecode_db_user.username, action='push',
947 scm=pull_request.target_repo.repo_type)
947 scm=pull_request.target_repo.repo_type)
948 self._merge_pull_request(
948 self._merge_pull_request(
949 pull_request, self._rhodecode_db_user, extras)
949 pull_request, self._rhodecode_db_user, extras)
950 else:
950 else:
951 log.debug("Pre-conditions failed, NOT merging.")
951 log.debug("Pre-conditions failed, NOT merging.")
952
952
953 raise HTTPFound(
953 raise HTTPFound(
954 h.route_path('pullrequest_show',
954 h.route_path('pullrequest_show',
955 repo_name=pull_request.target_repo.repo_name,
955 repo_name=pull_request.target_repo.repo_name,
956 pull_request_id=pull_request.pull_request_id))
956 pull_request_id=pull_request.pull_request_id))
957
957
958 def _merge_pull_request(self, pull_request, user, extras):
958 def _merge_pull_request(self, pull_request, user, extras):
959 _ = self.request.translate
959 _ = self.request.translate
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961
961
962 if merge_resp.executed:
962 if merge_resp.executed:
963 log.debug("The merge was successful, closing the pull request.")
963 log.debug("The merge was successful, closing the pull request.")
964 PullRequestModel().close_pull_request(
964 PullRequestModel().close_pull_request(
965 pull_request.pull_request_id, user)
965 pull_request.pull_request_id, user)
966 Session().commit()
966 Session().commit()
967 msg = _('Pull request was successfully merged and closed.')
967 msg = _('Pull request was successfully merged and closed.')
968 h.flash(msg, category='success')
968 h.flash(msg, category='success')
969 else:
969 else:
970 log.debug(
970 log.debug(
971 "The merge was not successful. Merge response: %s",
971 "The merge was not successful. Merge response: %s",
972 merge_resp)
972 merge_resp)
973 msg = PullRequestModel().merge_status_message(
973 msg = PullRequestModel().merge_status_message(
974 merge_resp.failure_reason)
974 merge_resp.failure_reason)
975 h.flash(msg, category='error')
975 h.flash(msg, category='error')
976
976
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
977 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
978 _ = self.request.translate
978 _ = self.request.translate
979 get_default_reviewers_data, validate_default_reviewers = \
979 get_default_reviewers_data, validate_default_reviewers = \
980 PullRequestModel().get_reviewer_functions()
980 PullRequestModel().get_reviewer_functions()
981
981
982 try:
982 try:
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 except ValueError as e:
984 except ValueError as e:
985 log.error('Reviewers Validation: {}'.format(e))
985 log.error('Reviewers Validation: {}'.format(e))
986 h.flash(e, category='error')
986 h.flash(e, category='error')
987 return
987 return
988
988
989 PullRequestModel().update_reviewers(
989 PullRequestModel().update_reviewers(
990 pull_request_id, reviewers, self._rhodecode_user)
990 pull_request, reviewers, self._rhodecode_user)
991 h.flash(_('Pull request reviewers updated.'), category='success')
991 h.flash(_('Pull request reviewers updated.'), category='success')
992 Session().commit()
992 Session().commit()
993
993
994 @LoginRequired()
994 @LoginRequired()
995 @NotAnonymous()
995 @NotAnonymous()
996 @HasRepoPermissionAnyDecorator(
996 @HasRepoPermissionAnyDecorator(
997 'repository.read', 'repository.write', 'repository.admin')
997 'repository.read', 'repository.write', 'repository.admin')
998 @CSRFRequired()
998 @CSRFRequired()
999 @view_config(
999 @view_config(
1000 route_name='pullrequest_delete', request_method='POST',
1000 route_name='pullrequest_delete', request_method='POST',
1001 renderer='json_ext')
1001 renderer='json_ext')
1002 def pull_request_delete(self):
1002 def pull_request_delete(self):
1003 _ = self.request.translate
1003 _ = self.request.translate
1004
1004
1005 pull_request_id = self.request.matchdict['pull_request_id']
1005 pull_request = PullRequest.get_or_404(
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1006 self.request.matchdict['pull_request_id'])
1007
1007
1008 pr_closed = pull_request.is_closed()
1008 pr_closed = pull_request.is_closed()
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 pull_request, self._rhodecode_user) and not pr_closed
1010 pull_request, self._rhodecode_user) and not pr_closed
1011
1011
1012 # only owner can delete it !
1012 # only owner can delete it !
1013 if allowed_to_delete:
1013 if allowed_to_delete:
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 Session().commit()
1015 Session().commit()
1016 h.flash(_('Successfully deleted pull request'),
1016 h.flash(_('Successfully deleted pull request'),
1017 category='success')
1017 category='success')
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019
1019
1020 log.warning('user %s tried to delete pull request without access',
1020 log.warning('user %s tried to delete pull request without access',
1021 self._rhodecode_user)
1021 self._rhodecode_user)
1022 raise HTTPNotFound()
1022 raise HTTPNotFound()
1023
1023
1024 @LoginRequired()
1024 @LoginRequired()
1025 @NotAnonymous()
1025 @NotAnonymous()
1026 @HasRepoPermissionAnyDecorator(
1026 @HasRepoPermissionAnyDecorator(
1027 'repository.read', 'repository.write', 'repository.admin')
1027 'repository.read', 'repository.write', 'repository.admin')
1028 @CSRFRequired()
1028 @CSRFRequired()
1029 @view_config(
1029 @view_config(
1030 route_name='pullrequest_comment_create', request_method='POST',
1030 route_name='pullrequest_comment_create', request_method='POST',
1031 renderer='json_ext')
1031 renderer='json_ext')
1032 def pull_request_comment_create(self):
1032 def pull_request_comment_create(self):
1033 _ = self.request.translate
1033 _ = self.request.translate
1034 pull_request_id = self.request.matchdict['pull_request_id']
1034
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1035 pull_request = PullRequest.get_or_404(
1036 self.request.matchdict['pull_request_id'])
1037 pull_request_id = pull_request.pull_request_id
1038
1036 if pull_request.is_closed():
1039 if pull_request.is_closed():
1037 log.debug('comment: forbidden because pull request is closed')
1040 log.debug('comment: forbidden because pull request is closed')
1038 raise HTTPForbidden()
1041 raise HTTPForbidden()
1039
1042
1040 c = self.load_default_context()
1043 c = self.load_default_context()
1041
1044
1042 status = self.request.POST.get('changeset_status', None)
1045 status = self.request.POST.get('changeset_status', None)
1043 text = self.request.POST.get('text')
1046 text = self.request.POST.get('text')
1044 comment_type = self.request.POST.get('comment_type')
1047 comment_type = self.request.POST.get('comment_type')
1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1048 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 close_pull_request = self.request.POST.get('close_pull_request')
1049 close_pull_request = self.request.POST.get('close_pull_request')
1047
1050
1048 # the logic here should work like following, if we submit close
1051 # the logic here should work like following, if we submit close
1049 # pr comment, use `close_pull_request_with_comment` function
1052 # pr comment, use `close_pull_request_with_comment` function
1050 # else handle regular comment logic
1053 # else handle regular comment logic
1051
1054
1052 if close_pull_request:
1055 if close_pull_request:
1053 # only owner or admin or person with write permissions
1056 # only owner or admin or person with write permissions
1054 allowed_to_close = PullRequestModel().check_user_update(
1057 allowed_to_close = PullRequestModel().check_user_update(
1055 pull_request, self._rhodecode_user)
1058 pull_request, self._rhodecode_user)
1056 if not allowed_to_close:
1059 if not allowed_to_close:
1057 log.debug('comment: forbidden because not allowed to close '
1060 log.debug('comment: forbidden because not allowed to close '
1058 'pull request %s', pull_request_id)
1061 'pull request %s', pull_request_id)
1059 raise HTTPForbidden()
1062 raise HTTPForbidden()
1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1063 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1064 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 Session().flush()
1065 Session().flush()
1063 events.trigger(
1066 events.trigger(
1064 events.PullRequestCommentEvent(pull_request, comment))
1067 events.PullRequestCommentEvent(pull_request, comment))
1065
1068
1066 else:
1069 else:
1067 # regular comment case, could be inline, or one with status.
1070 # regular comment case, could be inline, or one with status.
1068 # for that one we check also permissions
1071 # for that one we check also permissions
1069
1072
1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1073 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 pull_request, self._rhodecode_user)
1074 pull_request, self._rhodecode_user)
1072
1075
1073 if status and allowed_to_change_status:
1076 if status and allowed_to_change_status:
1074 message = (_('Status change %(transition_icon)s %(status)s')
1077 message = (_('Status change %(transition_icon)s %(status)s')
1075 % {'transition_icon': '>',
1078 % {'transition_icon': '>',
1076 'status': ChangesetStatus.get_status_lbl(status)})
1079 'status': ChangesetStatus.get_status_lbl(status)})
1077 text = text or message
1080 text = text or message
1078
1081
1079 comment = CommentsModel().create(
1082 comment = CommentsModel().create(
1080 text=text,
1083 text=text,
1081 repo=self.db_repo.repo_id,
1084 repo=self.db_repo.repo_id,
1082 user=self._rhodecode_user.user_id,
1085 user=self._rhodecode_user.user_id,
1083 pull_request=pull_request_id,
1086 pull_request=pull_request,
1084 f_path=self.request.POST.get('f_path'),
1087 f_path=self.request.POST.get('f_path'),
1085 line_no=self.request.POST.get('line'),
1088 line_no=self.request.POST.get('line'),
1086 status_change=(ChangesetStatus.get_status_lbl(status)
1089 status_change=(ChangesetStatus.get_status_lbl(status)
1087 if status and allowed_to_change_status else None),
1090 if status and allowed_to_change_status else None),
1088 status_change_type=(status
1091 status_change_type=(status
1089 if status and allowed_to_change_status else None),
1092 if status and allowed_to_change_status else None),
1090 comment_type=comment_type,
1093 comment_type=comment_type,
1091 resolves_comment_id=resolves_comment_id
1094 resolves_comment_id=resolves_comment_id
1092 )
1095 )
1093
1096
1094 if allowed_to_change_status:
1097 if allowed_to_change_status:
1095 # calculate old status before we change it
1098 # calculate old status before we change it
1096 old_calculated_status = pull_request.calculated_review_status()
1099 old_calculated_status = pull_request.calculated_review_status()
1097
1100
1098 # get status if set !
1101 # get status if set !
1099 if status:
1102 if status:
1100 ChangesetStatusModel().set_status(
1103 ChangesetStatusModel().set_status(
1101 self.db_repo.repo_id,
1104 self.db_repo.repo_id,
1102 status,
1105 status,
1103 self._rhodecode_user.user_id,
1106 self._rhodecode_user.user_id,
1104 comment,
1107 comment,
1105 pull_request=pull_request_id
1108 pull_request=pull_request
1106 )
1109 )
1107
1110
1108 Session().flush()
1111 Session().flush()
1109 events.trigger(
1112 events.trigger(
1110 events.PullRequestCommentEvent(pull_request, comment))
1113 events.PullRequestCommentEvent(pull_request, comment))
1111
1114
1112 # we now calculate the status of pull request, and based on that
1115 # we now calculate the status of pull request, and based on that
1113 # calculation we set the commits status
1116 # calculation we set the commits status
1114 calculated_status = pull_request.calculated_review_status()
1117 calculated_status = pull_request.calculated_review_status()
1115 if old_calculated_status != calculated_status:
1118 if old_calculated_status != calculated_status:
1116 PullRequestModel()._trigger_pull_request_hook(
1119 PullRequestModel()._trigger_pull_request_hook(
1117 pull_request, self._rhodecode_user, 'review_status_change')
1120 pull_request, self._rhodecode_user, 'review_status_change')
1118
1121
1119 Session().commit()
1122 Session().commit()
1120
1123
1121 data = {
1124 data = {
1122 'target_id': h.safeid(h.safe_unicode(
1125 'target_id': h.safeid(h.safe_unicode(
1123 self.request.POST.get('f_path'))),
1126 self.request.POST.get('f_path'))),
1124 }
1127 }
1125 if comment:
1128 if comment:
1126 c.co = comment
1129 c.co = comment
1127 rendered_comment = render(
1130 rendered_comment = render(
1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1131 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 self._get_template_context(c), self.request)
1132 self._get_template_context(c), self.request)
1130
1133
1131 data.update(comment.get_dict())
1134 data.update(comment.get_dict())
1132 data.update({'rendered_text': rendered_comment})
1135 data.update({'rendered_text': rendered_comment})
1133
1136
1134 return data
1137 return data
1135
1138
1136 @LoginRequired()
1139 @LoginRequired()
1137 @NotAnonymous()
1140 @NotAnonymous()
1138 @HasRepoPermissionAnyDecorator(
1141 @HasRepoPermissionAnyDecorator(
1139 'repository.read', 'repository.write', 'repository.admin')
1142 'repository.read', 'repository.write', 'repository.admin')
1140 @CSRFRequired()
1143 @CSRFRequired()
1141 @view_config(
1144 @view_config(
1142 route_name='pullrequest_comment_delete', request_method='POST',
1145 route_name='pullrequest_comment_delete', request_method='POST',
1143 renderer='json_ext')
1146 renderer='json_ext')
1144 def pull_request_comment_delete(self):
1147 def pull_request_comment_delete(self):
1145 pull_request_id = self.request.matchdict['pull_request_id']
1148 pull_request = PullRequest.get_or_404(
1146 comment_id = self.request.matchdict['comment_id']
1149 self.request.matchdict['pull_request_id'])
1147
1150
1148 pull_request = PullRequest.get_or_404(pull_request_id)
1151 comment = ChangesetComment.get_or_404(
1152 self.request.matchdict['comment_id'])
1153 comment_id = comment.comment_id
1154
1149 if pull_request.is_closed():
1155 if pull_request.is_closed():
1150 log.debug('comment: forbidden because pull request is closed')
1156 log.debug('comment: forbidden because pull request is closed')
1151 raise HTTPForbidden()
1157 raise HTTPForbidden()
1152
1158
1153 comment = ChangesetComment.get_or_404(comment_id)
1154 if not comment:
1159 if not comment:
1155 log.debug('Comment with id:%s not found, skipping', comment_id)
1160 log.debug('Comment with id:%s not found, skipping', comment_id)
1156 # comment already deleted in another call probably
1161 # comment already deleted in another call probably
1157 return True
1162 return True
1158
1163
1159 if comment.pull_request.is_closed():
1164 if comment.pull_request.is_closed():
1160 # don't allow deleting comments on closed pull request
1165 # don't allow deleting comments on closed pull request
1161 raise HTTPForbidden()
1166 raise HTTPForbidden()
1162
1167
1163 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1168 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1164 super_admin = h.HasPermissionAny('hg.admin')()
1169 super_admin = h.HasPermissionAny('hg.admin')()
1165 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1170 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1166 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1171 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1167 comment_repo_admin = is_repo_admin and is_repo_comment
1172 comment_repo_admin = is_repo_admin and is_repo_comment
1168
1173
1169 if super_admin or comment_owner or comment_repo_admin:
1174 if super_admin or comment_owner or comment_repo_admin:
1170 old_calculated_status = comment.pull_request.calculated_review_status()
1175 old_calculated_status = comment.pull_request.calculated_review_status()
1171 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1176 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1172 Session().commit()
1177 Session().commit()
1173 calculated_status = comment.pull_request.calculated_review_status()
1178 calculated_status = comment.pull_request.calculated_review_status()
1174 if old_calculated_status != calculated_status:
1179 if old_calculated_status != calculated_status:
1175 PullRequestModel()._trigger_pull_request_hook(
1180 PullRequestModel()._trigger_pull_request_hook(
1176 comment.pull_request, self._rhodecode_user, 'review_status_change')
1181 comment.pull_request, self._rhodecode_user, 'review_status_change')
1177 return True
1182 return True
1178 else:
1183 else:
1179 log.warning('No permissions for user %s to delete comment_id: %s',
1184 log.warning('No permissions for user %s to delete comment_id: %s',
1180 self._rhodecode_db_user, comment_id)
1185 self._rhodecode_db_user, comment_id)
1181 raise HTTPNotFound()
1186 raise HTTPNotFound()
@@ -1,831 +1,830 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.closesPr = '#close_pull_request';
95 this.closesPr = '#close_pull_request';
96
96
97 this.cmBox = this.withLineNo('#text');
97 this.cmBox = this.withLineNo('#text');
98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
99
99
100 this.statusChange = this.withLineNo('#change_status');
100 this.statusChange = this.withLineNo('#change_status');
101
101
102 this.submitForm = formElement;
102 this.submitForm = formElement;
103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
104 this.submitButtonText = this.submitButton.val();
104 this.submitButtonText = this.submitButton.val();
105
105
106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
107 {'repo_name': templateContext.repo_name,
107 {'repo_name': templateContext.repo_name,
108 'commit_id': templateContext.commit_data.commit_id});
108 'commit_id': templateContext.commit_data.commit_id});
109
109
110 if (resolvesCommentId){
110 if (resolvesCommentId){
111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
113 $(this.commentType).prop('disabled', true);
113 $(this.commentType).prop('disabled', true);
114 $(this.commentType).addClass('disabled');
114 $(this.commentType).addClass('disabled');
115
115
116 // disable select
116 // disable select
117 setTimeout(function() {
117 setTimeout(function() {
118 $(self.statusChange).select2('readonly', true);
118 $(self.statusChange).select2('readonly', true);
119 }, 10);
119 }, 10);
120
120
121 var resolvedInfo = (
121 var resolvedInfo = (
122 '<li class="resolve-action">' +
122 '<li class="resolve-action">' +
123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
125 '</li>'
125 '</li>'
126 ).format(resolvesCommentId, _gettext('resolve comment'));
126 ).format(resolvesCommentId, _gettext('resolve comment'));
127 $(resolvedInfo).insertAfter($(this.commentType).parent());
127 $(resolvedInfo).insertAfter($(this.commentType).parent());
128 }
128 }
129
129
130 // based on commitId, or pullRequestId decide where do we submit
130 // based on commitId, or pullRequestId decide where do we submit
131 // out data
131 // out data
132 if (this.commitId){
132 if (this.commitId){
133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
134 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
135 'commit_id': this.commitId});
135 'commit_id': this.commitId});
136 this.selfUrl = pyroutes.url('repo_commit',
136 this.selfUrl = pyroutes.url('repo_commit',
137 {'repo_name': templateContext.repo_name,
137 {'repo_name': templateContext.repo_name,
138 'commit_id': this.commitId});
138 'commit_id': this.commitId});
139
139
140 } else if (this.pullRequestId) {
140 } else if (this.pullRequestId) {
141 this.submitUrl = pyroutes.url('pullrequest_comment_create',
141 this.submitUrl = pyroutes.url('pullrequest_comment_create',
142 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
144 this.selfUrl = pyroutes.url('pullrequest_show',
144 this.selfUrl = pyroutes.url('pullrequest_show',
145 {'repo_name': templateContext.repo_name,
145 {'repo_name': templateContext.repo_name,
146 'pull_request_id': this.pullRequestId});
146 'pull_request_id': this.pullRequestId});
147
147
148 } else {
148 } else {
149 throw new Error(
149 throw new Error(
150 'CommentForm requires pullRequestId, or commitId to be specified.')
150 'CommentForm requires pullRequestId, or commitId to be specified.')
151 }
151 }
152
152
153 // FUNCTIONS and helpers
153 // FUNCTIONS and helpers
154 var self = this;
154 var self = this;
155
155
156 this.isInline = function(){
156 this.isInline = function(){
157 return this.lineNo && this.lineNo != 'general';
157 return this.lineNo && this.lineNo != 'general';
158 };
158 };
159
159
160 this.getCmInstance = function(){
160 this.getCmInstance = function(){
161 return this.cm
161 return this.cm
162 };
162 };
163
163
164 this.setPlaceholder = function(placeholder) {
164 this.setPlaceholder = function(placeholder) {
165 var cm = this.getCmInstance();
165 var cm = this.getCmInstance();
166 if (cm){
166 if (cm){
167 cm.setOption('placeholder', placeholder);
167 cm.setOption('placeholder', placeholder);
168 }
168 }
169 };
169 };
170
170
171 this.getCommentStatus = function() {
171 this.getCommentStatus = function() {
172 return $(this.submitForm).find(this.statusChange).val();
172 return $(this.submitForm).find(this.statusChange).val();
173 };
173 };
174 this.getCommentType = function() {
174 this.getCommentType = function() {
175 return $(this.submitForm).find(this.commentType).val();
175 return $(this.submitForm).find(this.commentType).val();
176 };
176 };
177
177
178 this.getResolvesId = function() {
178 this.getResolvesId = function() {
179 return $(this.submitForm).find(this.resolvesId).val() || null;
179 return $(this.submitForm).find(this.resolvesId).val() || null;
180 };
180 };
181
181
182 this.getClosePr = function() {
182 this.getClosePr = function() {
183 return $(this.submitForm).find(this.closesPr).val() || null;
183 return $(this.submitForm).find(this.closesPr).val() || null;
184 };
184 };
185
185
186 this.markCommentResolved = function(resolvedCommentId){
186 this.markCommentResolved = function(resolvedCommentId){
187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
189 };
189 };
190
190
191 this.isAllowedToSubmit = function() {
191 this.isAllowedToSubmit = function() {
192 return !$(this.submitButton).prop('disabled');
192 return !$(this.submitButton).prop('disabled');
193 };
193 };
194
194
195 this.initStatusChangeSelector = function(){
195 this.initStatusChangeSelector = function(){
196 var formatChangeStatus = function(state, escapeMarkup) {
196 var formatChangeStatus = function(state, escapeMarkup) {
197 var originalOption = state.element;
197 var originalOption = state.element;
198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
199 '<span>' + escapeMarkup(state.text) + '</span>';
199 '<span>' + escapeMarkup(state.text) + '</span>';
200 };
200 };
201 var formatResult = function(result, container, query, escapeMarkup) {
201 var formatResult = function(result, container, query, escapeMarkup) {
202 return formatChangeStatus(result, escapeMarkup);
202 return formatChangeStatus(result, escapeMarkup);
203 };
203 };
204
204
205 var formatSelection = function(data, container, escapeMarkup) {
205 var formatSelection = function(data, container, escapeMarkup) {
206 return formatChangeStatus(data, escapeMarkup);
206 return formatChangeStatus(data, escapeMarkup);
207 };
207 };
208
208
209 $(this.submitForm).find(this.statusChange).select2({
209 $(this.submitForm).find(this.statusChange).select2({
210 placeholder: _gettext('Status Review'),
210 placeholder: _gettext('Status Review'),
211 formatResult: formatResult,
211 formatResult: formatResult,
212 formatSelection: formatSelection,
212 formatSelection: formatSelection,
213 containerCssClass: "drop-menu status_box_menu",
213 containerCssClass: "drop-menu status_box_menu",
214 dropdownCssClass: "drop-menu-dropdown",
214 dropdownCssClass: "drop-menu-dropdown",
215 dropdownAutoWidth: true,
215 dropdownAutoWidth: true,
216 minimumResultsForSearch: -1
216 minimumResultsForSearch: -1
217 });
217 });
218 $(this.submitForm).find(this.statusChange).on('change', function() {
218 $(this.submitForm).find(this.statusChange).on('change', function() {
219 var status = self.getCommentStatus();
219 var status = self.getCommentStatus();
220
220
221 if (status && !self.isInline()) {
221 if (status && !self.isInline()) {
222 $(self.submitButton).prop('disabled', false);
222 $(self.submitButton).prop('disabled', false);
223 }
223 }
224
224
225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
226 self.setPlaceholder(placeholderText)
226 self.setPlaceholder(placeholderText)
227 })
227 })
228 };
228 };
229
229
230 // reset the comment form into it's original state
230 // reset the comment form into it's original state
231 this.resetCommentFormState = function(content) {
231 this.resetCommentFormState = function(content) {
232 content = content || '';
232 content = content || '';
233
233
234 $(this.editContainer).show();
234 $(this.editContainer).show();
235 $(this.editButton).parent().addClass('active');
235 $(this.editButton).parent().addClass('active');
236
236
237 $(this.previewContainer).hide();
237 $(this.previewContainer).hide();
238 $(this.previewButton).parent().removeClass('active');
238 $(this.previewButton).parent().removeClass('active');
239
239
240 this.setActionButtonsDisabled(true);
240 this.setActionButtonsDisabled(true);
241 self.cm.setValue(content);
241 self.cm.setValue(content);
242 self.cm.setOption("readOnly", false);
242 self.cm.setOption("readOnly", false);
243
243
244 if (this.resolvesId) {
244 if (this.resolvesId) {
245 // destroy the resolve action
245 // destroy the resolve action
246 $(this.resolvesId).parent().remove();
246 $(this.resolvesId).parent().remove();
247 }
247 }
248 // reset closingPR flag
248 // reset closingPR flag
249 $('.close-pr-input').remove();
249 $('.close-pr-input').remove();
250
250
251 $(this.statusChange).select2('readonly', false);
251 $(this.statusChange).select2('readonly', false);
252 };
252 };
253
253
254 this.globalSubmitSuccessCallback = function(){
254 this.globalSubmitSuccessCallback = function(){
255 // default behaviour is to call GLOBAL hook, if it's registered.
255 // default behaviour is to call GLOBAL hook, if it's registered.
256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
257 commentFormGlobalSubmitSuccessCallback()
257 commentFormGlobalSubmitSuccessCallback()
258 }
258 }
259 };
259 };
260
260
261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
262 failHandler = failHandler || function() {};
262 failHandler = failHandler || function() {};
263 var postData = toQueryString(postData);
263 var postData = toQueryString(postData);
264 var request = $.ajax({
264 var request = $.ajax({
265 url: url,
265 url: url,
266 type: 'POST',
266 type: 'POST',
267 data: postData,
267 data: postData,
268 headers: {'X-PARTIAL-XHR': true}
268 headers: {'X-PARTIAL-XHR': true}
269 })
269 })
270 .done(function(data) {
270 .done(function(data) {
271 successHandler(data);
271 successHandler(data);
272 })
272 })
273 .fail(function(data, textStatus, errorThrown){
273 .fail(function(data, textStatus, errorThrown){
274 alert(
274 alert(
275 "Error while submitting comment.\n" +
275 "Error while submitting comment.\n" +
276 "Error code {0} ({1}).".format(data.status, data.statusText));
276 "Error code {0} ({1}).".format(data.status, data.statusText));
277 failHandler()
277 failHandler()
278 });
278 });
279 return request;
279 return request;
280 };
280 };
281
281
282 // overwrite a submitHandler, we need to do it for inline comments
282 // overwrite a submitHandler, we need to do it for inline comments
283 this.setHandleFormSubmit = function(callback) {
283 this.setHandleFormSubmit = function(callback) {
284 this.handleFormSubmit = callback;
284 this.handleFormSubmit = callback;
285 };
285 };
286
286
287 // overwrite a submitSuccessHandler
287 // overwrite a submitSuccessHandler
288 this.setGlobalSubmitSuccessCallback = function(callback) {
288 this.setGlobalSubmitSuccessCallback = function(callback) {
289 this.globalSubmitSuccessCallback = callback;
289 this.globalSubmitSuccessCallback = callback;
290 };
290 };
291
291
292 // default handler for for submit for main comments
292 // default handler for for submit for main comments
293 this.handleFormSubmit = function() {
293 this.handleFormSubmit = function() {
294 var text = self.cm.getValue();
294 var text = self.cm.getValue();
295 var status = self.getCommentStatus();
295 var status = self.getCommentStatus();
296 var commentType = self.getCommentType();
296 var commentType = self.getCommentType();
297 var resolvesCommentId = self.getResolvesId();
297 var resolvesCommentId = self.getResolvesId();
298 var closePullRequest = self.getClosePr();
298 var closePullRequest = self.getClosePr();
299
299
300 if (text === "" && !status) {
300 if (text === "" && !status) {
301 return;
301 return;
302 }
302 }
303
303
304 var excludeCancelBtn = false;
304 var excludeCancelBtn = false;
305 var submitEvent = true;
305 var submitEvent = true;
306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
307 self.cm.setOption("readOnly", true);
307 self.cm.setOption("readOnly", true);
308
308
309 var postData = {
309 var postData = {
310 'text': text,
310 'text': text,
311 'changeset_status': status,
311 'changeset_status': status,
312 'comment_type': commentType,
312 'comment_type': commentType,
313 'csrf_token': CSRF_TOKEN
313 'csrf_token': CSRF_TOKEN
314 };
314 };
315
315
316 if (resolvesCommentId) {
316 if (resolvesCommentId) {
317 postData['resolves_comment_id'] = resolvesCommentId;
317 postData['resolves_comment_id'] = resolvesCommentId;
318 }
318 }
319
319
320 if (closePullRequest) {
320 if (closePullRequest) {
321 postData['close_pull_request'] = true;
321 postData['close_pull_request'] = true;
322 }
322 }
323
323
324 var submitSuccessCallback = function(o) {
324 var submitSuccessCallback = function(o) {
325 // reload page if we change status for single commit.
325 // reload page if we change status for single commit.
326 if (status && self.commitId) {
326 if (status && self.commitId) {
327 location.reload(true);
327 location.reload(true);
328 } else {
328 } else {
329 $('#injected_page_comments').append(o.rendered_text);
329 $('#injected_page_comments').append(o.rendered_text);
330 self.resetCommentFormState();
330 self.resetCommentFormState();
331 timeagoActivate();
331 timeagoActivate();
332
332
333 // mark visually which comment was resolved
333 // mark visually which comment was resolved
334 if (resolvesCommentId) {
334 if (resolvesCommentId) {
335 self.markCommentResolved(resolvesCommentId);
335 self.markCommentResolved(resolvesCommentId);
336 }
336 }
337 }
337 }
338
338
339 // run global callback on submit
339 // run global callback on submit
340 self.globalSubmitSuccessCallback();
340 self.globalSubmitSuccessCallback();
341
341
342 };
342 };
343 var submitFailCallback = function(){
343 var submitFailCallback = function(){
344 self.resetCommentFormState(text);
344 self.resetCommentFormState(text);
345 };
345 };
346 self.submitAjaxPOST(
346 self.submitAjaxPOST(
347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
348 };
348 };
349
349
350 this.previewSuccessCallback = function(o) {
350 this.previewSuccessCallback = function(o) {
351 $(self.previewBoxSelector).html(o);
351 $(self.previewBoxSelector).html(o);
352 $(self.previewBoxSelector).removeClass('unloaded');
352 $(self.previewBoxSelector).removeClass('unloaded');
353
353
354 // swap buttons, making preview active
354 // swap buttons, making preview active
355 $(self.previewButton).parent().addClass('active');
355 $(self.previewButton).parent().addClass('active');
356 $(self.editButton).parent().removeClass('active');
356 $(self.editButton).parent().removeClass('active');
357
357
358 // unlock buttons
358 // unlock buttons
359 self.setActionButtonsDisabled(false);
359 self.setActionButtonsDisabled(false);
360 };
360 };
361
361
362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
363 excludeCancelBtn = excludeCancelBtn || false;
363 excludeCancelBtn = excludeCancelBtn || false;
364 submitEvent = submitEvent || false;
364 submitEvent = submitEvent || false;
365
365
366 $(this.editButton).prop('disabled', state);
366 $(this.editButton).prop('disabled', state);
367 $(this.previewButton).prop('disabled', state);
367 $(this.previewButton).prop('disabled', state);
368
368
369 if (!excludeCancelBtn) {
369 if (!excludeCancelBtn) {
370 $(this.cancelButton).prop('disabled', state);
370 $(this.cancelButton).prop('disabled', state);
371 }
371 }
372
372
373 var submitState = state;
373 var submitState = state;
374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
375 // if the value of commit review status is set, we allow
375 // if the value of commit review status is set, we allow
376 // submit button, but only on Main form, isInline means inline
376 // submit button, but only on Main form, isInline means inline
377 submitState = false
377 submitState = false
378 }
378 }
379
379
380 $(this.submitButton).prop('disabled', submitState);
380 $(this.submitButton).prop('disabled', submitState);
381 if (submitEvent) {
381 if (submitEvent) {
382 $(this.submitButton).val(_gettext('Submitting...'));
382 $(this.submitButton).val(_gettext('Submitting...'));
383 } else {
383 } else {
384 $(this.submitButton).val(this.submitButtonText);
384 $(this.submitButton).val(this.submitButtonText);
385 }
385 }
386
386
387 };
387 };
388
388
389 // lock preview/edit/submit buttons on load, but exclude cancel button
389 // lock preview/edit/submit buttons on load, but exclude cancel button
390 var excludeCancelBtn = true;
390 var excludeCancelBtn = true;
391 this.setActionButtonsDisabled(true, excludeCancelBtn);
391 this.setActionButtonsDisabled(true, excludeCancelBtn);
392
392
393 // anonymous users don't have access to initialized CM instance
393 // anonymous users don't have access to initialized CM instance
394 if (this.cm !== undefined){
394 if (this.cm !== undefined){
395 this.cm.on('change', function(cMirror) {
395 this.cm.on('change', function(cMirror) {
396 if (cMirror.getValue() === "") {
396 if (cMirror.getValue() === "") {
397 self.setActionButtonsDisabled(true, excludeCancelBtn)
397 self.setActionButtonsDisabled(true, excludeCancelBtn)
398 } else {
398 } else {
399 self.setActionButtonsDisabled(false, excludeCancelBtn)
399 self.setActionButtonsDisabled(false, excludeCancelBtn)
400 }
400 }
401 });
401 });
402 }
402 }
403
403
404 $(this.editButton).on('click', function(e) {
404 $(this.editButton).on('click', function(e) {
405 e.preventDefault();
405 e.preventDefault();
406
406
407 $(self.previewButton).parent().removeClass('active');
407 $(self.previewButton).parent().removeClass('active');
408 $(self.previewContainer).hide();
408 $(self.previewContainer).hide();
409
409
410 $(self.editButton).parent().addClass('active');
410 $(self.editButton).parent().addClass('active');
411 $(self.editContainer).show();
411 $(self.editContainer).show();
412
412
413 });
413 });
414
414
415 $(this.previewButton).on('click', function(e) {
415 $(this.previewButton).on('click', function(e) {
416 e.preventDefault();
416 e.preventDefault();
417 var text = self.cm.getValue();
417 var text = self.cm.getValue();
418
418
419 if (text === "") {
419 if (text === "") {
420 return;
420 return;
421 }
421 }
422
422
423 var postData = {
423 var postData = {
424 'text': text,
424 'text': text,
425 'renderer': templateContext.visual.default_renderer,
425 'renderer': templateContext.visual.default_renderer,
426 'csrf_token': CSRF_TOKEN
426 'csrf_token': CSRF_TOKEN
427 };
427 };
428
428
429 // lock ALL buttons on preview
429 // lock ALL buttons on preview
430 self.setActionButtonsDisabled(true);
430 self.setActionButtonsDisabled(true);
431
431
432 $(self.previewBoxSelector).addClass('unloaded');
432 $(self.previewBoxSelector).addClass('unloaded');
433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
434
434
435 $(self.editContainer).hide();
435 $(self.editContainer).hide();
436 $(self.previewContainer).show();
436 $(self.previewContainer).show();
437
437
438 // by default we reset state of comment preserving the text
438 // by default we reset state of comment preserving the text
439 var previewFailCallback = function(){
439 var previewFailCallback = function(){
440 self.resetCommentFormState(text)
440 self.resetCommentFormState(text)
441 };
441 };
442 self.submitAjaxPOST(
442 self.submitAjaxPOST(
443 self.previewUrl, postData, self.previewSuccessCallback,
443 self.previewUrl, postData, self.previewSuccessCallback,
444 previewFailCallback);
444 previewFailCallback);
445
445
446 $(self.previewButton).parent().addClass('active');
446 $(self.previewButton).parent().addClass('active');
447 $(self.editButton).parent().removeClass('active');
447 $(self.editButton).parent().removeClass('active');
448 });
448 });
449
449
450 $(this.submitForm).submit(function(e) {
450 $(this.submitForm).submit(function(e) {
451 e.preventDefault();
451 e.preventDefault();
452 var allowedToSubmit = self.isAllowedToSubmit();
452 var allowedToSubmit = self.isAllowedToSubmit();
453 if (!allowedToSubmit){
453 if (!allowedToSubmit){
454 return false;
454 return false;
455 }
455 }
456 self.handleFormSubmit();
456 self.handleFormSubmit();
457 });
457 });
458
458
459 }
459 }
460
460
461 return CommentForm;
461 return CommentForm;
462 });
462 });
463
463
464 /* comments controller */
464 /* comments controller */
465 var CommentsController = function() {
465 var CommentsController = function() {
466 var mainComment = '#text';
466 var mainComment = '#text';
467 var self = this;
467 var self = this;
468
468
469 this.cancelComment = function(node) {
469 this.cancelComment = function(node) {
470 var $node = $(node);
470 var $node = $(node);
471 var $td = $node.closest('td');
471 var $td = $node.closest('td');
472 $node.closest('.comment-inline-form').remove();
472 $node.closest('.comment-inline-form').remove();
473 return false;
473 return false;
474 };
474 };
475
475
476 this.getLineNumber = function(node) {
476 this.getLineNumber = function(node) {
477 var $node = $(node);
477 var $node = $(node);
478 return $node.closest('td').attr('data-line-number');
478 return $node.closest('td').attr('data-line-number');
479 };
479 };
480
480
481 this.scrollToComment = function(node, offset, outdated) {
481 this.scrollToComment = function(node, offset, outdated) {
482 if (offset === undefined) {
482 if (offset === undefined) {
483 offset = 0;
483 offset = 0;
484 }
484 }
485 var outdated = outdated || false;
485 var outdated = outdated || false;
486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
487
487
488 if (!node) {
488 if (!node) {
489 node = $('.comment-selected');
489 node = $('.comment-selected');
490 if (!node.length) {
490 if (!node.length) {
491 node = $('comment-current')
491 node = $('comment-current')
492 }
492 }
493 }
493 }
494 $wrapper = $(node).closest('div.comment');
494 $wrapper = $(node).closest('div.comment');
495 $comment = $(node).closest(klass);
495 $comment = $(node).closest(klass);
496 $comments = $(klass);
496 $comments = $(klass);
497
497
498 // show hidden comment when referenced.
498 // show hidden comment when referenced.
499 if (!$wrapper.is(':visible')){
499 if (!$wrapper.is(':visible')){
500 $wrapper.show();
500 $wrapper.show();
501 }
501 }
502
502
503 $('.comment-selected').removeClass('comment-selected');
503 $('.comment-selected').removeClass('comment-selected');
504
504
505 var nextIdx = $(klass).index($comment) + offset;
505 var nextIdx = $(klass).index($comment) + offset;
506 if (nextIdx >= $comments.length) {
506 if (nextIdx >= $comments.length) {
507 nextIdx = 0;
507 nextIdx = 0;
508 }
508 }
509 var $next = $(klass).eq(nextIdx);
509 var $next = $(klass).eq(nextIdx);
510
510
511 var $cb = $next.closest('.cb');
511 var $cb = $next.closest('.cb');
512 $cb.removeClass('cb-collapsed');
512 $cb.removeClass('cb-collapsed');
513
513
514 var $filediffCollapseState = $cb.closest('.filediff').prev();
514 var $filediffCollapseState = $cb.closest('.filediff').prev();
515 $filediffCollapseState.prop('checked', false);
515 $filediffCollapseState.prop('checked', false);
516 $next.addClass('comment-selected');
516 $next.addClass('comment-selected');
517 scrollToElement($next);
517 scrollToElement($next);
518 return false;
518 return false;
519 };
519 };
520
520
521 this.nextComment = function(node) {
521 this.nextComment = function(node) {
522 return self.scrollToComment(node, 1);
522 return self.scrollToComment(node, 1);
523 };
523 };
524
524
525 this.prevComment = function(node) {
525 this.prevComment = function(node) {
526 return self.scrollToComment(node, -1);
526 return self.scrollToComment(node, -1);
527 };
527 };
528
528
529 this.nextOutdatedComment = function(node) {
529 this.nextOutdatedComment = function(node) {
530 return self.scrollToComment(node, 1, true);
530 return self.scrollToComment(node, 1, true);
531 };
531 };
532
532
533 this.prevOutdatedComment = function(node) {
533 this.prevOutdatedComment = function(node) {
534 return self.scrollToComment(node, -1, true);
534 return self.scrollToComment(node, -1, true);
535 };
535 };
536
536
537 this.deleteComment = function(node) {
537 this.deleteComment = function(node) {
538 if (!confirm(_gettext('Delete this comment?'))) {
538 if (!confirm(_gettext('Delete this comment?'))) {
539 return false;
539 return false;
540 }
540 }
541 var $node = $(node);
541 var $node = $(node);
542 var $td = $node.closest('td');
542 var $td = $node.closest('td');
543 var $comment = $node.closest('.comment');
543 var $comment = $node.closest('.comment');
544 var comment_id = $comment.attr('data-comment-id');
544 var comment_id = $comment.attr('data-comment-id');
545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
546 var postData = {
546 var postData = {
547 '_method': 'delete',
548 'csrf_token': CSRF_TOKEN
547 'csrf_token': CSRF_TOKEN
549 };
548 };
550
549
551 $comment.addClass('comment-deleting');
550 $comment.addClass('comment-deleting');
552 $comment.hide('fast');
551 $comment.hide('fast');
553
552
554 var success = function(response) {
553 var success = function(response) {
555 $comment.remove();
554 $comment.remove();
556 return false;
555 return false;
557 };
556 };
558 var failure = function(data, textStatus, xhr) {
557 var failure = function(data, textStatus, xhr) {
559 alert("error processing request: " + textStatus);
558 alert("error processing request: " + textStatus);
560 $comment.show('fast');
559 $comment.show('fast');
561 $comment.removeClass('comment-deleting');
560 $comment.removeClass('comment-deleting');
562 return false;
561 return false;
563 };
562 };
564 ajaxPOST(url, postData, success, failure);
563 ajaxPOST(url, postData, success, failure);
565 };
564 };
566
565
567 this.toggleWideMode = function (node) {
566 this.toggleWideMode = function (node) {
568 if ($('#content').hasClass('wrapper')) {
567 if ($('#content').hasClass('wrapper')) {
569 $('#content').removeClass("wrapper");
568 $('#content').removeClass("wrapper");
570 $('#content').addClass("wide-mode-wrapper");
569 $('#content').addClass("wide-mode-wrapper");
571 $(node).addClass('btn-success');
570 $(node).addClass('btn-success');
572 } else {
571 } else {
573 $('#content').removeClass("wide-mode-wrapper");
572 $('#content').removeClass("wide-mode-wrapper");
574 $('#content').addClass("wrapper");
573 $('#content').addClass("wrapper");
575 $(node).removeClass('btn-success');
574 $(node).removeClass('btn-success');
576 }
575 }
577 return false;
576 return false;
578 };
577 };
579
578
580 this.toggleComments = function(node, show) {
579 this.toggleComments = function(node, show) {
581 var $filediff = $(node).closest('.filediff');
580 var $filediff = $(node).closest('.filediff');
582 if (show === true) {
581 if (show === true) {
583 $filediff.removeClass('hide-comments');
582 $filediff.removeClass('hide-comments');
584 } else if (show === false) {
583 } else if (show === false) {
585 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
584 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
586 $filediff.addClass('hide-comments');
585 $filediff.addClass('hide-comments');
587 } else {
586 } else {
588 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
587 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
589 $filediff.toggleClass('hide-comments');
588 $filediff.toggleClass('hide-comments');
590 }
589 }
591 return false;
590 return false;
592 };
591 };
593
592
594 this.toggleLineComments = function(node) {
593 this.toggleLineComments = function(node) {
595 self.toggleComments(node, true);
594 self.toggleComments(node, true);
596 var $node = $(node);
595 var $node = $(node);
597 $node.closest('tr').toggleClass('hide-line-comments');
596 $node.closest('tr').toggleClass('hide-line-comments');
598 };
597 };
599
598
600 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
599 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
601 var pullRequestId = templateContext.pull_request_data.pull_request_id;
600 var pullRequestId = templateContext.pull_request_data.pull_request_id;
602 var commitId = templateContext.commit_data.commit_id;
601 var commitId = templateContext.commit_data.commit_id;
603
602
604 var commentForm = new CommentForm(
603 var commentForm = new CommentForm(
605 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
604 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
606 var cm = commentForm.getCmInstance();
605 var cm = commentForm.getCmInstance();
607
606
608 if (resolvesCommentId){
607 if (resolvesCommentId){
609 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
608 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
610 }
609 }
611
610
612 setTimeout(function() {
611 setTimeout(function() {
613 // callbacks
612 // callbacks
614 if (cm !== undefined) {
613 if (cm !== undefined) {
615 commentForm.setPlaceholder(placeholderText);
614 commentForm.setPlaceholder(placeholderText);
616 if (commentForm.isInline()) {
615 if (commentForm.isInline()) {
617 cm.focus();
616 cm.focus();
618 cm.refresh();
617 cm.refresh();
619 }
618 }
620 }
619 }
621 }, 10);
620 }, 10);
622
621
623 // trigger scrolldown to the resolve comment, since it might be away
622 // trigger scrolldown to the resolve comment, since it might be away
624 // from the clicked
623 // from the clicked
625 if (resolvesCommentId){
624 if (resolvesCommentId){
626 var actionNode = $(commentForm.resolvesActionId).offset();
625 var actionNode = $(commentForm.resolvesActionId).offset();
627
626
628 setTimeout(function() {
627 setTimeout(function() {
629 if (actionNode) {
628 if (actionNode) {
630 $('body, html').animate({scrollTop: actionNode.top}, 10);
629 $('body, html').animate({scrollTop: actionNode.top}, 10);
631 }
630 }
632 }, 100);
631 }, 100);
633 }
632 }
634
633
635 return commentForm;
634 return commentForm;
636 };
635 };
637
636
638 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
637 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
639
638
640 var tmpl = $('#cb-comment-general-form-template').html();
639 var tmpl = $('#cb-comment-general-form-template').html();
641 tmpl = tmpl.format(null, 'general');
640 tmpl = tmpl.format(null, 'general');
642 var $form = $(tmpl);
641 var $form = $(tmpl);
643
642
644 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
643 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
645 var curForm = $formPlaceholder.find('form');
644 var curForm = $formPlaceholder.find('form');
646 if (curForm){
645 if (curForm){
647 curForm.remove();
646 curForm.remove();
648 }
647 }
649 $formPlaceholder.append($form);
648 $formPlaceholder.append($form);
650
649
651 var _form = $($form[0]);
650 var _form = $($form[0]);
652 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
651 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
653 var commentForm = this.createCommentForm(
652 var commentForm = this.createCommentForm(
654 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
653 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
655 commentForm.initStatusChangeSelector();
654 commentForm.initStatusChangeSelector();
656
655
657 return commentForm;
656 return commentForm;
658 };
657 };
659
658
660 this.createComment = function(node, resolutionComment) {
659 this.createComment = function(node, resolutionComment) {
661 var resolvesCommentId = resolutionComment || null;
660 var resolvesCommentId = resolutionComment || null;
662 var $node = $(node);
661 var $node = $(node);
663 var $td = $node.closest('td');
662 var $td = $node.closest('td');
664 var $form = $td.find('.comment-inline-form');
663 var $form = $td.find('.comment-inline-form');
665
664
666 if (!$form.length) {
665 if (!$form.length) {
667
666
668 var $filediff = $node.closest('.filediff');
667 var $filediff = $node.closest('.filediff');
669 $filediff.removeClass('hide-comments');
668 $filediff.removeClass('hide-comments');
670 var f_path = $filediff.attr('data-f-path');
669 var f_path = $filediff.attr('data-f-path');
671 var lineno = self.getLineNumber(node);
670 var lineno = self.getLineNumber(node);
672 // create a new HTML from template
671 // create a new HTML from template
673 var tmpl = $('#cb-comment-inline-form-template').html();
672 var tmpl = $('#cb-comment-inline-form-template').html();
674 tmpl = tmpl.format(f_path, lineno);
673 tmpl = tmpl.format(f_path, lineno);
675 $form = $(tmpl);
674 $form = $(tmpl);
676
675
677 var $comments = $td.find('.inline-comments');
676 var $comments = $td.find('.inline-comments');
678 if (!$comments.length) {
677 if (!$comments.length) {
679 $comments = $(
678 $comments = $(
680 $('#cb-comments-inline-container-template').html());
679 $('#cb-comments-inline-container-template').html());
681 $td.append($comments);
680 $td.append($comments);
682 }
681 }
683
682
684 $td.find('.cb-comment-add-button').before($form);
683 $td.find('.cb-comment-add-button').before($form);
685
684
686 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
685 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
687 var _form = $($form[0]).find('form');
686 var _form = $($form[0]).find('form');
688 var autocompleteActions = ['as_note', 'as_todo'];
687 var autocompleteActions = ['as_note', 'as_todo'];
689 var commentForm = this.createCommentForm(
688 var commentForm = this.createCommentForm(
690 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
689 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
691
690
692 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
691 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
693 form: _form,
692 form: _form,
694 parent: $td[0],
693 parent: $td[0],
695 lineno: lineno,
694 lineno: lineno,
696 f_path: f_path}
695 f_path: f_path}
697 );
696 );
698
697
699 // set a CUSTOM submit handler for inline comments.
698 // set a CUSTOM submit handler for inline comments.
700 commentForm.setHandleFormSubmit(function(o) {
699 commentForm.setHandleFormSubmit(function(o) {
701 var text = commentForm.cm.getValue();
700 var text = commentForm.cm.getValue();
702 var commentType = commentForm.getCommentType();
701 var commentType = commentForm.getCommentType();
703 var resolvesCommentId = commentForm.getResolvesId();
702 var resolvesCommentId = commentForm.getResolvesId();
704
703
705 if (text === "") {
704 if (text === "") {
706 return;
705 return;
707 }
706 }
708
707
709 if (lineno === undefined) {
708 if (lineno === undefined) {
710 alert('missing line !');
709 alert('missing line !');
711 return;
710 return;
712 }
711 }
713 if (f_path === undefined) {
712 if (f_path === undefined) {
714 alert('missing file path !');
713 alert('missing file path !');
715 return;
714 return;
716 }
715 }
717
716
718 var excludeCancelBtn = false;
717 var excludeCancelBtn = false;
719 var submitEvent = true;
718 var submitEvent = true;
720 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
719 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
721 commentForm.cm.setOption("readOnly", true);
720 commentForm.cm.setOption("readOnly", true);
722 var postData = {
721 var postData = {
723 'text': text,
722 'text': text,
724 'f_path': f_path,
723 'f_path': f_path,
725 'line': lineno,
724 'line': lineno,
726 'comment_type': commentType,
725 'comment_type': commentType,
727 'csrf_token': CSRF_TOKEN
726 'csrf_token': CSRF_TOKEN
728 };
727 };
729 if (resolvesCommentId){
728 if (resolvesCommentId){
730 postData['resolves_comment_id'] = resolvesCommentId;
729 postData['resolves_comment_id'] = resolvesCommentId;
731 }
730 }
732
731
733 var submitSuccessCallback = function(json_data) {
732 var submitSuccessCallback = function(json_data) {
734 $form.remove();
733 $form.remove();
735 try {
734 try {
736 var html = json_data.rendered_text;
735 var html = json_data.rendered_text;
737 var lineno = json_data.line_no;
736 var lineno = json_data.line_no;
738 var target_id = json_data.target_id;
737 var target_id = json_data.target_id;
739
738
740 $comments.find('.cb-comment-add-button').before(html);
739 $comments.find('.cb-comment-add-button').before(html);
741
740
742 //mark visually which comment was resolved
741 //mark visually which comment was resolved
743 if (resolvesCommentId) {
742 if (resolvesCommentId) {
744 commentForm.markCommentResolved(resolvesCommentId);
743 commentForm.markCommentResolved(resolvesCommentId);
745 }
744 }
746
745
747 // run global callback on submit
746 // run global callback on submit
748 commentForm.globalSubmitSuccessCallback();
747 commentForm.globalSubmitSuccessCallback();
749
748
750 } catch (e) {
749 } catch (e) {
751 console.error(e);
750 console.error(e);
752 }
751 }
753
752
754 // re trigger the linkification of next/prev navigation
753 // re trigger the linkification of next/prev navigation
755 linkifyComments($('.inline-comment-injected'));
754 linkifyComments($('.inline-comment-injected'));
756 timeagoActivate();
755 timeagoActivate();
757 commentForm.setActionButtonsDisabled(false);
756 commentForm.setActionButtonsDisabled(false);
758
757
759 };
758 };
760 var submitFailCallback = function(){
759 var submitFailCallback = function(){
761 commentForm.resetCommentFormState(text)
760 commentForm.resetCommentFormState(text)
762 };
761 };
763 commentForm.submitAjaxPOST(
762 commentForm.submitAjaxPOST(
764 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
763 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
765 });
764 });
766 }
765 }
767
766
768 $form.addClass('comment-inline-form-open');
767 $form.addClass('comment-inline-form-open');
769 };
768 };
770
769
771 this.createResolutionComment = function(commentId){
770 this.createResolutionComment = function(commentId){
772 // hide the trigger text
771 // hide the trigger text
773 $('#resolve-comment-{0}'.format(commentId)).hide();
772 $('#resolve-comment-{0}'.format(commentId)).hide();
774
773
775 var comment = $('#comment-'+commentId);
774 var comment = $('#comment-'+commentId);
776 var commentData = comment.data();
775 var commentData = comment.data();
777 if (commentData.commentInline) {
776 if (commentData.commentInline) {
778 this.createComment(comment, commentId)
777 this.createComment(comment, commentId)
779 } else {
778 } else {
780 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
779 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
781 }
780 }
782
781
783 return false;
782 return false;
784 };
783 };
785
784
786 this.submitResolution = function(commentId){
785 this.submitResolution = function(commentId){
787 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
786 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
788 var commentForm = form.get(0).CommentForm;
787 var commentForm = form.get(0).CommentForm;
789
788
790 var cm = commentForm.getCmInstance();
789 var cm = commentForm.getCmInstance();
791 var renderer = templateContext.visual.default_renderer;
790 var renderer = templateContext.visual.default_renderer;
792 if (renderer == 'rst'){
791 if (renderer == 'rst'){
793 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
792 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
794 } else if (renderer == 'markdown') {
793 } else if (renderer == 'markdown') {
795 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
794 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
796 } else {
795 } else {
797 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
796 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
798 }
797 }
799
798
800 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
799 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
801 form.submit();
800 form.submit();
802 return false;
801 return false;
803 };
802 };
804
803
805 this.renderInlineComments = function(file_comments) {
804 this.renderInlineComments = function(file_comments) {
806 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
805 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
807
806
808 for (var i = 0; i < file_comments.length; i++) {
807 for (var i = 0; i < file_comments.length; i++) {
809 var box = file_comments[i];
808 var box = file_comments[i];
810
809
811 var target_id = $(box).attr('target_id');
810 var target_id = $(box).attr('target_id');
812
811
813 // actually comments with line numbers
812 // actually comments with line numbers
814 var comments = box.children;
813 var comments = box.children;
815
814
816 for (var j = 0; j < comments.length; j++) {
815 for (var j = 0; j < comments.length; j++) {
817 var data = {
816 var data = {
818 'rendered_text': comments[j].outerHTML,
817 'rendered_text': comments[j].outerHTML,
819 'line_no': $(comments[j]).attr('line'),
818 'line_no': $(comments[j]).attr('line'),
820 'target_id': target_id
819 'target_id': target_id
821 };
820 };
822 }
821 }
823 }
822 }
824
823
825 // since order of injection is random, we're now re-iterating
824 // since order of injection is random, we're now re-iterating
826 // from correct order and filling in links
825 // from correct order and filling in links
827 linkifyComments($('.inline-comment-injected'));
826 linkifyComments($('.inline-comment-injected'));
828 firefoxAnchorFix();
827 firefoxAnchorFix();
829 };
828 };
830
829
831 };
830 };
@@ -1,604 +1,603 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#save').attr('disabled', 'disabled');
44 $('#save').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#save').removeAttr('disabled');
47 $('#save').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 }
84 }
85 else {
85 else {
86 // use reference name
86 // use reference name
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 }
88 }
89
89
90 return [title, desc]
90 return [title, desc]
91 };
91 };
92
92
93
93
94
94
95 ReviewersController = function () {
95 ReviewersController = function () {
96 var self = this;
96 var self = this;
97 this.$reviewRulesContainer = $('#review_rules');
97 this.$reviewRulesContainer = $('#review_rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 this.forbidReviewUsers = undefined;
99 this.forbidReviewUsers = undefined;
100 this.$reviewMembers = $('#review_members');
100 this.$reviewMembers = $('#review_members');
101 this.currentRequest = null;
101 this.currentRequest = null;
102
102
103 this.defaultForbidReviewUsers = function() {
103 this.defaultForbidReviewUsers = function() {
104 return [
104 return [
105 {'username': 'default',
105 {'username': 'default',
106 'user_id': templateContext.default_user.user_id}
106 'user_id': templateContext.default_user.user_id}
107 ];
107 ];
108 };
108 };
109
109
110 this.hideReviewRules = function() {
110 this.hideReviewRules = function() {
111 self.$reviewRulesContainer.hide();
111 self.$reviewRulesContainer.hide();
112 };
112 };
113
113
114 this.showReviewRules = function() {
114 this.showReviewRules = function() {
115 self.$reviewRulesContainer.show();
115 self.$reviewRulesContainer.show();
116 };
116 };
117
117
118 this.addRule = function(ruleText) {
118 this.addRule = function(ruleText) {
119 self.showReviewRules();
119 self.showReviewRules();
120 return '<div>- {0}</div>'.format(ruleText)
120 return '<div>- {0}</div>'.format(ruleText)
121 };
121 };
122
122
123 this.loadReviewRules = function(data) {
123 this.loadReviewRules = function(data) {
124 // reset forbidden Users
124 // reset forbidden Users
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126
126
127 // reset state of review rules
127 // reset state of review rules
128 self.$rulesList.html('');
128 self.$rulesList.html('');
129
129
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 // default rule, case for older repo that don't have any rules stored
131 // default rule, case for older repo that don't have any rules stored
132 self.$rulesList.append(
132 self.$rulesList.append(
133 self.addRule(
133 self.addRule(
134 _gettext('All reviewers must vote.'))
134 _gettext('All reviewers must vote.'))
135 );
135 );
136 return self.forbidReviewUsers
136 return self.forbidReviewUsers
137 }
137 }
138
138
139 if (data.rules.voting !== undefined) {
139 if (data.rules.voting !== undefined) {
140 if (data.rules.voting < 0){
140 if (data.rules.voting < 0){
141 self.$rulesList.append(
141 self.$rulesList.append(
142 self.addRule(
142 self.addRule(
143 _gettext('All reviewers must vote.'))
143 _gettext('All reviewers must vote.'))
144 )
144 )
145 } else if (data.rules.voting === 1) {
145 } else if (data.rules.voting === 1) {
146 self.$rulesList.append(
146 self.$rulesList.append(
147 self.addRule(
147 self.addRule(
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 )
149 )
150
150
151 } else {
151 } else {
152 self.$rulesList.append(
152 self.$rulesList.append(
153 self.addRule(
153 self.addRule(
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 )
155 )
156 }
156 }
157 }
157 }
158 if (data.rules.use_code_authors_for_review) {
158 if (data.rules.use_code_authors_for_review) {
159 self.$rulesList.append(
159 self.$rulesList.append(
160 self.addRule(
160 self.addRule(
161 _gettext('Reviewers picked from source code changes.'))
161 _gettext('Reviewers picked from source code changes.'))
162 )
162 )
163 }
163 }
164 if (data.rules.forbid_adding_reviewers) {
164 if (data.rules.forbid_adding_reviewers) {
165 $('#add_reviewer_input').remove();
165 $('#add_reviewer_input').remove();
166 self.$rulesList.append(
166 self.$rulesList.append(
167 self.addRule(
167 self.addRule(
168 _gettext('Adding new reviewers is forbidden.'))
168 _gettext('Adding new reviewers is forbidden.'))
169 )
169 )
170 }
170 }
171 if (data.rules.forbid_author_to_review) {
171 if (data.rules.forbid_author_to_review) {
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 self.$rulesList.append(
173 self.$rulesList.append(
174 self.addRule(
174 self.addRule(
175 _gettext('Author is not allowed to be a reviewer.'))
175 _gettext('Author is not allowed to be a reviewer.'))
176 )
176 )
177 }
177 }
178 if (data.rules.forbid_commit_author_to_review) {
178 if (data.rules.forbid_commit_author_to_review) {
179
179
180 if (data.rules_data.forbidden_users) {
180 if (data.rules_data.forbidden_users) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 self.forbidReviewUsers.push(member_data)
182 self.forbidReviewUsers.push(member_data)
183 });
183 });
184
184
185 }
185 }
186
186
187 self.$rulesList.append(
187 self.$rulesList.append(
188 self.addRule(
188 self.addRule(
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 )
190 )
191 }
191 }
192
192
193 return self.forbidReviewUsers
193 return self.forbidReviewUsers
194 };
194 };
195
195
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197
197
198 if (self.currentRequest) {
198 if (self.currentRequest) {
199 // make sure we cleanup old running requests before triggering this
199 // make sure we cleanup old running requests before triggering this
200 // again
200 // again
201 self.currentRequest.abort();
201 self.currentRequest.abort();
202 }
202 }
203
203
204 $('.calculate-reviewers').show();
204 $('.calculate-reviewers').show();
205 // reset reviewer members
205 // reset reviewer members
206 self.$reviewMembers.empty();
206 self.$reviewMembers.empty();
207
207
208 prButtonLock(true, null, 'reviewers');
208 prButtonLock(true, null, 'reviewers');
209 $('#user').hide(); // hide user autocomplete before load
209 $('#user').hide(); // hide user autocomplete before load
210
210
211 var url = pyroutes.url('repo_default_reviewers_data',
211 var url = pyroutes.url('repo_default_reviewers_data',
212 {
212 {
213 'repo_name': templateContext.repo_name,
213 'repo_name': templateContext.repo_name,
214 'source_repo': sourceRepo,
214 'source_repo': sourceRepo,
215 'source_ref': sourceRef[2],
215 'source_ref': sourceRef[2],
216 'target_repo': targetRepo,
216 'target_repo': targetRepo,
217 'target_ref': targetRef[2]
217 'target_ref': targetRef[2]
218 });
218 });
219
219
220 self.currentRequest = $.get(url)
220 self.currentRequest = $.get(url)
221 .done(function(data) {
221 .done(function(data) {
222 self.currentRequest = null;
222 self.currentRequest = null;
223
223
224 // review rules
224 // review rules
225 self.loadReviewRules(data);
225 self.loadReviewRules(data);
226
226
227 for (var i = 0; i < data.reviewers.length; i++) {
227 for (var i = 0; i < data.reviewers.length; i++) {
228 var reviewer = data.reviewers[i];
228 var reviewer = data.reviewers[i];
229 self.addReviewMember(
229 self.addReviewMember(
230 reviewer.user_id, reviewer.first_name,
230 reviewer.user_id, reviewer.first_name,
231 reviewer.last_name, reviewer.username,
231 reviewer.last_name, reviewer.username,
232 reviewer.gravatar_link, reviewer.reasons,
232 reviewer.gravatar_link, reviewer.reasons,
233 reviewer.mandatory);
233 reviewer.mandatory);
234 }
234 }
235 $('.calculate-reviewers').hide();
235 $('.calculate-reviewers').hide();
236 prButtonLock(false, null, 'reviewers');
236 prButtonLock(false, null, 'reviewers');
237 $('#user').show(); // show user autocomplete after load
237 $('#user').show(); // show user autocomplete after load
238 });
238 });
239 };
239 };
240
240
241 // check those, refactor
241 // check those, refactor
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
242 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244
244
245 if(typeof(mark_delete) === undefined){
245 if(typeof(mark_delete) === undefined){
246 mark_delete = false;
246 mark_delete = false;
247 }
247 }
248
248
249 if(mark_delete === true){
249 if(mark_delete === true){
250 if (reviewer){
250 if (reviewer){
251 // now delete the input
251 // now delete the input
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 // mark as to-delete
253 // mark as to-delete
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 obj.addClass('to-delete');
255 obj.addClass('to-delete');
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 }
257 }
258 }
258 }
259 else{
259 else{
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
260 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 }
261 }
262 };
262 };
263
263
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
265 var members = self.$reviewMembers.get(0);
265 var members = self.$reviewMembers.get(0);
266 var reasons_html = '';
266 var reasons_html = '';
267 var reasons_inputs = '';
267 var reasons_inputs = '';
268 var reasons = reasons || [];
268 var reasons = reasons || [];
269 var mandatory = mandatory || false;
269 var mandatory = mandatory || false;
270
270
271 if (reasons) {
271 if (reasons) {
272 for (var i = 0; i < reasons.length; i++) {
272 for (var i = 0; i < reasons.length; i++) {
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 }
275 }
276 }
276 }
277 var tmpl = '' +
277 var tmpl = '' +
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 '<div class="reviewer_status">'+
280 '<div class="reviewer_status">'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 '</div>'+
282 '</div>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 '<span class="reviewer_name user">{1}</span>'+
284 '<span class="reviewer_name user">{1}</span>'+
285 reasons_html +
285 reasons_html +
286 '<input type="hidden" name="user_id" value="{2}">'+
286 '<input type="hidden" name="user_id" value="{2}">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 '{3}'+
288 '{3}'+
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290
290
291 if (mandatory) {
291 if (mandatory) {
292 tmpl += ''+
292 tmpl += ''+
293 '<div class="reviewer_member_mandatory_remove">' +
293 '<div class="reviewer_member_mandatory_remove">' +
294 '<i class="icon-remove-sign"></i>'+
294 '<i class="icon-remove-sign"></i>'+
295 '</div>' +
295 '</div>' +
296 '<input type="hidden" name="mandatory" value="true">'+
296 '<input type="hidden" name="mandatory" value="true">'+
297 '<div class="reviewer_member_mandatory">' +
297 '<div class="reviewer_member_mandatory">' +
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 '</div>';
299 '</div>';
300
300
301 } else {
301 } else {
302 tmpl += ''+
302 tmpl += ''+
303 '<input type="hidden" name="mandatory" value="false">'+
303 '<input type="hidden" name="mandatory" value="false">'+
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 '<i class="icon-remove-sign"></i>'+
305 '<i class="icon-remove-sign"></i>'+
306 '</div>';
306 '</div>';
307 }
307 }
308 // continue template
308 // continue template
309 tmpl += ''+
309 tmpl += ''+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 '</li>' ;
311 '</li>' ;
312
312
313 var displayname = "{0} ({1} {2})".format(
313 var displayname = "{0} ({1} {2})".format(
314 nname, escapeHtml(fname), escapeHtml(lname));
314 nname, escapeHtml(fname), escapeHtml(lname));
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 // check if we don't have this ID already in
316 // check if we don't have this ID already in
317 var ids = [];
317 var ids = [];
318 var _els = self.$reviewMembers.find('li').toArray();
318 var _els = self.$reviewMembers.find('li').toArray();
319 for (el in _els){
319 for (el in _els){
320 ids.push(_els[el].id)
320 ids.push(_els[el].id)
321 }
321 }
322
322
323 var userAllowedReview = function(userId) {
323 var userAllowedReview = function(userId) {
324 var allowed = true;
324 var allowed = true;
325 $.each(self.forbidReviewUsers, function(index, member_data) {
325 $.each(self.forbidReviewUsers, function(index, member_data) {
326 if (parseInt(userId) === member_data['user_id']) {
326 if (parseInt(userId) === member_data['user_id']) {
327 allowed = false;
327 allowed = false;
328 return false // breaks the loop
328 return false // breaks the loop
329 }
329 }
330 });
330 });
331 return allowed
331 return allowed
332 };
332 };
333
333
334 var userAllowed = userAllowedReview(id);
334 var userAllowed = userAllowedReview(id);
335 if (!userAllowed){
335 if (!userAllowed){
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 }
337 }
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
339
339
340 if(shouldAdd) {
340 if(shouldAdd) {
341 // only add if it's not there
341 // only add if it's not there
342 members.innerHTML += element;
342 members.innerHTML += element;
343 }
343 }
344
344
345 };
345 };
346
346
347 this.updateReviewers = function(repo_name, pull_request_id){
347 this.updateReviewers = function(repo_name, pull_request_id){
348 var postData = '_method=put&' + $('#reviewers input').serialize();
348 var postData = '_method=put&' + $('#reviewers input').serialize();
349 _updatePullRequest(repo_name, pull_request_id, postData);
349 _updatePullRequest(repo_name, pull_request_id, postData);
350 };
350 };
351
351
352 };
352 };
353
353
354
354
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 var url = pyroutes.url(
356 var url = pyroutes.url(
357 'pullrequest_update',
357 'pullrequest_update',
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 if (typeof postData === 'string' ) {
359 if (typeof postData === 'string' ) {
360 postData += '&csrf_token=' + CSRF_TOKEN;
360 postData += '&csrf_token=' + CSRF_TOKEN;
361 } else {
361 } else {
362 postData.csrf_token = CSRF_TOKEN;
362 postData.csrf_token = CSRF_TOKEN;
363 }
363 }
364 var success = function(o) {
364 var success = function(o) {
365 window.location.reload();
365 window.location.reload();
366 };
366 };
367 ajaxPOST(url, postData, success);
367 ajaxPOST(url, postData, success);
368 };
368 };
369
369
370 /**
370 /**
371 * PULL REQUEST update commits
371 * PULL REQUEST update commits
372 */
372 */
373 var updateCommits = function(repo_name, pull_request_id) {
373 var updateCommits = function(repo_name, pull_request_id) {
374 var postData = {
374 var postData = {
375 '_method': 'put',
376 'update_commits': true};
375 'update_commits': true};
377 _updatePullRequest(repo_name, pull_request_id, postData);
376 _updatePullRequest(repo_name, pull_request_id, postData);
378 };
377 };
379
378
380
379
381 /**
380 /**
382 * PULL REQUEST edit info
381 * PULL REQUEST edit info
383 */
382 */
384 var editPullRequest = function(repo_name, pull_request_id, title, description) {
383 var editPullRequest = function(repo_name, pull_request_id, title, description) {
385 var url = pyroutes.url(
384 var url = pyroutes.url(
386 'pullrequest_update',
385 'pullrequest_update',
387 {"repo_name": repo_name, "pull_request_id": pull_request_id});
386 {"repo_name": repo_name, "pull_request_id": pull_request_id});
388
387
389 var postData = {
388 var postData = {
390 'title': title,
389 'title': title,
391 'description': description,
390 'description': description,
392 'edit_pull_request': true,
391 'edit_pull_request': true,
393 'csrf_token': CSRF_TOKEN
392 'csrf_token': CSRF_TOKEN
394 };
393 };
395 var success = function(o) {
394 var success = function(o) {
396 window.location.reload();
395 window.location.reload();
397 };
396 };
398 ajaxPOST(url, postData, success);
397 ajaxPOST(url, postData, success);
399 };
398 };
400
399
401 var initPullRequestsCodeMirror = function (textAreaId) {
400 var initPullRequestsCodeMirror = function (textAreaId) {
402 var ta = $(textAreaId).get(0);
401 var ta = $(textAreaId).get(0);
403 var initialHeight = '100px';
402 var initialHeight = '100px';
404
403
405 // default options
404 // default options
406 var codeMirrorOptions = {
405 var codeMirrorOptions = {
407 mode: "text",
406 mode: "text",
408 lineNumbers: false,
407 lineNumbers: false,
409 indentUnit: 4,
408 indentUnit: 4,
410 theme: 'rc-input'
409 theme: 'rc-input'
411 };
410 };
412
411
413 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
412 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
414 // marker for manually set description
413 // marker for manually set description
415 codeMirrorInstance._userDefinedDesc = false;
414 codeMirrorInstance._userDefinedDesc = false;
416 codeMirrorInstance.setSize(null, initialHeight);
415 codeMirrorInstance.setSize(null, initialHeight);
417 codeMirrorInstance.on("change", function(instance, changeObj) {
416 codeMirrorInstance.on("change", function(instance, changeObj) {
418 var height = initialHeight;
417 var height = initialHeight;
419 var lines = instance.lineCount();
418 var lines = instance.lineCount();
420 if (lines > 6 && lines < 20) {
419 if (lines > 6 && lines < 20) {
421 height = "auto"
420 height = "auto"
422 }
421 }
423 else if (lines >= 20) {
422 else if (lines >= 20) {
424 height = 20 * 15;
423 height = 20 * 15;
425 }
424 }
426 instance.setSize(null, height);
425 instance.setSize(null, height);
427
426
428 // detect if the change was trigger by auto desc, or user input
427 // detect if the change was trigger by auto desc, or user input
429 changeOrigin = changeObj.origin;
428 changeOrigin = changeObj.origin;
430
429
431 if (changeOrigin === "setValue") {
430 if (changeOrigin === "setValue") {
432 cmLog.debug('Change triggered by setValue');
431 cmLog.debug('Change triggered by setValue');
433 }
432 }
434 else {
433 else {
435 cmLog.debug('user triggered change !');
434 cmLog.debug('user triggered change !');
436 // set special marker to indicate user has created an input.
435 // set special marker to indicate user has created an input.
437 instance._userDefinedDesc = true;
436 instance._userDefinedDesc = true;
438 }
437 }
439
438
440 });
439 });
441
440
442 return codeMirrorInstance
441 return codeMirrorInstance
443 };
442 };
444
443
445 /**
444 /**
446 * Reviewer autocomplete
445 * Reviewer autocomplete
447 */
446 */
448 var ReviewerAutoComplete = function(inputId) {
447 var ReviewerAutoComplete = function(inputId) {
449 $(inputId).autocomplete({
448 $(inputId).autocomplete({
450 serviceUrl: pyroutes.url('user_autocomplete_data'),
449 serviceUrl: pyroutes.url('user_autocomplete_data'),
451 minChars:2,
450 minChars:2,
452 maxHeight:400,
451 maxHeight:400,
453 deferRequestBy: 300, //miliseconds
452 deferRequestBy: 300, //miliseconds
454 showNoSuggestionNotice: true,
453 showNoSuggestionNotice: true,
455 tabDisabled: true,
454 tabDisabled: true,
456 autoSelectFirst: true,
455 autoSelectFirst: true,
457 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
456 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
458 formatResult: autocompleteFormatResult,
457 formatResult: autocompleteFormatResult,
459 lookupFilter: autocompleteFilterResult,
458 lookupFilter: autocompleteFilterResult,
460 onSelect: function(element, data) {
459 onSelect: function(element, data) {
461
460
462 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
461 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
463 if (data.value_type == 'user_group') {
462 if (data.value_type == 'user_group') {
464 reasons.push(_gettext('member of "{0}"').format(data.value_display));
463 reasons.push(_gettext('member of "{0}"').format(data.value_display));
465
464
466 $.each(data.members, function(index, member_data) {
465 $.each(data.members, function(index, member_data) {
467 reviewersController.addReviewMember(
466 reviewersController.addReviewMember(
468 member_data.id, member_data.first_name, member_data.last_name,
467 member_data.id, member_data.first_name, member_data.last_name,
469 member_data.username, member_data.icon_link, reasons);
468 member_data.username, member_data.icon_link, reasons);
470 })
469 })
471
470
472 } else {
471 } else {
473 reviewersController.addReviewMember(
472 reviewersController.addReviewMember(
474 data.id, data.first_name, data.last_name,
473 data.id, data.first_name, data.last_name,
475 data.username, data.icon_link, reasons);
474 data.username, data.icon_link, reasons);
476 }
475 }
477
476
478 $(inputId).val('');
477 $(inputId).val('');
479 }
478 }
480 });
479 });
481 };
480 };
482
481
483
482
484 VersionController = function () {
483 VersionController = function () {
485 var self = this;
484 var self = this;
486 this.$verSource = $('input[name=ver_source]');
485 this.$verSource = $('input[name=ver_source]');
487 this.$verTarget = $('input[name=ver_target]');
486 this.$verTarget = $('input[name=ver_target]');
488 this.$showVersionDiff = $('#show-version-diff');
487 this.$showVersionDiff = $('#show-version-diff');
489
488
490 this.adjustRadioSelectors = function (curNode) {
489 this.adjustRadioSelectors = function (curNode) {
491 var getVal = function (item) {
490 var getVal = function (item) {
492 if (item == 'latest') {
491 if (item == 'latest') {
493 return Number.MAX_SAFE_INTEGER
492 return Number.MAX_SAFE_INTEGER
494 }
493 }
495 else {
494 else {
496 return parseInt(item)
495 return parseInt(item)
497 }
496 }
498 };
497 };
499
498
500 var curVal = getVal($(curNode).val());
499 var curVal = getVal($(curNode).val());
501 var cleared = false;
500 var cleared = false;
502
501
503 $.each(self.$verSource, function (index, value) {
502 $.each(self.$verSource, function (index, value) {
504 var elVal = getVal($(value).val());
503 var elVal = getVal($(value).val());
505
504
506 if (elVal > curVal) {
505 if (elVal > curVal) {
507 if ($(value).is(':checked')) {
506 if ($(value).is(':checked')) {
508 cleared = true;
507 cleared = true;
509 }
508 }
510 $(value).attr('disabled', 'disabled');
509 $(value).attr('disabled', 'disabled');
511 $(value).removeAttr('checked');
510 $(value).removeAttr('checked');
512 $(value).css({'opacity': 0.1});
511 $(value).css({'opacity': 0.1});
513 }
512 }
514 else {
513 else {
515 $(value).css({'opacity': 1});
514 $(value).css({'opacity': 1});
516 $(value).removeAttr('disabled');
515 $(value).removeAttr('disabled');
517 }
516 }
518 });
517 });
519
518
520 if (cleared) {
519 if (cleared) {
521 // if we unchecked an active, set the next one to same loc.
520 // if we unchecked an active, set the next one to same loc.
522 $(this.$verSource).filter('[value={0}]'.format(
521 $(this.$verSource).filter('[value={0}]'.format(
523 curVal)).attr('checked', 'checked');
522 curVal)).attr('checked', 'checked');
524 }
523 }
525
524
526 self.setLockAction(false,
525 self.setLockAction(false,
527 $(curNode).data('verPos'),
526 $(curNode).data('verPos'),
528 $(this.$verSource).filter(':checked').data('verPos')
527 $(this.$verSource).filter(':checked').data('verPos')
529 );
528 );
530 };
529 };
531
530
532
531
533 this.attachVersionListener = function () {
532 this.attachVersionListener = function () {
534 self.$verTarget.change(function (e) {
533 self.$verTarget.change(function (e) {
535 self.adjustRadioSelectors(this)
534 self.adjustRadioSelectors(this)
536 });
535 });
537 self.$verSource.change(function (e) {
536 self.$verSource.change(function (e) {
538 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
537 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
539 });
538 });
540 };
539 };
541
540
542 this.init = function () {
541 this.init = function () {
543
542
544 var curNode = self.$verTarget.filter(':checked');
543 var curNode = self.$verTarget.filter(':checked');
545 self.adjustRadioSelectors(curNode);
544 self.adjustRadioSelectors(curNode);
546 self.setLockAction(true);
545 self.setLockAction(true);
547 self.attachVersionListener();
546 self.attachVersionListener();
548
547
549 };
548 };
550
549
551 this.setLockAction = function (state, selectedVersion, otherVersion) {
550 this.setLockAction = function (state, selectedVersion, otherVersion) {
552 var $showVersionDiff = this.$showVersionDiff;
551 var $showVersionDiff = this.$showVersionDiff;
553
552
554 if (state) {
553 if (state) {
555 $showVersionDiff.attr('disabled', 'disabled');
554 $showVersionDiff.attr('disabled', 'disabled');
556 $showVersionDiff.addClass('disabled');
555 $showVersionDiff.addClass('disabled');
557 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
556 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
558 }
557 }
559 else {
558 else {
560 $showVersionDiff.removeAttr('disabled');
559 $showVersionDiff.removeAttr('disabled');
561 $showVersionDiff.removeClass('disabled');
560 $showVersionDiff.removeClass('disabled');
562
561
563 if (selectedVersion == otherVersion) {
562 if (selectedVersion == otherVersion) {
564 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
563 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
565 } else {
564 } else {
566 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
565 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
567 }
566 }
568 }
567 }
569
568
570 };
569 };
571
570
572 this.showVersionDiff = function () {
571 this.showVersionDiff = function () {
573 var target = self.$verTarget.filter(':checked');
572 var target = self.$verTarget.filter(':checked');
574 var source = self.$verSource.filter(':checked');
573 var source = self.$verSource.filter(':checked');
575
574
576 if (target.val() && source.val()) {
575 if (target.val() && source.val()) {
577 var params = {
576 var params = {
578 'pull_request_id': templateContext.pull_request_data.pull_request_id,
577 'pull_request_id': templateContext.pull_request_data.pull_request_id,
579 'repo_name': templateContext.repo_name,
578 'repo_name': templateContext.repo_name,
580 'version': target.val(),
579 'version': target.val(),
581 'from_version': source.val()
580 'from_version': source.val()
582 };
581 };
583 window.location = pyroutes.url('pullrequest_show', params)
582 window.location = pyroutes.url('pullrequest_show', params)
584 }
583 }
585
584
586 return false;
585 return false;
587 };
586 };
588
587
589 this.toggleVersionView = function (elem) {
588 this.toggleVersionView = function (elem) {
590
589
591 if (this.$showVersionDiff.is(':visible')) {
590 if (this.$showVersionDiff.is(':visible')) {
592 $('.version-pr').hide();
591 $('.version-pr').hide();
593 this.$showVersionDiff.hide();
592 this.$showVersionDiff.hide();
594 $(elem).html($(elem).data('toggleOn'))
593 $(elem).html($(elem).data('toggleOn'))
595 } else {
594 } else {
596 $('.version-pr').show();
595 $('.version-pr').show();
597 this.$showVersionDiff.show();
596 this.$showVersionDiff.show();
598 $(elem).html($(elem).data('toggleOff'))
597 $(elem).html($(elem).data('toggleOff'))
599 }
598 }
600
599
601 return false
600 return false
602 }
601 }
603
602
604 }; No newline at end of file
603 };
General Comments 0
You need to be logged in to leave comments. Login now