##// END OF EJS Templates
pull-requests: migrated code from pylons to pyramid
marcink -
r1974:cb4db595 default
parent child Browse files
Show More
@@ -1,134 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23 import urlobject
24 from pylons import url
25 24
26 25 from rhodecode.api.tests.utils import (
27 26 build_data, api_call, assert_error, assert_ok)
27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.utils2 import safe_unicode
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequest(object):
35 35
36 36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
37 37 from rhodecode.model.pull_request import PullRequestModel
38 38 pull_request = pr_util.create_pull_request(mergeable=True)
39 39 id_, params = build_data(
40 40 self.apikey, 'get_pull_request',
41 41 repoid=pull_request.target_repo.repo_name,
42 42 pullrequestid=pull_request.pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 assert response.status == '200 OK'
47 47
48 48 url_obj = urlobject.URLObject(
49 url(
49 h.route_url(
50 50 'pullrequest_show',
51 51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id))
53 53
54 54 pr_url = safe_unicode(
55 55 url_obj.with_netloc(http_host_only_stub))
56 56 source_url = safe_unicode(
57 57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 58 target_url = safe_unicode(
59 59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 60 shadow_url = safe_unicode(
61 61 PullRequestModel().get_shadow_clone_url(pull_request))
62 62
63 63 expected = {
64 64 'pull_request_id': pull_request.pull_request_id,
65 65 'url': pr_url,
66 66 'title': pull_request.title,
67 67 'description': pull_request.description,
68 68 'status': pull_request.status,
69 69 'created_on': pull_request.created_on,
70 70 'updated_on': pull_request.updated_on,
71 71 'commit_ids': pull_request.revisions,
72 72 'review_status': pull_request.calculated_review_status(),
73 73 'mergeable': {
74 74 'status': True,
75 75 'message': 'This pull request can be automatically merged.',
76 76 },
77 77 'source': {
78 78 'clone_url': source_url,
79 79 'repository': pull_request.source_repo.repo_name,
80 80 'reference': {
81 81 'name': pull_request.source_ref_parts.name,
82 82 'type': pull_request.source_ref_parts.type,
83 83 'commit_id': pull_request.source_ref_parts.commit_id,
84 84 },
85 85 },
86 86 'target': {
87 87 'clone_url': target_url,
88 88 'repository': pull_request.target_repo.repo_name,
89 89 'reference': {
90 90 'name': pull_request.target_ref_parts.name,
91 91 'type': pull_request.target_ref_parts.type,
92 92 'commit_id': pull_request.target_ref_parts.commit_id,
93 93 },
94 94 },
95 95 'merge': {
96 96 'clone_url': shadow_url,
97 97 'reference': {
98 98 'name': pull_request.shadow_merge_ref.name,
99 99 'type': pull_request.shadow_merge_ref.type,
100 100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 101 },
102 102 },
103 103 'author': pull_request.author.get_api_data(include_secrets=False,
104 104 details='basic'),
105 105 'reviewers': [
106 106 {
107 107 'user': reviewer.get_api_data(include_secrets=False,
108 108 details='basic'),
109 109 'reasons': reasons,
110 110 'review_status': st[0][1].status if st else 'not_reviewed',
111 111 }
112 112 for reviewer, reasons, mandatory, st in
113 113 pull_request.reviewers_statuses()
114 114 ]
115 115 }
116 116 assert_ok(id_, expected, response.body)
117 117
118 118 def test_api_get_pull_request_repo_error(self):
119 119 id_, params = build_data(
120 120 self.apikey, 'get_pull_request',
121 121 repoid=666, pullrequestid=1)
122 122 response = api_call(self.app, params)
123 123
124 124 expected = 'repository `666` does not exist'
125 125 assert_error(id_, expected, given=response.body)
126 126
127 127 def test_api_get_pull_request_pull_request_error(self):
128 128 id_, params = build_data(
129 129 self.apikey, 'get_pull_request',
130 130 repoid=1, pullrequestid=666)
131 131 response = api_call(self.app, params)
132 132
133 133 expected = 'pull request `666` does not exist'
134 134 assert_error(id_, expected, given=response.body)
@@ -1,312 +1,357 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # Summary
26 26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 27 # all pattern
28 28 config.add_route(
29 29 name='repo_summary_explicit',
30 30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 31 config.add_route(
32 32 name='repo_summary_commits',
33 33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34 34
35 35 # Commits
36 36 config.add_route(
37 37 name='repo_commit',
38 38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39 39
40 40 config.add_route(
41 41 name='repo_commit_children',
42 42 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
43 43
44 44 config.add_route(
45 45 name='repo_commit_parents',
46 46 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
47 47
48 48 config.add_route(
49 49 name='repo_commit_raw',
50 50 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
51 51
52 52 config.add_route(
53 53 name='repo_commit_patch',
54 54 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
55 55
56 56 config.add_route(
57 57 name='repo_commit_download',
58 58 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
59 59
60 60 config.add_route(
61 61 name='repo_commit_data',
62 62 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
63 63
64 64 config.add_route(
65 65 name='repo_commit_comment_create',
66 66 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
67 67
68 68 config.add_route(
69 69 name='repo_commit_comment_preview',
70 70 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
71 71
72 72 config.add_route(
73 73 name='repo_commit_comment_delete',
74 74 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
75 75
76 76 # still working url for backward compat.
77 77 config.add_route(
78 78 name='repo_commit_raw_deprecated',
79 79 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
80 80
81 81 # Files
82 82 config.add_route(
83 83 name='repo_archivefile',
84 84 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
85 85
86 86 config.add_route(
87 87 name='repo_files_diff',
88 88 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
89 89 config.add_route( # legacy route to make old links work
90 90 name='repo_files_diff_2way_redirect',
91 91 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
92 92
93 93 config.add_route(
94 94 name='repo_files',
95 95 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
96 96 config.add_route(
97 97 name='repo_files:default_path',
98 98 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
99 99 config.add_route(
100 100 name='repo_files:default_commit',
101 101 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
102 102
103 103 config.add_route(
104 104 name='repo_files:rendered',
105 105 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
106 106
107 107 config.add_route(
108 108 name='repo_files:annotated',
109 109 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
110 110 config.add_route(
111 111 name='repo_files:annotated_previous',
112 112 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
113 113
114 114 config.add_route(
115 115 name='repo_nodetree_full',
116 116 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
117 117 config.add_route(
118 118 name='repo_nodetree_full:default_path',
119 119 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
120 120
121 121 config.add_route(
122 122 name='repo_files_nodelist',
123 123 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
124 124
125 125 config.add_route(
126 126 name='repo_file_raw',
127 127 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
128 128
129 129 config.add_route(
130 130 name='repo_file_download',
131 131 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
132 132 config.add_route( # backward compat to keep old links working
133 133 name='repo_file_download:legacy',
134 134 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
135 135 repo_route=True)
136 136
137 137 config.add_route(
138 138 name='repo_file_history',
139 139 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
140 140
141 141 config.add_route(
142 142 name='repo_file_authors',
143 143 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
144 144
145 145 config.add_route(
146 146 name='repo_files_remove_file',
147 147 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
148 148 repo_route=True)
149 149 config.add_route(
150 150 name='repo_files_delete_file',
151 151 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
152 152 repo_route=True)
153 153 config.add_route(
154 154 name='repo_files_edit_file',
155 155 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
156 156 repo_route=True)
157 157 config.add_route(
158 158 name='repo_files_update_file',
159 159 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
160 160 repo_route=True)
161 161 config.add_route(
162 162 name='repo_files_add_file',
163 163 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
164 164 repo_route=True)
165 165 config.add_route(
166 166 name='repo_files_create_file',
167 167 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
168 168 repo_route=True)
169 169
170 170 # Refs data
171 171 config.add_route(
172 172 name='repo_refs_data',
173 173 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
174 174
175 175 config.add_route(
176 176 name='repo_refs_changelog_data',
177 177 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
178 178
179 179 config.add_route(
180 180 name='repo_stats',
181 181 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
182 182
183 183 # Changelog
184 184 config.add_route(
185 185 name='repo_changelog',
186 186 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
187 187 config.add_route(
188 188 name='repo_changelog_file',
189 189 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
190 190 config.add_route(
191 191 name='repo_changelog_elements',
192 192 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
193 193
194 194 # Compare
195 195 config.add_route(
196 196 name='repo_compare_select',
197 197 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
198 198
199 199 config.add_route(
200 200 name='repo_compare',
201 201 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
202 202
203 203 # Tags
204 204 config.add_route(
205 205 name='tags_home',
206 206 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
207 207
208 208 # Branches
209 209 config.add_route(
210 210 name='branches_home',
211 211 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
212 212
213 213 config.add_route(
214 214 name='bookmarks_home',
215 215 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
216 216
217 217 # Pull Requests
218 218 config.add_route(
219 219 name='pullrequest_show',
220 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
220 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
221 221 repo_route=True)
222 222
223 223 config.add_route(
224 224 name='pullrequest_show_all',
225 225 pattern='/{repo_name:.*?[^/]}/pull-request',
226 226 repo_route=True, repo_accepted_types=['hg', 'git'])
227 227
228 228 config.add_route(
229 229 name='pullrequest_show_all_data',
230 230 pattern='/{repo_name:.*?[^/]}/pull-request-data',
231 231 repo_route=True, repo_accepted_types=['hg', 'git'])
232 232
233 config.add_route(
234 name='pullrequest_repo_refs',
235 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
236 repo_route=True)
237
238 config.add_route(
239 name='pullrequest_repo_destinations',
240 pattern='/{repo_name:.*?[^/]}/pull-request/repo-destinations',
241 repo_route=True)
242
243 config.add_route(
244 name='pullrequest_new',
245 pattern='/{repo_name:.*?[^/]}/pull-request/new',
246 repo_route=True, repo_accepted_types=['hg', 'git'])
247
248 config.add_route(
249 name='pullrequest_create',
250 pattern='/{repo_name:.*?[^/]}/pull-request/create',
251 repo_route=True, repo_accepted_types=['hg', 'git'])
252
253 config.add_route(
254 name='pullrequest_update',
255 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
256 repo_route=True)
257
258 config.add_route(
259 name='pullrequest_merge',
260 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
261 repo_route=True)
262
263 config.add_route(
264 name='pullrequest_delete',
265 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
266 repo_route=True)
267
268 config.add_route(
269 name='pullrequest_comment_create',
270 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
271 repo_route=True)
272
273 config.add_route(
274 name='pullrequest_comment_delete',
275 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
276 repo_route=True, repo_accepted_types=['hg', 'git'])
277
233 278 # Settings
234 279 config.add_route(
235 280 name='edit_repo',
236 281 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
237 282
238 283 # Settings advanced
239 284 config.add_route(
240 285 name='edit_repo_advanced',
241 286 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
242 287 config.add_route(
243 288 name='edit_repo_advanced_delete',
244 289 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
245 290 config.add_route(
246 291 name='edit_repo_advanced_locking',
247 292 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
248 293 config.add_route(
249 294 name='edit_repo_advanced_journal',
250 295 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
251 296 config.add_route(
252 297 name='edit_repo_advanced_fork',
253 298 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
254 299
255 300 # Caches
256 301 config.add_route(
257 302 name='edit_repo_caches',
258 303 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
259 304
260 305 # Permissions
261 306 config.add_route(
262 307 name='edit_repo_perms',
263 308 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
264 309
265 310 # Repo Review Rules
266 311 config.add_route(
267 312 name='repo_reviewers',
268 313 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
269 314
270 315 config.add_route(
271 316 name='repo_default_reviewers_data',
272 317 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
273 318
274 319 # Maintenance
275 320 config.add_route(
276 321 name='repo_maintenance',
277 322 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
278 323
279 324 config.add_route(
280 325 name='repo_maintenance_execute',
281 326 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
282 327
283 328 # Strip
284 329 config.add_route(
285 330 name='strip',
286 331 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
287 332
288 333 config.add_route(
289 334 name='strip_check',
290 335 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
291 336
292 337 config.add_route(
293 338 name='strip_execute',
294 339 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
295 340
296 341 # ATOM/RSS Feed
297 342 config.add_route(
298 343 name='rss_feed_home',
299 344 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
300 345
301 346 config.add_route(
302 347 name='atom_feed_home',
303 348 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
304 349
305 350 # NOTE(marcink): needs to be at the end for catch-all
306 351 add_route_with_slash(
307 352 config,
308 353 name='repo_summary',
309 354 pattern='/{repo_name:.*?[^/]}', repo_route=True)
310 355
311 356 # Scan module for configuration decorators.
312 357 config.scan()
@@ -1,1106 +1,1112 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 20 import mock
22 21 import pytest
23 from webob.exc import HTTPNotFound
24 22
25 23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
26 25 from rhodecode.lib.vcs.nodes import FileNode
27 26 from rhodecode.lib import helpers as h
28 27 from rhodecode.model.changeset_status import ChangesetStatusModel
29 28 from rhodecode.model.db import (
30 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
31 30 from rhodecode.model.meta import Session
32 31 from rhodecode.model.pull_request import PullRequestModel
33 32 from rhodecode.model.user import UserModel
34 33 from rhodecode.tests import (
35 assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36 35 from rhodecode.tests.utils import AssertResponse
37 36
38 37
39 38 def route_path(name, params=None, **kwargs):
40 39 import urllib
41 40
42 41 base_url = {
43 42 'repo_changelog':'/{repo_name}/changelog',
44 43 'repo_changelog_file':'/{repo_name}/changelog/{commit_id}/{f_path}',
44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
45 56 }[name].format(**kwargs)
46 57
47 58 if params:
48 59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
49 60 return base_url
50 61
51 62
52 63 @pytest.mark.usefixtures('app', 'autologin_user')
53 64 @pytest.mark.backends("git", "hg")
54 class TestPullrequestsController(object):
65 class TestPullrequestsView(object):
55 66
56 67 def test_index(self, backend):
57 self.app.get(url(
58 controller='pullrequests', action='index',
68 self.app.get(route_path(
69 'pullrequest_new',
59 70 repo_name=backend.repo_name))
60 71
61 72 def test_option_menu_create_pull_request_exists(self, backend):
62 73 repo_name = backend.repo_name
63 74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
64 75
65 create_pr_link = '<a href="%s">Create Pull Request</a>' % url(
66 'pullrequest', repo_name=repo_name)
76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 'pullrequest_new', repo_name=repo_name)
67 78 response.mustcontain(create_pr_link)
68 79
69 80 def test_create_pr_form_with_raw_commit_id(self, backend):
70 81 repo = backend.repo
71 82
72 83 self.app.get(
73 url(controller='pullrequests', action='index',
84 route_path('pullrequest_new',
74 85 repo_name=repo.repo_name,
75 86 commit=repo.get_commit().raw_id),
76 87 status=200)
77 88
78 89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
79 90 def test_show(self, pr_util, pr_merge_enabled):
80 91 pull_request = pr_util.create_pull_request(
81 92 mergeable=pr_merge_enabled, enable_notifications=False)
82 93
83 response = self.app.get(url(
84 controller='pullrequests', action='show',
94 response = self.app.get(route_path(
95 'pullrequest_show',
85 96 repo_name=pull_request.target_repo.scm_instance().name,
86 pull_request_id=str(pull_request.pull_request_id)))
97 pull_request_id=pull_request.pull_request_id))
87 98
88 99 for commit_id in pull_request.revisions:
89 100 response.mustcontain(commit_id)
90 101
91 102 assert pull_request.target_ref_parts.type in response
92 103 assert pull_request.target_ref_parts.name in response
93 104 target_clone_url = pull_request.target_repo.clone_url()
94 105 assert target_clone_url in response
95 106
96 107 assert 'class="pull-request-merge"' in response
97 108 assert (
98 109 'Server-side pull request merging is disabled.'
99 110 in response) != pr_merge_enabled
100 111
101 112 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
102 113 # Logout
103 114 response = self.app.post(
104 115 h.route_path('logout'),
105 116 params={'csrf_token': csrf_token})
106 117 # Login as regular user
107 118 response = self.app.post(h.route_path('login'),
108 119 {'username': TEST_USER_REGULAR_LOGIN,
109 120 'password': 'test12'})
110 121
111 122 pull_request = pr_util.create_pull_request(
112 123 author=TEST_USER_REGULAR_LOGIN)
113 124
114 response = self.app.get(url(
115 controller='pullrequests', action='show',
125 response = self.app.get(route_path(
126 'pullrequest_show',
116 127 repo_name=pull_request.target_repo.scm_instance().name,
117 pull_request_id=str(pull_request.pull_request_id)))
128 pull_request_id=pull_request.pull_request_id))
118 129
119 130 response.mustcontain('Server-side pull request merging is disabled.')
120 131
121 132 assert_response = response.assert_response()
122 133 # for regular user without a merge permissions, we don't see it
123 134 assert_response.no_element_exists('#close-pull-request-action')
124 135
125 136 user_util.grant_user_permission_to_repo(
126 137 pull_request.target_repo,
127 138 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
128 139 'repository.write')
129 response = self.app.get(url(
130 controller='pullrequests', action='show',
140 response = self.app.get(route_path(
141 'pullrequest_show',
131 142 repo_name=pull_request.target_repo.scm_instance().name,
132 pull_request_id=str(pull_request.pull_request_id)))
143 pull_request_id=pull_request.pull_request_id))
133 144
134 145 response.mustcontain('Server-side pull request merging is disabled.')
135 146
136 147 assert_response = response.assert_response()
137 148 # now regular user has a merge permissions, we have CLOSE button
138 149 assert_response.one_element_exists('#close-pull-request-action')
139 150
140 151 def test_show_invalid_commit_id(self, pr_util):
141 152 # Simulating invalid revisions which will cause a lookup error
142 153 pull_request = pr_util.create_pull_request()
143 154 pull_request.revisions = ['invalid']
144 155 Session().add(pull_request)
145 156 Session().commit()
146 157
147 response = self.app.get(url(
148 controller='pullrequests', action='show',
158 response = self.app.get(route_path(
159 'pullrequest_show',
149 160 repo_name=pull_request.target_repo.scm_instance().name,
150 pull_request_id=str(pull_request.pull_request_id)))
161 pull_request_id=pull_request.pull_request_id))
151 162
152 163 for commit_id in pull_request.revisions:
153 164 response.mustcontain(commit_id)
154 165
155 166 def test_show_invalid_source_reference(self, pr_util):
156 167 pull_request = pr_util.create_pull_request()
157 168 pull_request.source_ref = 'branch:b:invalid'
158 169 Session().add(pull_request)
159 170 Session().commit()
160 171
161 self.app.get(url(
162 controller='pullrequests', action='show',
172 self.app.get(route_path(
173 'pullrequest_show',
163 174 repo_name=pull_request.target_repo.scm_instance().name,
164 pull_request_id=str(pull_request.pull_request_id)))
175 pull_request_id=pull_request.pull_request_id))
165 176
166 177 def test_edit_title_description(self, pr_util, csrf_token):
167 178 pull_request = pr_util.create_pull_request()
168 179 pull_request_id = pull_request.pull_request_id
169 180
170 181 response = self.app.post(
171 url(controller='pullrequests', action='update',
182 route_path('pullrequest_update',
172 183 repo_name=pull_request.target_repo.repo_name,
173 pull_request_id=str(pull_request_id)),
184 pull_request_id=pull_request_id),
174 185 params={
175 186 'edit_pull_request': 'true',
176 '_method': 'put',
177 187 'title': 'New title',
178 188 'description': 'New description',
179 189 'csrf_token': csrf_token})
180 190
181 191 assert_session_flash(
182 192 response, u'Pull request title & description updated.',
183 193 category='success')
184 194
185 195 pull_request = PullRequest.get(pull_request_id)
186 196 assert pull_request.title == 'New title'
187 197 assert pull_request.description == 'New description'
188 198
189 199 def test_edit_title_description_closed(self, pr_util, csrf_token):
190 200 pull_request = pr_util.create_pull_request()
191 201 pull_request_id = pull_request.pull_request_id
192 202 pr_util.close()
193 203
194 204 response = self.app.post(
195 url(controller='pullrequests', action='update',
205 route_path('pullrequest_update',
196 206 repo_name=pull_request.target_repo.repo_name,
197 pull_request_id=str(pull_request_id)),
207 pull_request_id=pull_request_id),
198 208 params={
199 209 'edit_pull_request': 'true',
200 '_method': 'put',
201 210 'title': 'New title',
202 211 'description': 'New description',
203 212 'csrf_token': csrf_token})
204 213
205 214 assert_session_flash(
206 215 response, u'Cannot update closed pull requests.',
207 216 category='error')
208 217
209 218 def test_update_invalid_source_reference(self, pr_util, csrf_token):
210 219 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
211 220
212 221 pull_request = pr_util.create_pull_request()
213 222 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
214 223 Session().add(pull_request)
215 224 Session().commit()
216 225
217 226 pull_request_id = pull_request.pull_request_id
218 227
219 228 response = self.app.post(
220 url(controller='pullrequests', action='update',
229 route_path('pullrequest_update',
221 230 repo_name=pull_request.target_repo.repo_name,
222 pull_request_id=str(pull_request_id)),
223 params={'update_commits': 'true', '_method': 'put',
231 pull_request_id=pull_request_id),
232 params={'update_commits': 'true',
224 233 'csrf_token': csrf_token})
225 234
226 235 expected_msg = PullRequestModel.UPDATE_STATUS_MESSAGES[
227 236 UpdateFailureReason.MISSING_SOURCE_REF]
228 237 assert_session_flash(response, expected_msg, category='error')
229 238
230 239 def test_missing_target_reference(self, pr_util, csrf_token):
231 240 from rhodecode.lib.vcs.backends.base import MergeFailureReason
232 241 pull_request = pr_util.create_pull_request(
233 242 approved=True, mergeable=True)
234 243 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
235 244 Session().add(pull_request)
236 245 Session().commit()
237 246
238 247 pull_request_id = pull_request.pull_request_id
239 pull_request_url = url(
240 controller='pullrequests', action='show',
248 pull_request_url = route_path(
249 'pullrequest_show',
241 250 repo_name=pull_request.target_repo.repo_name,
242 pull_request_id=str(pull_request_id))
251 pull_request_id=pull_request_id)
243 252
244 253 response = self.app.get(pull_request_url)
245 254
246 255 assertr = AssertResponse(response)
247 256 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
248 257 MergeFailureReason.MISSING_TARGET_REF]
249 258 assertr.element_contains(
250 259 'span[data-role="merge-message"]', str(expected_msg))
251 260
252 261 def test_comment_and_close_pull_request_custom_message_approved(
253 262 self, pr_util, csrf_token, xhr_header):
254 263
255 264 pull_request = pr_util.create_pull_request(approved=True)
256 265 pull_request_id = pull_request.pull_request_id
257 266 author = pull_request.user_id
258 267 repo = pull_request.target_repo.repo_id
259 268
260 269 self.app.post(
261 url(controller='pullrequests',
262 action='comment',
270 route_path('pullrequest_comment_create',
263 271 repo_name=pull_request.target_repo.scm_instance().name,
264 pull_request_id=str(pull_request_id)),
272 pull_request_id=pull_request_id),
265 273 params={
266 274 'close_pull_request': '1',
267 275 'text': 'Closing a PR',
268 276 'csrf_token': csrf_token},
269 277 extra_environ=xhr_header,)
270 278
271 279 journal = UserLog.query()\
272 280 .filter(UserLog.user_id == author)\
273 281 .filter(UserLog.repository_id == repo) \
274 282 .order_by('user_log_id') \
275 283 .all()
276 284 assert journal[-1].action == 'repo.pull_request.close'
277 285
278 286 pull_request = PullRequest.get(pull_request_id)
279 287 assert pull_request.is_closed()
280 288
281 289 status = ChangesetStatusModel().get_status(
282 290 pull_request.source_repo, pull_request=pull_request)
283 291 assert status == ChangesetStatus.STATUS_APPROVED
284 292 comments = ChangesetComment().query() \
285 293 .filter(ChangesetComment.pull_request == pull_request) \
286 294 .order_by(ChangesetComment.comment_id.asc())\
287 295 .all()
288 296 assert comments[-1].text == 'Closing a PR'
289 297
290 298 def test_comment_force_close_pull_request_rejected(
291 299 self, pr_util, csrf_token, xhr_header):
292 300 pull_request = pr_util.create_pull_request()
293 301 pull_request_id = pull_request.pull_request_id
294 302 PullRequestModel().update_reviewers(
295 303 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
296 304 pull_request.author)
297 305 author = pull_request.user_id
298 306 repo = pull_request.target_repo.repo_id
299 307
300 308 self.app.post(
301 url(controller='pullrequests',
302 action='comment',
309 route_path('pullrequest_comment_create',
303 310 repo_name=pull_request.target_repo.scm_instance().name,
304 pull_request_id=str(pull_request_id)),
311 pull_request_id=pull_request_id),
305 312 params={
306 313 'close_pull_request': '1',
307 314 'csrf_token': csrf_token},
308 315 extra_environ=xhr_header)
309 316
310 317 pull_request = PullRequest.get(pull_request_id)
311 318
312 319 journal = UserLog.query()\
313 320 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
314 321 .order_by('user_log_id') \
315 322 .all()
316 323 assert journal[-1].action == 'repo.pull_request.close'
317 324
318 325 # check only the latest status, not the review status
319 326 status = ChangesetStatusModel().get_status(
320 327 pull_request.source_repo, pull_request=pull_request)
321 328 assert status == ChangesetStatus.STATUS_REJECTED
322 329
323 330 def test_comment_and_close_pull_request(
324 331 self, pr_util, csrf_token, xhr_header):
325 332 pull_request = pr_util.create_pull_request()
326 333 pull_request_id = pull_request.pull_request_id
327 334
328 335 response = self.app.post(
329 url(controller='pullrequests',
330 action='comment',
336 route_path('pullrequest_comment_create',
331 337 repo_name=pull_request.target_repo.scm_instance().name,
332 pull_request_id=str(pull_request.pull_request_id)),
338 pull_request_id=pull_request.pull_request_id),
333 339 params={
334 340 'close_pull_request': 'true',
335 341 'csrf_token': csrf_token},
336 342 extra_environ=xhr_header)
337 343
338 344 assert response.json
339 345
340 346 pull_request = PullRequest.get(pull_request_id)
341 347 assert pull_request.is_closed()
342 348
343 349 # check only the latest status, not the review status
344 350 status = ChangesetStatusModel().get_status(
345 351 pull_request.source_repo, pull_request=pull_request)
346 352 assert status == ChangesetStatus.STATUS_REJECTED
347 353
348 354 def test_create_pull_request(self, backend, csrf_token):
349 355 commits = [
350 356 {'message': 'ancestor'},
351 357 {'message': 'change'},
352 358 {'message': 'change2'},
353 359 ]
354 360 commit_ids = backend.create_master_repo(commits)
355 361 target = backend.create_repo(heads=['ancestor'])
356 362 source = backend.create_repo(heads=['change2'])
357 363
358 364 response = self.app.post(
359 url(
360 controller='pullrequests',
361 action='create',
362 repo_name=source.repo_name
363 ),
365 route_path('pullrequest_create', repo_name=source.repo_name),
364 366 [
365 367 ('source_repo', source.repo_name),
366 368 ('source_ref', 'branch:default:' + commit_ids['change2']),
367 369 ('target_repo', target.repo_name),
368 370 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
369 371 ('common_ancestor', commit_ids['ancestor']),
370 372 ('pullrequest_desc', 'Description'),
371 373 ('pullrequest_title', 'Title'),
372 374 ('__start__', 'review_members:sequence'),
373 375 ('__start__', 'reviewer:mapping'),
374 376 ('user_id', '1'),
375 377 ('__start__', 'reasons:sequence'),
376 378 ('reason', 'Some reason'),
377 379 ('__end__', 'reasons:sequence'),
378 380 ('mandatory', 'False'),
379 381 ('__end__', 'reviewer:mapping'),
380 382 ('__end__', 'review_members:sequence'),
381 383 ('__start__', 'revisions:sequence'),
382 384 ('revisions', commit_ids['change']),
383 385 ('revisions', commit_ids['change2']),
384 386 ('__end__', 'revisions:sequence'),
385 387 ('user', ''),
386 388 ('csrf_token', csrf_token),
387 389 ],
388 390 status=302)
389 391
390 392 location = response.headers['Location']
391 393 pull_request_id = location.rsplit('/', 1)[1]
392 394 assert pull_request_id != 'new'
393 395 pull_request = PullRequest.get(int(pull_request_id))
394 396
395 397 # check that we have now both revisions
396 398 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
397 399 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
398 400 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
399 401 assert pull_request.target_ref == expected_target_ref
400 402
401 403 def test_reviewer_notifications(self, backend, csrf_token):
402 404 # We have to use the app.post for this test so it will create the
403 405 # notifications properly with the new PR
404 406 commits = [
405 407 {'message': 'ancestor',
406 408 'added': [FileNode('file_A', content='content_of_ancestor')]},
407 409 {'message': 'change',
408 410 'added': [FileNode('file_a', content='content_of_change')]},
409 411 {'message': 'change-child'},
410 412 {'message': 'ancestor-child', 'parents': ['ancestor'],
411 413 'added': [
412 414 FileNode('file_B', content='content_of_ancestor_child')]},
413 415 {'message': 'ancestor-child-2'},
414 416 ]
415 417 commit_ids = backend.create_master_repo(commits)
416 418 target = backend.create_repo(heads=['ancestor-child'])
417 419 source = backend.create_repo(heads=['change'])
418 420
419 421 response = self.app.post(
420 url(
421 controller='pullrequests',
422 action='create',
423 repo_name=source.repo_name
424 ),
422 route_path('pullrequest_create', repo_name=source.repo_name),
425 423 [
426 424 ('source_repo', source.repo_name),
427 425 ('source_ref', 'branch:default:' + commit_ids['change']),
428 426 ('target_repo', target.repo_name),
429 427 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
430 428 ('common_ancestor', commit_ids['ancestor']),
431 429 ('pullrequest_desc', 'Description'),
432 430 ('pullrequest_title', 'Title'),
433 431 ('__start__', 'review_members:sequence'),
434 432 ('__start__', 'reviewer:mapping'),
435 433 ('user_id', '2'),
436 434 ('__start__', 'reasons:sequence'),
437 435 ('reason', 'Some reason'),
438 436 ('__end__', 'reasons:sequence'),
439 437 ('mandatory', 'False'),
440 438 ('__end__', 'reviewer:mapping'),
441 439 ('__end__', 'review_members:sequence'),
442 440 ('__start__', 'revisions:sequence'),
443 441 ('revisions', commit_ids['change']),
444 442 ('__end__', 'revisions:sequence'),
445 443 ('user', ''),
446 444 ('csrf_token', csrf_token),
447 445 ],
448 446 status=302)
449 447
450 448 location = response.headers['Location']
451 449
452 450 pull_request_id = location.rsplit('/', 1)[1]
453 451 assert pull_request_id != 'new'
454 452 pull_request = PullRequest.get(int(pull_request_id))
455 453
456 454 # Check that a notification was made
457 455 notifications = Notification.query()\
458 456 .filter(Notification.created_by == pull_request.author.user_id,
459 457 Notification.type_ == Notification.TYPE_PULL_REQUEST,
460 458 Notification.subject.contains(
461 459 "wants you to review pull request #%s" % pull_request_id))
462 460 assert len(notifications.all()) == 1
463 461
464 462 # Change reviewers and check that a notification was made
465 463 PullRequestModel().update_reviewers(
466 464 pull_request.pull_request_id, [(1, [], False)],
467 465 pull_request.author)
468 466 assert len(notifications.all()) == 2
469 467
470 468 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
471 469 csrf_token):
472 470 commits = [
473 471 {'message': 'ancestor',
474 472 'added': [FileNode('file_A', content='content_of_ancestor')]},
475 473 {'message': 'change',
476 474 'added': [FileNode('file_a', content='content_of_change')]},
477 475 {'message': 'change-child'},
478 476 {'message': 'ancestor-child', 'parents': ['ancestor'],
479 477 'added': [
480 478 FileNode('file_B', content='content_of_ancestor_child')]},
481 479 {'message': 'ancestor-child-2'},
482 480 ]
483 481 commit_ids = backend.create_master_repo(commits)
484 482 target = backend.create_repo(heads=['ancestor-child'])
485 483 source = backend.create_repo(heads=['change'])
486 484
487 485 response = self.app.post(
488 url(
489 controller='pullrequests',
490 action='create',
491 repo_name=source.repo_name
492 ),
486 route_path('pullrequest_create', repo_name=source.repo_name),
493 487 [
494 488 ('source_repo', source.repo_name),
495 489 ('source_ref', 'branch:default:' + commit_ids['change']),
496 490 ('target_repo', target.repo_name),
497 491 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
498 492 ('common_ancestor', commit_ids['ancestor']),
499 493 ('pullrequest_desc', 'Description'),
500 494 ('pullrequest_title', 'Title'),
501 495 ('__start__', 'review_members:sequence'),
502 496 ('__start__', 'reviewer:mapping'),
503 497 ('user_id', '1'),
504 498 ('__start__', 'reasons:sequence'),
505 499 ('reason', 'Some reason'),
506 500 ('__end__', 'reasons:sequence'),
507 501 ('mandatory', 'False'),
508 502 ('__end__', 'reviewer:mapping'),
509 503 ('__end__', 'review_members:sequence'),
510 504 ('__start__', 'revisions:sequence'),
511 505 ('revisions', commit_ids['change']),
512 506 ('__end__', 'revisions:sequence'),
513 507 ('user', ''),
514 508 ('csrf_token', csrf_token),
515 509 ],
516 510 status=302)
517 511
518 512 location = response.headers['Location']
519 513
520 514 pull_request_id = location.rsplit('/', 1)[1]
521 515 assert pull_request_id != 'new'
522 516 pull_request = PullRequest.get(int(pull_request_id))
523 517
524 518 # target_ref has to point to the ancestor's commit_id in order to
525 519 # show the correct diff
526 520 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
527 521 assert pull_request.target_ref == expected_target_ref
528 522
529 523 # Check generated diff contents
530 524 response = response.follow()
531 525 assert 'content_of_ancestor' not in response.body
532 526 assert 'content_of_ancestor-child' not in response.body
533 527 assert 'content_of_change' in response.body
534 528
535 529 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
536 530 # Clear any previous calls to rcextensions
537 531 rhodecode.EXTENSIONS.calls.clear()
538 532
539 533 pull_request = pr_util.create_pull_request(
540 534 approved=True, mergeable=True)
541 535 pull_request_id = pull_request.pull_request_id
542 536 repo_name = pull_request.target_repo.scm_instance().name,
543 537
544 538 response = self.app.post(
545 url(controller='pullrequests',
546 action='merge',
539 route_path('pullrequest_merge',
547 540 repo_name=str(repo_name[0]),
548 pull_request_id=str(pull_request_id)),
541 pull_request_id=pull_request_id),
549 542 params={'csrf_token': csrf_token}).follow()
550 543
551 544 pull_request = PullRequest.get(pull_request_id)
552 545
553 546 assert response.status_int == 200
554 547 assert pull_request.is_closed()
555 548 assert_pull_request_status(
556 549 pull_request, ChangesetStatus.STATUS_APPROVED)
557 550
558 551 # Check the relevant log entries were added
559 552 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
560 553 actions = [log.action for log in user_logs]
561 554 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
562 555 expected_actions = [
563 556 u'repo.pull_request.close',
564 557 u'repo.pull_request.merge',
565 558 u'repo.pull_request.comment.create'
566 559 ]
567 560 assert actions == expected_actions
568 561
569 562 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
570 563 actions = [log for log in user_logs]
571 564 assert actions[-1].action == 'user.push'
572 565 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
573 566
574 567 # Check post_push rcextension was really executed
575 568 push_calls = rhodecode.EXTENSIONS.calls['post_push']
576 569 assert len(push_calls) == 1
577 570 unused_last_call_args, last_call_kwargs = push_calls[0]
578 571 assert last_call_kwargs['action'] == 'push'
579 572 assert last_call_kwargs['pushed_revs'] == pr_commit_ids
580 573
581 574 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
582 575 pull_request = pr_util.create_pull_request(mergeable=False)
583 576 pull_request_id = pull_request.pull_request_id
584 577 pull_request = PullRequest.get(pull_request_id)
585 578
586 579 response = self.app.post(
587 url(controller='pullrequests',
588 action='merge',
580 route_path('pullrequest_merge',
589 581 repo_name=pull_request.target_repo.scm_instance().name,
590 pull_request_id=str(pull_request.pull_request_id)),
582 pull_request_id=pull_request.pull_request_id),
591 583 params={'csrf_token': csrf_token}).follow()
592 584
593 585 assert response.status_int == 200
594 586 response.mustcontain(
595 587 'Merge is not currently possible because of below failed checks.')
596 588 response.mustcontain('Server-side pull request merging is disabled.')
597 589
598 590 @pytest.mark.skip_backends('svn')
599 591 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
600 592 pull_request = pr_util.create_pull_request(mergeable=True)
601 593 pull_request_id = pull_request.pull_request_id
602 repo_name = pull_request.target_repo.scm_instance().name,
594 repo_name = pull_request.target_repo.scm_instance().name
603 595
604 596 response = self.app.post(
605 url(controller='pullrequests',
606 action='merge',
607 repo_name=str(repo_name[0]),
608 pull_request_id=str(pull_request_id)),
597 route_path('pullrequest_merge',
598 repo_name=repo_name,
599 pull_request_id=pull_request_id),
609 600 params={'csrf_token': csrf_token}).follow()
610 601
611 602 assert response.status_int == 200
612 603
613 604 response.mustcontain(
614 605 'Merge is not currently possible because of below failed checks.')
615 606 response.mustcontain('Pull request reviewer approval is pending.')
616 607
608 def test_merge_pull_request_renders_failure_reason(
609 self, user_regular, csrf_token, pr_util):
610 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
611 pull_request_id = pull_request.pull_request_id
612 repo_name = pull_request.target_repo.scm_instance().name
613
614 model_patcher = mock.patch.multiple(
615 PullRequestModel,
616 merge=mock.Mock(return_value=MergeResponse(
617 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
618 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
619
620 with model_patcher:
621 response = self.app.post(
622 route_path('pullrequest_merge',
623 repo_name=repo_name,
624 pull_request_id=pull_request_id),
625 params={'csrf_token': csrf_token}, status=302)
626
627 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
628 MergeFailureReason.PUSH_FAILED])
629
617 630 def test_update_source_revision(self, backend, csrf_token):
618 631 commits = [
619 632 {'message': 'ancestor'},
620 633 {'message': 'change'},
621 634 {'message': 'change-2'},
622 635 ]
623 636 commit_ids = backend.create_master_repo(commits)
624 637 target = backend.create_repo(heads=['ancestor'])
625 638 source = backend.create_repo(heads=['change'])
626 639
627 640 # create pr from a in source to A in target
628 641 pull_request = PullRequest()
629 642 pull_request.source_repo = source
630 643 # TODO: johbo: Make sure that we write the source ref this way!
631 644 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
632 645 branch=backend.default_branch_name, commit_id=commit_ids['change'])
633 646 pull_request.target_repo = target
634 647
635 648 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
636 649 branch=backend.default_branch_name,
637 650 commit_id=commit_ids['ancestor'])
638 651 pull_request.revisions = [commit_ids['change']]
639 652 pull_request.title = u"Test"
640 653 pull_request.description = u"Description"
641 654 pull_request.author = UserModel().get_by_username(
642 655 TEST_USER_ADMIN_LOGIN)
643 656 Session().add(pull_request)
644 657 Session().commit()
645 658 pull_request_id = pull_request.pull_request_id
646 659
647 660 # source has ancestor - change - change-2
648 661 backend.pull_heads(source, heads=['change-2'])
649 662
650 663 # update PR
651 664 self.app.post(
652 url(controller='pullrequests', action='update',
665 route_path('pullrequest_update',
653 666 repo_name=target.repo_name,
654 pull_request_id=str(pull_request_id)),
655 params={'update_commits': 'true', '_method': 'put',
667 pull_request_id=pull_request_id),
668 params={'update_commits': 'true',
656 669 'csrf_token': csrf_token})
657 670
658 671 # check that we have now both revisions
659 672 pull_request = PullRequest.get(pull_request_id)
660 673 assert pull_request.revisions == [
661 674 commit_ids['change-2'], commit_ids['change']]
662 675
663 676 # TODO: johbo: this should be a test on its own
664 response = self.app.get(url(
665 controller='pullrequests', action='index',
677 response = self.app.get(route_path(
678 'pullrequest_new',
666 679 repo_name=target.repo_name))
667 680 assert response.status_int == 200
668 681 assert 'Pull request updated to' in response.body
669 682 assert 'with 1 added, 0 removed commits.' in response.body
670 683
671 684 def test_update_target_revision(self, backend, csrf_token):
672 685 commits = [
673 686 {'message': 'ancestor'},
674 687 {'message': 'change'},
675 688 {'message': 'ancestor-new', 'parents': ['ancestor']},
676 689 {'message': 'change-rebased'},
677 690 ]
678 691 commit_ids = backend.create_master_repo(commits)
679 692 target = backend.create_repo(heads=['ancestor'])
680 693 source = backend.create_repo(heads=['change'])
681 694
682 695 # create pr from a in source to A in target
683 696 pull_request = PullRequest()
684 697 pull_request.source_repo = source
685 698 # TODO: johbo: Make sure that we write the source ref this way!
686 699 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
687 700 branch=backend.default_branch_name, commit_id=commit_ids['change'])
688 701 pull_request.target_repo = target
689 702 # TODO: johbo: Target ref should be branch based, since tip can jump
690 703 # from branch to branch
691 704 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
692 705 branch=backend.default_branch_name,
693 706 commit_id=commit_ids['ancestor'])
694 707 pull_request.revisions = [commit_ids['change']]
695 708 pull_request.title = u"Test"
696 709 pull_request.description = u"Description"
697 710 pull_request.author = UserModel().get_by_username(
698 711 TEST_USER_ADMIN_LOGIN)
699 712 Session().add(pull_request)
700 713 Session().commit()
701 714 pull_request_id = pull_request.pull_request_id
702 715
703 716 # target has ancestor - ancestor-new
704 717 # source has ancestor - ancestor-new - change-rebased
705 718 backend.pull_heads(target, heads=['ancestor-new'])
706 719 backend.pull_heads(source, heads=['change-rebased'])
707 720
708 721 # update PR
709 722 self.app.post(
710 url(controller='pullrequests', action='update',
723 route_path('pullrequest_update',
711 724 repo_name=target.repo_name,
712 pull_request_id=str(pull_request_id)),
713 params={'update_commits': 'true', '_method': 'put',
725 pull_request_id=pull_request_id),
726 params={'update_commits': 'true',
714 727 'csrf_token': csrf_token},
715 728 status=200)
716 729
717 730 # check that we have now both revisions
718 731 pull_request = PullRequest.get(pull_request_id)
719 732 assert pull_request.revisions == [commit_ids['change-rebased']]
720 733 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
721 734 branch=backend.default_branch_name,
722 735 commit_id=commit_ids['ancestor-new'])
723 736
724 737 # TODO: johbo: This should be a test on its own
725 response = self.app.get(url(
726 controller='pullrequests', action='index',
738 response = self.app.get(route_path(
739 'pullrequest_new',
727 740 repo_name=target.repo_name))
728 741 assert response.status_int == 200
729 742 assert 'Pull request updated to' in response.body
730 743 assert 'with 1 added, 1 removed commits.' in response.body
731 744
732 745 def test_update_of_ancestor_reference(self, backend, csrf_token):
733 746 commits = [
734 747 {'message': 'ancestor'},
735 748 {'message': 'change'},
736 749 {'message': 'change-2'},
737 750 {'message': 'ancestor-new', 'parents': ['ancestor']},
738 751 {'message': 'change-rebased'},
739 752 ]
740 753 commit_ids = backend.create_master_repo(commits)
741 754 target = backend.create_repo(heads=['ancestor'])
742 755 source = backend.create_repo(heads=['change'])
743 756
744 757 # create pr from a in source to A in target
745 758 pull_request = PullRequest()
746 759 pull_request.source_repo = source
747 760 # TODO: johbo: Make sure that we write the source ref this way!
748 761 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
749 762 branch=backend.default_branch_name,
750 763 commit_id=commit_ids['change'])
751 764 pull_request.target_repo = target
752 765 # TODO: johbo: Target ref should be branch based, since tip can jump
753 766 # from branch to branch
754 767 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
755 768 branch=backend.default_branch_name,
756 769 commit_id=commit_ids['ancestor'])
757 770 pull_request.revisions = [commit_ids['change']]
758 771 pull_request.title = u"Test"
759 772 pull_request.description = u"Description"
760 773 pull_request.author = UserModel().get_by_username(
761 774 TEST_USER_ADMIN_LOGIN)
762 775 Session().add(pull_request)
763 776 Session().commit()
764 777 pull_request_id = pull_request.pull_request_id
765 778
766 779 # target has ancestor - ancestor-new
767 780 # source has ancestor - ancestor-new - change-rebased
768 781 backend.pull_heads(target, heads=['ancestor-new'])
769 782 backend.pull_heads(source, heads=['change-rebased'])
770 783
771 784 # update PR
772 785 self.app.post(
773 url(controller='pullrequests', action='update',
786 route_path('pullrequest_update',
774 787 repo_name=target.repo_name,
775 pull_request_id=str(pull_request_id)),
776 params={'update_commits': 'true', '_method': 'put',
788 pull_request_id=pull_request_id),
789 params={'update_commits': 'true',
777 790 'csrf_token': csrf_token},
778 791 status=200)
779 792
780 793 # Expect the target reference to be updated correctly
781 794 pull_request = PullRequest.get(pull_request_id)
782 795 assert pull_request.revisions == [commit_ids['change-rebased']]
783 796 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
784 797 branch=backend.default_branch_name,
785 798 commit_id=commit_ids['ancestor-new'])
786 799 assert pull_request.target_ref == expected_target_ref
787 800
788 801 def test_remove_pull_request_branch(self, backend_git, csrf_token):
789 802 branch_name = 'development'
790 803 commits = [
791 804 {'message': 'initial-commit'},
792 805 {'message': 'old-feature'},
793 806 {'message': 'new-feature', 'branch': branch_name},
794 807 ]
795 808 repo = backend_git.create_repo(commits)
796 809 commit_ids = backend_git.commit_ids
797 810
798 811 pull_request = PullRequest()
799 812 pull_request.source_repo = repo
800 813 pull_request.target_repo = repo
801 814 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
802 815 branch=branch_name, commit_id=commit_ids['new-feature'])
803 816 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
804 817 branch=backend_git.default_branch_name,
805 818 commit_id=commit_ids['old-feature'])
806 819 pull_request.revisions = [commit_ids['new-feature']]
807 820 pull_request.title = u"Test"
808 821 pull_request.description = u"Description"
809 822 pull_request.author = UserModel().get_by_username(
810 823 TEST_USER_ADMIN_LOGIN)
811 824 Session().add(pull_request)
812 825 Session().commit()
813 826
814 827 vcs = repo.scm_instance()
815 828 vcs.remove_ref('refs/heads/{}'.format(branch_name))
816 829
817 response = self.app.get(url(
818 controller='pullrequests', action='show',
830 response = self.app.get(route_path(
831 'pullrequest_show',
819 832 repo_name=repo.repo_name,
820 pull_request_id=str(pull_request.pull_request_id)))
833 pull_request_id=pull_request.pull_request_id))
821 834
822 835 assert response.status_int == 200
823 836 assert_response = AssertResponse(response)
824 837 assert_response.element_contains(
825 838 '#changeset_compare_view_content .alert strong',
826 839 'Missing commits')
827 840 assert_response.element_contains(
828 841 '#changeset_compare_view_content .alert',
829 842 'This pull request cannot be displayed, because one or more'
830 843 ' commits no longer exist in the source repository.')
831 844
832 845 def test_strip_commits_from_pull_request(
833 846 self, backend, pr_util, csrf_token):
834 847 commits = [
835 848 {'message': 'initial-commit'},
836 849 {'message': 'old-feature'},
837 850 {'message': 'new-feature', 'parents': ['initial-commit']},
838 851 ]
839 852 pull_request = pr_util.create_pull_request(
840 853 commits, target_head='initial-commit', source_head='new-feature',
841 854 revisions=['new-feature'])
842 855
843 856 vcs = pr_util.source_repository.scm_instance()
844 857 if backend.alias == 'git':
845 858 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
846 859 else:
847 860 vcs.strip(pr_util.commit_ids['new-feature'])
848 861
849 response = self.app.get(url(
850 controller='pullrequests', action='show',
862 response = self.app.get(route_path(
863 'pullrequest_show',
851 864 repo_name=pr_util.target_repository.repo_name,
852 pull_request_id=str(pull_request.pull_request_id)))
865 pull_request_id=pull_request.pull_request_id))
853 866
854 867 assert response.status_int == 200
855 868 assert_response = AssertResponse(response)
856 869 assert_response.element_contains(
857 870 '#changeset_compare_view_content .alert strong',
858 871 'Missing commits')
859 872 assert_response.element_contains(
860 873 '#changeset_compare_view_content .alert',
861 874 'This pull request cannot be displayed, because one or more'
862 875 ' commits no longer exist in the source repository.')
863 876 assert_response.element_contains(
864 877 '#update_commits',
865 878 'Update commits')
866 879
867 880 def test_strip_commits_and_update(
868 881 self, backend, pr_util, csrf_token):
869 882 commits = [
870 883 {'message': 'initial-commit'},
871 884 {'message': 'old-feature'},
872 885 {'message': 'new-feature', 'parents': ['old-feature']},
873 886 ]
874 887 pull_request = pr_util.create_pull_request(
875 888 commits, target_head='old-feature', source_head='new-feature',
876 889 revisions=['new-feature'], mergeable=True)
877 890
878 891 vcs = pr_util.source_repository.scm_instance()
879 892 if backend.alias == 'git':
880 893 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
881 894 else:
882 895 vcs.strip(pr_util.commit_ids['new-feature'])
883 896
884 897 response = self.app.post(
885 url(controller='pullrequests', action='update',
898 route_path('pullrequest_update',
886 899 repo_name=pull_request.target_repo.repo_name,
887 pull_request_id=str(pull_request.pull_request_id)),
888 params={'update_commits': 'true', '_method': 'put',
900 pull_request_id=pull_request.pull_request_id),
901 params={'update_commits': 'true',
889 902 'csrf_token': csrf_token})
890 903
891 904 assert response.status_int == 200
892 905 assert response.body == 'true'
893 906
894 907 # Make sure that after update, it won't raise 500 errors
895 response = self.app.get(url(
896 controller='pullrequests', action='show',
908 response = self.app.get(route_path(
909 'pullrequest_show',
897 910 repo_name=pr_util.target_repository.repo_name,
898 pull_request_id=str(pull_request.pull_request_id)))
911 pull_request_id=pull_request.pull_request_id))
899 912
900 913 assert response.status_int == 200
901 914 assert_response = AssertResponse(response)
902 915 assert_response.element_contains(
903 916 '#changeset_compare_view_content .alert strong',
904 917 'Missing commits')
905 918
906 919 def test_branch_is_a_link(self, pr_util):
907 920 pull_request = pr_util.create_pull_request()
908 921 pull_request.source_ref = 'branch:origin:1234567890abcdef'
909 922 pull_request.target_ref = 'branch:target:abcdef1234567890'
910 923 Session().add(pull_request)
911 924 Session().commit()
912 925
913 response = self.app.get(url(
914 controller='pullrequests', action='show',
926 response = self.app.get(route_path(
927 'pullrequest_show',
915 928 repo_name=pull_request.target_repo.scm_instance().name,
916 pull_request_id=str(pull_request.pull_request_id)))
929 pull_request_id=pull_request.pull_request_id))
917 930 assert response.status_int == 200
918 931 assert_response = AssertResponse(response)
919 932
920 933 origin = assert_response.get_element('.pr-origininfo .tag')
921 934 origin_children = origin.getchildren()
922 935 assert len(origin_children) == 1
923 936 target = assert_response.get_element('.pr-targetinfo .tag')
924 937 target_children = target.getchildren()
925 938 assert len(target_children) == 1
926 939
927 940 expected_origin_link = route_path(
928 941 'repo_changelog',
929 942 repo_name=pull_request.source_repo.scm_instance().name,
930 943 params=dict(branch='origin'))
931 944 expected_target_link = route_path(
932 945 'repo_changelog',
933 946 repo_name=pull_request.target_repo.scm_instance().name,
934 947 params=dict(branch='target'))
935 948 assert origin_children[0].attrib['href'] == expected_origin_link
936 949 assert origin_children[0].text == 'branch: origin'
937 950 assert target_children[0].attrib['href'] == expected_target_link
938 951 assert target_children[0].text == 'branch: target'
939 952
940 953 def test_bookmark_is_not_a_link(self, pr_util):
941 954 pull_request = pr_util.create_pull_request()
942 955 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
943 956 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
944 957 Session().add(pull_request)
945 958 Session().commit()
946 959
947 response = self.app.get(url(
948 controller='pullrequests', action='show',
960 response = self.app.get(route_path(
961 'pullrequest_show',
949 962 repo_name=pull_request.target_repo.scm_instance().name,
950 pull_request_id=str(pull_request.pull_request_id)))
963 pull_request_id=pull_request.pull_request_id))
951 964 assert response.status_int == 200
952 965 assert_response = AssertResponse(response)
953 966
954 967 origin = assert_response.get_element('.pr-origininfo .tag')
955 968 assert origin.text.strip() == 'bookmark: origin'
956 969 assert origin.getchildren() == []
957 970
958 971 target = assert_response.get_element('.pr-targetinfo .tag')
959 972 assert target.text.strip() == 'bookmark: target'
960 973 assert target.getchildren() == []
961 974
962 975 def test_tag_is_not_a_link(self, pr_util):
963 976 pull_request = pr_util.create_pull_request()
964 977 pull_request.source_ref = 'tag:origin:1234567890abcdef'
965 978 pull_request.target_ref = 'tag:target:abcdef1234567890'
966 979 Session().add(pull_request)
967 980 Session().commit()
968 981
969 response = self.app.get(url(
970 controller='pullrequests', action='show',
982 response = self.app.get(route_path(
983 'pullrequest_show',
971 984 repo_name=pull_request.target_repo.scm_instance().name,
972 pull_request_id=str(pull_request.pull_request_id)))
985 pull_request_id=pull_request.pull_request_id))
973 986 assert response.status_int == 200
974 987 assert_response = AssertResponse(response)
975 988
976 989 origin = assert_response.get_element('.pr-origininfo .tag')
977 990 assert origin.text.strip() == 'tag: origin'
978 991 assert origin.getchildren() == []
979 992
980 993 target = assert_response.get_element('.pr-targetinfo .tag')
981 994 assert target.text.strip() == 'tag: target'
982 995 assert target.getchildren() == []
983 996
984 997 @pytest.mark.parametrize('mergeable', [True, False])
985 998 def test_shadow_repository_link(
986 999 self, mergeable, pr_util, http_host_only_stub):
987 1000 """
988 1001 Check that the pull request summary page displays a link to the shadow
989 1002 repository if the pull request is mergeable. If it is not mergeable
990 1003 the link should not be displayed.
991 1004 """
992 1005 pull_request = pr_util.create_pull_request(
993 1006 mergeable=mergeable, enable_notifications=False)
994 1007 target_repo = pull_request.target_repo.scm_instance()
995 1008 pr_id = pull_request.pull_request_id
996 1009 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
997 1010 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
998 1011
999 response = self.app.get(url(
1000 controller='pullrequests', action='show',
1012 response = self.app.get(route_path(
1013 'pullrequest_show',
1001 1014 repo_name=target_repo.name,
1002 pull_request_id=str(pr_id)))
1015 pull_request_id=pr_id))
1003 1016
1004 1017 assertr = AssertResponse(response)
1005 1018 if mergeable:
1006 1019 assertr.element_value_contains(
1007 1020 'div.pr-mergeinfo input', shadow_url)
1008 1021 assertr.element_value_contains(
1009 1022 'div.pr-mergeinfo input', 'pr-merge')
1010 1023 else:
1011 1024 assertr.no_element_exists('div.pr-mergeinfo')
1012 1025
1013 1026
1014 1027 @pytest.mark.usefixtures('app')
1015 1028 @pytest.mark.backends("git", "hg")
1016 1029 class TestPullrequestsControllerDelete(object):
1017 1030 def test_pull_request_delete_button_permissions_admin(
1018 1031 self, autologin_user, user_admin, pr_util):
1019 1032 pull_request = pr_util.create_pull_request(
1020 1033 author=user_admin.username, enable_notifications=False)
1021 1034
1022 response = self.app.get(url(
1023 controller='pullrequests', action='show',
1035 response = self.app.get(route_path(
1036 'pullrequest_show',
1024 1037 repo_name=pull_request.target_repo.scm_instance().name,
1025 pull_request_id=str(pull_request.pull_request_id)))
1038 pull_request_id=pull_request.pull_request_id))
1026 1039
1027 1040 response.mustcontain('id="delete_pullrequest"')
1028 1041 response.mustcontain('Confirm to delete this pull request')
1029 1042
1030 1043 def test_pull_request_delete_button_permissions_owner(
1031 1044 self, autologin_regular_user, user_regular, pr_util):
1032 1045 pull_request = pr_util.create_pull_request(
1033 1046 author=user_regular.username, enable_notifications=False)
1034 1047
1035 response = self.app.get(url(
1036 controller='pullrequests', action='show',
1048 response = self.app.get(route_path(
1049 'pullrequest_show',
1037 1050 repo_name=pull_request.target_repo.scm_instance().name,
1038 pull_request_id=str(pull_request.pull_request_id)))
1051 pull_request_id=pull_request.pull_request_id))
1039 1052
1040 1053 response.mustcontain('id="delete_pullrequest"')
1041 1054 response.mustcontain('Confirm to delete this pull request')
1042 1055
1043 1056 def test_pull_request_delete_button_permissions_forbidden(
1044 1057 self, autologin_regular_user, user_regular, user_admin, pr_util):
1045 1058 pull_request = pr_util.create_pull_request(
1046 1059 author=user_admin.username, enable_notifications=False)
1047 1060
1048 response = self.app.get(url(
1049 controller='pullrequests', action='show',
1061 response = self.app.get(route_path(
1062 'pullrequest_show',
1050 1063 repo_name=pull_request.target_repo.scm_instance().name,
1051 pull_request_id=str(pull_request.pull_request_id)))
1064 pull_request_id=pull_request.pull_request_id))
1052 1065 response.mustcontain(no=['id="delete_pullrequest"'])
1053 1066 response.mustcontain(no=['Confirm to delete this pull request'])
1054 1067
1055 1068 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1056 1069 self, autologin_regular_user, user_regular, user_admin, pr_util,
1057 1070 user_util):
1058 1071
1059 1072 pull_request = pr_util.create_pull_request(
1060 1073 author=user_admin.username, enable_notifications=False)
1061 1074
1062 1075 user_util.grant_user_permission_to_repo(
1063 1076 pull_request.target_repo, user_regular,
1064 1077 'repository.write')
1065 1078
1066 response = self.app.get(url(
1067 controller='pullrequests', action='show',
1079 response = self.app.get(route_path(
1080 'pullrequest_show',
1068 1081 repo_name=pull_request.target_repo.scm_instance().name,
1069 pull_request_id=str(pull_request.pull_request_id)))
1082 pull_request_id=pull_request.pull_request_id))
1070 1083
1071 1084 response.mustcontain('id="open_edit_pullrequest"')
1072 1085 response.mustcontain('id="delete_pullrequest"')
1073 1086 response.mustcontain(no=['Confirm to delete this pull request'])
1074 1087
1075 1088 def test_delete_comment_returns_404_if_comment_does_not_exist(
1076 1089 self, autologin_user, pr_util, user_admin):
1077 1090
1078 1091 pull_request = pr_util.create_pull_request(
1079 1092 author=user_admin.username, enable_notifications=False)
1080 1093
1081 self.app.get(url(
1082 controller='pullrequests', action='delete_comment',
1094 self.app.get(route_path(
1095 'pullrequest_comment_delete',
1083 1096 repo_name=pull_request.target_repo.scm_instance().name,
1097 pull_request_id=pull_request.pull_request_id,
1084 1098 comment_id=1024404), status=404)
1085 1099
1086 1100
1087 1101 def assert_pull_request_status(pull_request, expected_status):
1088 1102 status = ChangesetStatusModel().calculated_review_status(
1089 1103 pull_request=pull_request)
1090 1104 assert status == expected_status
1091 1105
1092 1106
1093 @pytest.mark.parametrize('action', ['index', 'create'])
1107 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1094 1108 @pytest.mark.usefixtures("autologin_user")
1095 def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action):
1096 response = app.get(url(
1097 controller='pullrequests', action=action,
1098 repo_name=backend_svn.repo_name))
1099 assert response.status_int == 302
1109 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1110 response = app.get(
1111 route_path(route, repo_name=backend_svn.repo_name), status=404)
1100 1112
1101 # Not allowed, redirect to the summary
1102 redirected = response.follow()
1103 summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name)
1104
1105 # URL adds leading slash and path doesn't have it
1106 assert redirected.request.path == summary_url
This diff has been collapsed as it changes many lines, (630 lines changed) Show them Hide them
@@ -1,584 +1,1182 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 import collections
22 23
23 import collections
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 import formencode
25 import peppercorn
26 from pyramid.httpexceptions import (
27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
25 28 from pyramid.view import view_config
29 from pyramid.renderers import render
26 30
31 from rhodecode import events
27 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
28 from rhodecode.lib import helpers as h, diffs, codeblocks
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.ext_json import json
29 37 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator)
31 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
34 RepositoryRequirementError, NodeDoesNotExistError
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 from rhodecode.model.changeset_status import ChangesetStatusModel
35 44 from rhodecode.model.comment import CommentsModel
36 from rhodecode.model.db import PullRequest, PullRequestVersion, \
37 ChangesetComment, ChangesetStatus
45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 ChangesetComment, ChangesetStatus, Repository)
47 from rhodecode.model.forms import PullRequestForm
48 from rhodecode.model.meta import Session
38 49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 from rhodecode.model.scm import ScmModel
39 51
40 52 log = logging.getLogger(__name__)
41 53
42 54
43 55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
44 56
45 57 def load_default_context(self):
46 58 c = self._get_local_tmpl_context(include_app_defaults=True)
47 59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
48 60 c.repo_info = self.db_repo
49 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
50 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
51 63 self._register_global_c(c)
52 64 return c
53 65
54 66 def _get_pull_requests_list(
55 67 self, repo_name, source, filter_type, opened_by, statuses):
56 68
57 69 draw, start, limit = self._extract_chunk(self.request)
58 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
59 71 _render = self.request.get_partial_renderer(
60 72 'data_table/_dt_elements.mako')
61 73
62 74 # pagination
63 75
64 76 if filter_type == 'awaiting_review':
65 77 pull_requests = PullRequestModel().get_awaiting_review(
66 78 repo_name, source=source, opened_by=opened_by,
67 79 statuses=statuses, offset=start, length=limit,
68 80 order_by=order_by, order_dir=order_dir)
69 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
70 82 repo_name, source=source, statuses=statuses,
71 83 opened_by=opened_by)
72 84 elif filter_type == 'awaiting_my_review':
73 85 pull_requests = PullRequestModel().get_awaiting_my_review(
74 86 repo_name, source=source, opened_by=opened_by,
75 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
76 88 offset=start, length=limit, order_by=order_by,
77 89 order_dir=order_dir)
78 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
79 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
80 92 statuses=statuses, opened_by=opened_by)
81 93 else:
82 94 pull_requests = PullRequestModel().get_all(
83 95 repo_name, source=source, opened_by=opened_by,
84 96 statuses=statuses, offset=start, length=limit,
85 97 order_by=order_by, order_dir=order_dir)
86 98 pull_requests_total_count = PullRequestModel().count_all(
87 99 repo_name, source=source, statuses=statuses,
88 100 opened_by=opened_by)
89 101
90 102 data = []
91 103 comments_model = CommentsModel()
92 104 for pr in pull_requests:
93 105 comments = comments_model.get_all_comments(
94 106 self.db_repo.repo_id, pull_request=pr)
95 107
96 108 data.append({
97 109 'name': _render('pullrequest_name',
98 110 pr.pull_request_id, pr.target_repo.repo_name),
99 111 'name_raw': pr.pull_request_id,
100 112 'status': _render('pullrequest_status',
101 113 pr.calculated_review_status()),
102 114 'title': _render(
103 115 'pullrequest_title', pr.title, pr.description),
104 116 'description': h.escape(pr.description),
105 117 'updated_on': _render('pullrequest_updated_on',
106 118 h.datetime_to_time(pr.updated_on)),
107 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
108 120 'created_on': _render('pullrequest_updated_on',
109 121 h.datetime_to_time(pr.created_on)),
110 122 'created_on_raw': h.datetime_to_time(pr.created_on),
111 123 'author': _render('pullrequest_author',
112 124 pr.author.full_contact, ),
113 125 'author_raw': pr.author.full_name,
114 126 'comments': _render('pullrequest_comments', len(comments)),
115 127 'comments_raw': len(comments),
116 128 'closed': pr.is_closed(),
117 129 })
118 130
119 131 data = ({
120 132 'draw': draw,
121 133 'data': data,
122 134 'recordsTotal': pull_requests_total_count,
123 135 'recordsFiltered': pull_requests_total_count,
124 136 })
125 137 return data
126 138
127 139 @LoginRequired()
128 140 @HasRepoPermissionAnyDecorator(
129 141 'repository.read', 'repository.write', 'repository.admin')
130 142 @view_config(
131 143 route_name='pullrequest_show_all', request_method='GET',
132 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
133 145 def pull_request_list(self):
134 146 c = self.load_default_context()
135 147
136 148 req_get = self.request.GET
137 149 c.source = str2bool(req_get.get('source'))
138 150 c.closed = str2bool(req_get.get('closed'))
139 151 c.my = str2bool(req_get.get('my'))
140 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
141 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
142 154
143 155 c.active = 'open'
144 156 if c.my:
145 157 c.active = 'my'
146 158 if c.closed:
147 159 c.active = 'closed'
148 160 if c.awaiting_review and not c.source:
149 161 c.active = 'awaiting'
150 162 if c.source and not c.awaiting_review:
151 163 c.active = 'source'
152 164 if c.awaiting_my_review:
153 165 c.active = 'awaiting_my'
154 166
155 167 return self._get_template_context(c)
156 168
157 169 @LoginRequired()
158 170 @HasRepoPermissionAnyDecorator(
159 171 'repository.read', 'repository.write', 'repository.admin')
160 172 @view_config(
161 173 route_name='pullrequest_show_all_data', request_method='GET',
162 174 renderer='json_ext', xhr=True)
163 175 def pull_request_list_data(self):
164 176
165 177 # additional filters
166 178 req_get = self.request.GET
167 179 source = str2bool(req_get.get('source'))
168 180 closed = str2bool(req_get.get('closed'))
169 181 my = str2bool(req_get.get('my'))
170 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
171 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
172 184
173 185 filter_type = 'awaiting_review' if awaiting_review \
174 186 else 'awaiting_my_review' if awaiting_my_review \
175 187 else None
176 188
177 189 opened_by = None
178 190 if my:
179 191 opened_by = [self._rhodecode_user.user_id]
180 192
181 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
182 194 if closed:
183 195 statuses = [PullRequest.STATUS_CLOSED]
184 196
185 197 data = self._get_pull_requests_list(
186 198 repo_name=self.db_repo_name, source=source,
187 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
188 200
189 201 return data
190 202
191 203 def _get_pr_version(self, pull_request_id, version=None):
192 pull_request_id = safe_int(pull_request_id)
193 204 at_version = None
194 205
195 206 if version and version == 'latest':
196 207 pull_request_ver = PullRequest.get(pull_request_id)
197 208 pull_request_obj = pull_request_ver
198 209 _org_pull_request_obj = pull_request_obj
199 210 at_version = 'latest'
200 211 elif version:
201 212 pull_request_ver = PullRequestVersion.get_or_404(version)
202 213 pull_request_obj = pull_request_ver
203 214 _org_pull_request_obj = pull_request_ver.pull_request
204 215 at_version = pull_request_ver.pull_request_version_id
205 216 else:
206 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
207 218 pull_request_id)
208 219
209 220 pull_request_display_obj = PullRequest.get_pr_display_object(
210 221 pull_request_obj, _org_pull_request_obj)
211 222
212 223 return _org_pull_request_obj, pull_request_obj, \
213 224 pull_request_display_obj, at_version
214 225
215 226 def _get_diffset(self, source_repo_name, source_repo,
216 227 source_ref_id, target_ref_id,
217 228 target_commit, source_commit, diff_limit, fulldiff,
218 229 file_limit, display_inline_comments):
219 230
220 231 vcs_diff = PullRequestModel().get_diff(
221 232 source_repo, source_ref_id, target_ref_id)
222 233
223 234 diff_processor = diffs.DiffProcessor(
224 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 236 file_limit=file_limit, show_full_diff=fulldiff)
226 237
227 238 _parsed = diff_processor.prepare()
228 239
229 240 def _node_getter(commit):
230 241 def get_node(fname):
231 242 try:
232 243 return commit.get_node(fname)
233 244 except NodeDoesNotExistError:
234 245 return None
235 246
236 247 return get_node
237 248
238 249 diffset = codeblocks.DiffSet(
239 250 repo_name=self.db_repo_name,
240 251 source_repo_name=source_repo_name,
241 252 source_node_getter=_node_getter(target_commit),
242 253 target_node_getter=_node_getter(source_commit),
243 254 comments=display_inline_comments
244 255 )
245 256 diffset = diffset.render_patchset(
246 257 _parsed, target_commit.raw_id, source_commit.raw_id)
247 258
248 259 return diffset
249 260
250 261 @LoginRequired()
251 262 @HasRepoPermissionAnyDecorator(
252 263 'repository.read', 'repository.write', 'repository.admin')
253 # @view_config(
254 # route_name='pullrequest_show', request_method='GET',
255 # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
256 267 def pull_request_show(self):
257 pull_request_id = safe_int(
258 self.request.matchdict.get('pull_request_id'))
268 pull_request_id = self.request.matchdict.get('pull_request_id')
269
259 270 c = self.load_default_context()
260 271
261 272 version = self.request.GET.get('version')
262 273 from_version = self.request.GET.get('from_version') or version
263 274 merge_checks = self.request.GET.get('merge_checks')
264 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
265 276
266 277 (pull_request_latest,
267 278 pull_request_at_ver,
268 279 pull_request_display_obj,
269 280 at_version) = self._get_pr_version(
270 281 pull_request_id, version=version)
271 282 pr_closed = pull_request_latest.is_closed()
272 283
273 284 if pr_closed and (version or from_version):
274 285 # not allow to browse versions
275 286 raise HTTPFound(h.route_path(
276 287 'pullrequest_show', repo_name=self.db_repo_name,
277 288 pull_request_id=pull_request_id))
278 289
279 290 versions = pull_request_display_obj.versions()
280 291
281 292 c.at_version = at_version
282 293 c.at_version_num = (at_version
283 294 if at_version and at_version != 'latest'
284 295 else None)
285 296 c.at_version_pos = ChangesetComment.get_index_from_version(
286 297 c.at_version_num, versions)
287 298
288 299 (prev_pull_request_latest,
289 300 prev_pull_request_at_ver,
290 301 prev_pull_request_display_obj,
291 302 prev_at_version) = self._get_pr_version(
292 303 pull_request_id, version=from_version)
293 304
294 305 c.from_version = prev_at_version
295 306 c.from_version_num = (prev_at_version
296 307 if prev_at_version and prev_at_version != 'latest'
297 308 else None)
298 309 c.from_version_pos = ChangesetComment.get_index_from_version(
299 310 c.from_version_num, versions)
300 311
301 312 # define if we're in COMPARE mode or VIEW at version mode
302 313 compare = at_version != prev_at_version
303 314
304 315 # pull_requests repo_name we opened it against
305 316 # ie. target_repo must match
306 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
307 318 raise HTTPNotFound()
308 319
309 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
310 321 pull_request_at_ver)
311 322
312 323 c.pull_request = pull_request_display_obj
313 324 c.pull_request_latest = pull_request_latest
314 325
315 326 if compare or (at_version and not at_version == 'latest'):
316 327 c.allowed_to_change_status = False
317 328 c.allowed_to_update = False
318 329 c.allowed_to_merge = False
319 330 c.allowed_to_delete = False
320 331 c.allowed_to_comment = False
321 332 c.allowed_to_close = False
322 333 else:
323 334 can_change_status = PullRequestModel().check_user_change_status(
324 335 pull_request_at_ver, self._rhodecode_user)
325 336 c.allowed_to_change_status = can_change_status and not pr_closed
326 337
327 338 c.allowed_to_update = PullRequestModel().check_user_update(
328 339 pull_request_latest, self._rhodecode_user) and not pr_closed
329 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
330 341 pull_request_latest, self._rhodecode_user) and not pr_closed
331 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
332 343 pull_request_latest, self._rhodecode_user) and not pr_closed
333 344 c.allowed_to_comment = not pr_closed
334 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
335 346
336 347 c.forbid_adding_reviewers = False
337 348 c.forbid_author_to_review = False
338 349 c.forbid_commit_author_to_review = False
339 350
340 351 if pull_request_latest.reviewer_data and \
341 352 'rules' in pull_request_latest.reviewer_data:
342 353 rules = pull_request_latest.reviewer_data['rules'] or {}
343 354 try:
344 355 c.forbid_adding_reviewers = rules.get(
345 356 'forbid_adding_reviewers')
346 357 c.forbid_author_to_review = rules.get(
347 358 'forbid_author_to_review')
348 359 c.forbid_commit_author_to_review = rules.get(
349 360 'forbid_commit_author_to_review')
350 361 except Exception:
351 362 pass
352 363
353 364 # check merge capabilities
354 365 _merge_check = MergeCheck.validate(
355 366 pull_request_latest, user=self._rhodecode_user)
356 367 c.pr_merge_errors = _merge_check.error_details
357 368 c.pr_merge_possible = not _merge_check.failed
358 369 c.pr_merge_message = _merge_check.merge_msg
359 370
360 371 c.pull_request_review_status = _merge_check.review_status
361 372 if merge_checks:
362 373 self.request.override_renderer = \
363 374 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
364 375 return self._get_template_context(c)
365 376
366 377 comments_model = CommentsModel()
367 378
368 379 # reviewers and statuses
369 380 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
370 381 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
371 382
372 383 # GENERAL COMMENTS with versions #
373 384 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
374 385 q = q.order_by(ChangesetComment.comment_id.asc())
375 386 general_comments = q
376 387
377 388 # pick comments we want to render at current version
378 389 c.comment_versions = comments_model.aggregate_comments(
379 390 general_comments, versions, c.at_version_num)
380 391 c.comments = c.comment_versions[c.at_version_num]['until']
381 392
382 393 # INLINE COMMENTS with versions #
383 394 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
384 395 q = q.order_by(ChangesetComment.comment_id.asc())
385 396 inline_comments = q
386 397
387 398 c.inline_versions = comments_model.aggregate_comments(
388 399 inline_comments, versions, c.at_version_num, inline=True)
389 400
390 401 # inject latest version
391 402 latest_ver = PullRequest.get_pr_display_object(
392 403 pull_request_latest, pull_request_latest)
393 404
394 405 c.versions = versions + [latest_ver]
395 406
396 407 # if we use version, then do not show later comments
397 408 # than current version
398 409 display_inline_comments = collections.defaultdict(
399 410 lambda: collections.defaultdict(list))
400 411 for co in inline_comments:
401 412 if c.at_version_num:
402 413 # pick comments that are at least UPTO given version, so we
403 414 # don't render comments for higher version
404 415 should_render = co.pull_request_version_id and \
405 416 co.pull_request_version_id <= c.at_version_num
406 417 else:
407 418 # showing all, for 'latest'
408 419 should_render = True
409 420
410 421 if should_render:
411 422 display_inline_comments[co.f_path][co.line_no].append(co)
412 423
413 424 # load diff data into template context, if we use compare mode then
414 425 # diff is calculated based on changes between versions of PR
415 426
416 427 source_repo = pull_request_at_ver.source_repo
417 428 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
418 429
419 430 target_repo = pull_request_at_ver.target_repo
420 431 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
421 432
422 433 if compare:
423 434 # in compare switch the diff base to latest commit from prev version
424 435 target_ref_id = prev_pull_request_display_obj.revisions[0]
425 436
426 437 # despite opening commits for bookmarks/branches/tags, we always
427 438 # convert this to rev to prevent changes after bookmark or branch change
428 439 c.source_ref_type = 'rev'
429 440 c.source_ref = source_ref_id
430 441
431 442 c.target_ref_type = 'rev'
432 443 c.target_ref = target_ref_id
433 444
434 445 c.source_repo = source_repo
435 446 c.target_repo = target_repo
436 447
437 448 c.commit_ranges = []
438 449 source_commit = EmptyCommit()
439 450 target_commit = EmptyCommit()
440 451 c.missing_requirements = False
441 452
442 453 source_scm = source_repo.scm_instance()
443 454 target_scm = target_repo.scm_instance()
444 455
445 456 # try first shadow repo, fallback to regular repo
446 457 try:
447 458 commits_source_repo = pull_request_latest.get_shadow_repo()
448 459 except Exception:
449 460 log.debug('Failed to get shadow repo', exc_info=True)
450 461 commits_source_repo = source_scm
451 462
452 463 c.commits_source_repo = commits_source_repo
453 464 commit_cache = {}
454 465 try:
455 466 pre_load = ["author", "branch", "date", "message"]
456 467 show_revs = pull_request_at_ver.revisions
457 468 for rev in show_revs:
458 469 comm = commits_source_repo.get_commit(
459 470 commit_id=rev, pre_load=pre_load)
460 471 c.commit_ranges.append(comm)
461 472 commit_cache[comm.raw_id] = comm
462 473
463 474 # Order here matters, we first need to get target, and then
464 475 # the source
465 476 target_commit = commits_source_repo.get_commit(
466 477 commit_id=safe_str(target_ref_id))
467 478
468 479 source_commit = commits_source_repo.get_commit(
469 480 commit_id=safe_str(source_ref_id))
470 481
471 482 except CommitDoesNotExistError:
472 483 log.warning(
473 484 'Failed to get commit from `{}` repo'.format(
474 485 commits_source_repo), exc_info=True)
475 486 except RepositoryRequirementError:
476 487 log.warning(
477 488 'Failed to get all required data from repo', exc_info=True)
478 489 c.missing_requirements = True
479 490
480 491 c.ancestor = None # set it to None, to hide it from PR view
481 492
482 493 try:
483 494 ancestor_id = source_scm.get_common_ancestor(
484 495 source_commit.raw_id, target_commit.raw_id, target_scm)
485 496 c.ancestor_commit = source_scm.get_commit(ancestor_id)
486 497 except Exception:
487 498 c.ancestor_commit = None
488 499
489 500 c.statuses = source_repo.statuses(
490 501 [x.raw_id for x in c.commit_ranges])
491 502
492 503 # auto collapse if we have more than limit
493 504 collapse_limit = diffs.DiffProcessor._collapse_commits_over
494 505 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
495 506 c.compare_mode = compare
496 507
497 508 # diff_limit is the old behavior, will cut off the whole diff
498 509 # if the limit is applied otherwise will just hide the
499 510 # big files from the front-end
500 511 diff_limit = c.visual.cut_off_limit_diff
501 512 file_limit = c.visual.cut_off_limit_file
502 513
503 514 c.missing_commits = False
504 515 if (c.missing_requirements
505 516 or isinstance(source_commit, EmptyCommit)
506 517 or source_commit == target_commit):
507 518
508 519 c.missing_commits = True
509 520 else:
510 521
511 522 c.diffset = self._get_diffset(
512 523 c.source_repo.repo_name, commits_source_repo,
513 524 source_ref_id, target_ref_id,
514 525 target_commit, source_commit,
515 526 diff_limit, c.fulldiff, file_limit, display_inline_comments)
516 527
517 528 c.limited_diff = c.diffset.limited_diff
518 529
519 530 # calculate removed files that are bound to comments
520 531 comment_deleted_files = [
521 532 fname for fname in display_inline_comments
522 533 if fname not in c.diffset.file_stats]
523 534
524 535 c.deleted_files_comments = collections.defaultdict(dict)
525 536 for fname, per_line_comments in display_inline_comments.items():
526 537 if fname in comment_deleted_files:
527 538 c.deleted_files_comments[fname]['stats'] = 0
528 539 c.deleted_files_comments[fname]['comments'] = list()
529 540 for lno, comments in per_line_comments.items():
530 541 c.deleted_files_comments[fname]['comments'].extend(
531 542 comments)
532 543
533 544 # this is a hack to properly display links, when creating PR, the
534 545 # compare view and others uses different notation, and
535 546 # compare_commits.mako renders links based on the target_repo.
536 547 # We need to swap that here to generate it properly on the html side
537 548 c.target_repo = c.source_repo
538 549
539 550 c.commit_statuses = ChangesetStatus.STATUSES
540 551
541 552 c.show_version_changes = not pr_closed
542 553 if c.show_version_changes:
543 554 cur_obj = pull_request_at_ver
544 555 prev_obj = prev_pull_request_at_ver
545 556
546 557 old_commit_ids = prev_obj.revisions
547 558 new_commit_ids = cur_obj.revisions
548 559 commit_changes = PullRequestModel()._calculate_commit_id_changes(
549 560 old_commit_ids, new_commit_ids)
550 561 c.commit_changes_summary = commit_changes
551 562
552 563 # calculate the diff for commits between versions
553 564 c.commit_changes = []
554 565 mark = lambda cs, fw: list(
555 566 h.itertools.izip_longest([], cs, fillvalue=fw))
556 567 for c_type, raw_id in mark(commit_changes.added, 'a') \
557 568 + mark(commit_changes.removed, 'r') \
558 569 + mark(commit_changes.common, 'c'):
559 570
560 571 if raw_id in commit_cache:
561 572 commit = commit_cache[raw_id]
562 573 else:
563 574 try:
564 575 commit = commits_source_repo.get_commit(raw_id)
565 576 except CommitDoesNotExistError:
566 577 # in case we fail extracting still use "dummy" commit
567 578 # for display in commit diff
568 579 commit = h.AttributeDict(
569 580 {'raw_id': raw_id,
570 581 'message': 'EMPTY or MISSING COMMIT'})
571 582 c.commit_changes.append([c_type, commit])
572 583
573 584 # current user review statuses for each version
574 585 c.review_versions = {}
575 586 if self._rhodecode_user.user_id in allowed_reviewers:
576 587 for co in general_comments:
577 588 if co.author.user_id == self._rhodecode_user.user_id:
578 589 # each comment has a status change
579 590 status = co.status_change
580 591 if status:
581 592 _ver_pr = status[0].comment.pull_request_version_id
582 593 c.review_versions[_ver_pr] = status[0]
583 594
584 595 return self._get_template_context(c)
596
597 def assure_not_empty_repo(self):
598 _ = self.request.translate
599
600 try:
601 self.db_repo.scm_instance().get_commit()
602 except EmptyRepositoryError:
603 h.flash(h.literal(_('There are no commits yet')),
604 category='warning')
605 raise HTTPFound(
606 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607
608 @LoginRequired()
609 @NotAnonymous()
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
613 route_name='pullrequest_new', request_method='GET',
614 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 def pull_request_new(self):
616 _ = self.request.translate
617 c = self.load_default_context()
618
619 self.assure_not_empty_repo()
620 source_repo = self.db_repo
621
622 commit_id = self.request.GET.get('commit')
623 branch_ref = self.request.GET.get('branch')
624 bookmark_ref = self.request.GET.get('bookmark')
625
626 try:
627 source_repo_data = PullRequestModel().generate_repo_data(
628 source_repo, commit_id=commit_id,
629 branch=branch_ref, bookmark=bookmark_ref)
630 except CommitDoesNotExistError as e:
631 log.exception(e)
632 h.flash(_('Commit does not exist'), 'error')
633 raise HTTPFound(
634 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635
636 default_target_repo = source_repo
637
638 if source_repo.parent:
639 parent_vcs_obj = source_repo.parent.scm_instance()
640 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 # change default if we have a parent repo
642 default_target_repo = source_repo.parent
643
644 target_repo_data = PullRequestModel().generate_repo_data(
645 default_target_repo)
646
647 selected_source_ref = source_repo_data['refs']['selected_ref']
648
649 title_source_ref = selected_source_ref.split(':', 2)[1]
650 c.default_title = PullRequestModel().generate_pullrequest_title(
651 source=source_repo.repo_name,
652 source_ref=title_source_ref,
653 target=default_target_repo.repo_name
654 )
655
656 c.default_repo_data = {
657 'source_repo_name': source_repo.repo_name,
658 'source_refs_json': json.dumps(source_repo_data),
659 'target_repo_name': default_target_repo.repo_name,
660 'target_refs_json': json.dumps(target_repo_data),
661 }
662 c.default_source_ref = selected_source_ref
663
664 return self._get_template_context(c)
665
666 @LoginRequired()
667 @NotAnonymous()
668 @HasRepoPermissionAnyDecorator(
669 'repository.read', 'repository.write', 'repository.admin')
670 @view_config(
671 route_name='pullrequest_repo_refs', request_method='GET',
672 renderer='json_ext', xhr=True)
673 def pull_request_repo_refs(self):
674 target_repo_name = self.request.matchdict['target_repo_name']
675 repo = Repository.get_by_repo_name(target_repo_name)
676 if not repo:
677 raise HTTPNotFound()
678 return PullRequestModel().generate_repo_data(repo)
679
680 @LoginRequired()
681 @NotAnonymous()
682 @HasRepoPermissionAnyDecorator(
683 'repository.read', 'repository.write', 'repository.admin')
684 @view_config(
685 route_name='pullrequest_repo_destinations', request_method='GET',
686 renderer='json_ext', xhr=True)
687 def pull_request_repo_destinations(self):
688 _ = self.request.translate
689 filter_query = self.request.GET.get('query')
690
691 query = Repository.query() \
692 .order_by(func.length(Repository.repo_name)) \
693 .filter(
694 or_(Repository.repo_name == self.db_repo.repo_name,
695 Repository.fork_id == self.db_repo.repo_id))
696
697 if filter_query:
698 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 query = query.filter(
700 Repository.repo_name.ilike(ilike_expression))
701
702 add_parent = False
703 if self.db_repo.parent:
704 if filter_query in self.db_repo.parent.repo_name:
705 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 add_parent = True
708
709 limit = 20 - 1 if add_parent else 20
710 all_repos = query.limit(limit).all()
711 if add_parent:
712 all_repos += [self.db_repo.parent]
713
714 repos = []
715 for obj in ScmModel().get_repos(all_repos):
716 repos.append({
717 'id': obj['name'],
718 'text': obj['name'],
719 'type': 'repo',
720 'obj': obj['dbrepo']
721 })
722
723 data = {
724 'more': False,
725 'results': [{
726 'text': _('Repositories'),
727 'children': repos
728 }] if repos else []
729 }
730 return data
731
732 @LoginRequired()
733 @NotAnonymous()
734 @HasRepoPermissionAnyDecorator(
735 'repository.read', 'repository.write', 'repository.admin')
736 @CSRFRequired()
737 @view_config(
738 route_name='pullrequest_create', request_method='POST',
739 renderer=None)
740 def pull_request_create(self):
741 _ = self.request.translate
742 self.assure_not_empty_repo()
743
744 controls = peppercorn.parse(self.request.POST.items())
745
746 try:
747 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 except formencode.Invalid as errors:
749 if errors.error_dict.get('revisions'):
750 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 elif errors.error_dict.get('pullrequest_title'):
752 msg = _('Pull request requires a title with min. 3 chars')
753 else:
754 msg = _('Error creating pull request: {}').format(errors)
755 log.exception(msg)
756 h.flash(msg, 'error')
757
758 # would rather just go back to form ...
759 raise HTTPFound(
760 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761
762 source_repo = _form['source_repo']
763 source_ref = _form['source_ref']
764 target_repo = _form['target_repo']
765 target_ref = _form['target_ref']
766 commit_ids = _form['revisions'][::-1]
767
768 # find the ancestor for this pr
769 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771
772 source_scm = source_db_repo.scm_instance()
773 target_scm = target_db_repo.scm_instance()
774
775 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777
778 ancestor = source_scm.get_common_ancestor(
779 source_commit.raw_id, target_commit.raw_id, target_scm)
780
781 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783
784 pullrequest_title = _form['pullrequest_title']
785 title_source_ref = source_ref.split(':', 2)[1]
786 if not pullrequest_title:
787 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 source=source_repo,
789 source_ref=title_source_ref,
790 target=target_repo
791 )
792
793 description = _form['pullrequest_desc']
794
795 get_default_reviewers_data, validate_default_reviewers = \
796 PullRequestModel().get_reviewer_functions()
797
798 # recalculate reviewers logic, to make sure we can validate this
799 reviewer_rules = get_default_reviewers_data(
800 self._rhodecode_db_user, source_db_repo,
801 source_commit, target_db_repo, target_commit)
802
803 given_reviewers = _form['review_members']
804 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805
806 try:
807 pull_request = PullRequestModel().create(
808 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 target_ref, commit_ids, reviewers, pullrequest_title,
810 description, reviewer_rules
811 )
812 Session().commit()
813 h.flash(_('Successfully opened new pull request'),
814 category='success')
815 except Exception as e:
816 msg = _('Error occurred during creation of this pull request.')
817 log.exception(msg)
818 h.flash(msg, category='error')
819 raise HTTPFound(
820 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
821
822 raise HTTPFound(
823 h.route_path('pullrequest_show', repo_name=target_repo,
824 pull_request_id=pull_request.pull_request_id))
825
826 @LoginRequired()
827 @NotAnonymous()
828 @HasRepoPermissionAnyDecorator(
829 'repository.read', 'repository.write', 'repository.admin')
830 @CSRFRequired()
831 @view_config(
832 route_name='pullrequest_update', request_method='POST',
833 renderer='json_ext')
834 def pull_request_update(self):
835 pull_request_id = self.request.matchdict['pull_request_id']
836 pull_request = PullRequest.get_or_404(pull_request_id)
837
838 # only owner or admin can update it
839 allowed_to_update = PullRequestModel().check_user_update(
840 pull_request, self._rhodecode_user)
841 if allowed_to_update:
842 controls = peppercorn.parse(self.request.POST.items())
843
844 if 'review_members' in controls:
845 self._update_reviewers(
846 pull_request_id, controls['review_members'],
847 pull_request.reviewer_data)
848 elif str2bool(self.request.POST.get('update_commits', 'false')):
849 self._update_commits(pull_request)
850 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
851 self._edit_pull_request(pull_request)
852 else:
853 raise HTTPBadRequest()
854 return True
855 raise HTTPForbidden()
856
857 def _edit_pull_request(self, pull_request):
858 _ = self.request.translate
859 try:
860 PullRequestModel().edit(
861 pull_request, self.request.POST.get('title'),
862 self.request.POST.get('description'), self._rhodecode_user)
863 except ValueError:
864 msg = _(u'Cannot update closed pull requests.')
865 h.flash(msg, category='error')
866 return
867 else:
868 Session().commit()
869
870 msg = _(u'Pull request title & description updated.')
871 h.flash(msg, category='success')
872 return
873
874 def _update_commits(self, pull_request):
875 _ = self.request.translate
876 resp = PullRequestModel().update_commits(pull_request)
877
878 if resp.executed:
879
880 if resp.target_changed and resp.source_changed:
881 changed = 'target and source repositories'
882 elif resp.target_changed and not resp.source_changed:
883 changed = 'target repository'
884 elif not resp.target_changed and resp.source_changed:
885 changed = 'source repository'
886 else:
887 changed = 'nothing'
888
889 msg = _(
890 u'Pull request updated to "{source_commit_id}" with '
891 u'{count_added} added, {count_removed} removed commits. '
892 u'Source of changes: {change_source}')
893 msg = msg.format(
894 source_commit_id=pull_request.source_ref_parts.commit_id,
895 count_added=len(resp.changes.added),
896 count_removed=len(resp.changes.removed),
897 change_source=changed)
898 h.flash(msg, category='success')
899
900 channel = '/repo${}$/pr/{}'.format(
901 pull_request.target_repo.repo_name,
902 pull_request.pull_request_id)
903 message = msg + (
904 ' - <a onclick="window.location.reload()">'
905 '<strong>{}</strong></a>'.format(_('Reload page')))
906 channelstream.post_message(
907 channel, message, self._rhodecode_user.username,
908 registry=self.request.registry)
909 else:
910 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
911 warning_reasons = [
912 UpdateFailureReason.NO_CHANGE,
913 UpdateFailureReason.WRONG_REF_TYPE,
914 ]
915 category = 'warning' if resp.reason in warning_reasons else 'error'
916 h.flash(msg, category=category)
917
918 @LoginRequired()
919 @NotAnonymous()
920 @HasRepoPermissionAnyDecorator(
921 'repository.read', 'repository.write', 'repository.admin')
922 @CSRFRequired()
923 @view_config(
924 route_name='pullrequest_merge', request_method='POST',
925 renderer='json_ext')
926 def pull_request_merge(self):
927 """
928 Merge will perform a server-side merge of the specified
929 pull request, if the pull request is approved and mergeable.
930 After successful merging, the pull request is automatically
931 closed, with a relevant comment.
932 """
933 pull_request_id = self.request.matchdict['pull_request_id']
934 pull_request = PullRequest.get_or_404(pull_request_id)
935
936 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
937 merge_possible = not check.failed
938
939 for err_type, error_msg in check.errors:
940 h.flash(error_msg, category=err_type)
941
942 if merge_possible:
943 log.debug("Pre-conditions checked, trying to merge.")
944 extras = vcs_operation_context(
945 self.request.environ, repo_name=pull_request.target_repo.repo_name,
946 username=self._rhodecode_db_user.username, action='push',
947 scm=pull_request.target_repo.repo_type)
948 self._merge_pull_request(
949 pull_request, self._rhodecode_db_user, extras)
950 else:
951 log.debug("Pre-conditions failed, NOT merging.")
952
953 raise HTTPFound(
954 h.route_path('pullrequest_show',
955 repo_name=pull_request.target_repo.repo_name,
956 pull_request_id=pull_request.pull_request_id))
957
958 def _merge_pull_request(self, pull_request, user, extras):
959 _ = self.request.translate
960 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
961
962 if merge_resp.executed:
963 log.debug("The merge was successful, closing the pull request.")
964 PullRequestModel().close_pull_request(
965 pull_request.pull_request_id, user)
966 Session().commit()
967 msg = _('Pull request was successfully merged and closed.')
968 h.flash(msg, category='success')
969 else:
970 log.debug(
971 "The merge was not successful. Merge response: %s",
972 merge_resp)
973 msg = PullRequestModel().merge_status_message(
974 merge_resp.failure_reason)
975 h.flash(msg, category='error')
976
977 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
978 _ = self.request.translate
979 get_default_reviewers_data, validate_default_reviewers = \
980 PullRequestModel().get_reviewer_functions()
981
982 try:
983 reviewers = validate_default_reviewers(review_members, reviewer_rules)
984 except ValueError as e:
985 log.error('Reviewers Validation: {}'.format(e))
986 h.flash(e, category='error')
987 return
988
989 PullRequestModel().update_reviewers(
990 pull_request_id, reviewers, self._rhodecode_user)
991 h.flash(_('Pull request reviewers updated.'), category='success')
992 Session().commit()
993
994 @LoginRequired()
995 @NotAnonymous()
996 @HasRepoPermissionAnyDecorator(
997 'repository.read', 'repository.write', 'repository.admin')
998 @CSRFRequired()
999 @view_config(
1000 route_name='pullrequest_delete', request_method='POST',
1001 renderer='json_ext')
1002 def pull_request_delete(self):
1003 _ = self.request.translate
1004
1005 pull_request_id = self.request.matchdict['pull_request_id']
1006 pull_request = PullRequest.get_or_404(pull_request_id)
1007
1008 pr_closed = pull_request.is_closed()
1009 allowed_to_delete = PullRequestModel().check_user_delete(
1010 pull_request, self._rhodecode_user) and not pr_closed
1011
1012 # only owner can delete it !
1013 if allowed_to_delete:
1014 PullRequestModel().delete(pull_request, self._rhodecode_user)
1015 Session().commit()
1016 h.flash(_('Successfully deleted pull request'),
1017 category='success')
1018 raise HTTPFound(h.route_path('my_account_pullrequests'))
1019
1020 log.warning('user %s tried to delete pull request without access',
1021 self._rhodecode_user)
1022 raise HTTPNotFound()
1023
1024 @LoginRequired()
1025 @NotAnonymous()
1026 @HasRepoPermissionAnyDecorator(
1027 'repository.read', 'repository.write', 'repository.admin')
1028 @CSRFRequired()
1029 @view_config(
1030 route_name='pullrequest_comment_create', request_method='POST',
1031 renderer='json_ext')
1032 def pull_request_comment_create(self):
1033 _ = self.request.translate
1034 pull_request_id = self.request.matchdict['pull_request_id']
1035 pull_request = PullRequest.get_or_404(pull_request_id)
1036 if pull_request.is_closed():
1037 log.debug('comment: forbidden because pull request is closed')
1038 raise HTTPForbidden()
1039
1040 c = self.load_default_context()
1041
1042 status = self.request.POST.get('changeset_status', None)
1043 text = self.request.POST.get('text')
1044 comment_type = self.request.POST.get('comment_type')
1045 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1046 close_pull_request = self.request.POST.get('close_pull_request')
1047
1048 # the logic here should work like following, if we submit close
1049 # pr comment, use `close_pull_request_with_comment` function
1050 # else handle regular comment logic
1051
1052 if close_pull_request:
1053 # only owner or admin or person with write permissions
1054 allowed_to_close = PullRequestModel().check_user_update(
1055 pull_request, self._rhodecode_user)
1056 if not allowed_to_close:
1057 log.debug('comment: forbidden because not allowed to close '
1058 'pull request %s', pull_request_id)
1059 raise HTTPForbidden()
1060 comment, status = PullRequestModel().close_pull_request_with_comment(
1061 pull_request, self._rhodecode_user, self.db_repo, message=text)
1062 Session().flush()
1063 events.trigger(
1064 events.PullRequestCommentEvent(pull_request, comment))
1065
1066 else:
1067 # regular comment case, could be inline, or one with status.
1068 # for that one we check also permissions
1069
1070 allowed_to_change_status = PullRequestModel().check_user_change_status(
1071 pull_request, self._rhodecode_user)
1072
1073 if status and allowed_to_change_status:
1074 message = (_('Status change %(transition_icon)s %(status)s')
1075 % {'transition_icon': '>',
1076 'status': ChangesetStatus.get_status_lbl(status)})
1077 text = text or message
1078
1079 comment = CommentsModel().create(
1080 text=text,
1081 repo=self.db_repo.repo_id,
1082 user=self._rhodecode_user.user_id,
1083 pull_request=pull_request_id,
1084 f_path=self.request.POST.get('f_path'),
1085 line_no=self.request.POST.get('line'),
1086 status_change=(ChangesetStatus.get_status_lbl(status)
1087 if status and allowed_to_change_status else None),
1088 status_change_type=(status
1089 if status and allowed_to_change_status else None),
1090 comment_type=comment_type,
1091 resolves_comment_id=resolves_comment_id
1092 )
1093
1094 if allowed_to_change_status:
1095 # calculate old status before we change it
1096 old_calculated_status = pull_request.calculated_review_status()
1097
1098 # get status if set !
1099 if status:
1100 ChangesetStatusModel().set_status(
1101 self.db_repo.repo_id,
1102 status,
1103 self._rhodecode_user.user_id,
1104 comment,
1105 pull_request=pull_request_id
1106 )
1107
1108 Session().flush()
1109 events.trigger(
1110 events.PullRequestCommentEvent(pull_request, comment))
1111
1112 # we now calculate the status of pull request, and based on that
1113 # calculation we set the commits status
1114 calculated_status = pull_request.calculated_review_status()
1115 if old_calculated_status != calculated_status:
1116 PullRequestModel()._trigger_pull_request_hook(
1117 pull_request, self._rhodecode_user, 'review_status_change')
1118
1119 Session().commit()
1120
1121 data = {
1122 'target_id': h.safeid(h.safe_unicode(
1123 self.request.POST.get('f_path'))),
1124 }
1125 if comment:
1126 c.co = comment
1127 rendered_comment = render(
1128 'rhodecode:templates/changeset/changeset_comment_block.mako',
1129 self._get_template_context(c), self.request)
1130
1131 data.update(comment.get_dict())
1132 data.update({'rendered_text': rendered_comment})
1133
1134 return data
1135
1136 @LoginRequired()
1137 @NotAnonymous()
1138 @HasRepoPermissionAnyDecorator(
1139 'repository.read', 'repository.write', 'repository.admin')
1140 @CSRFRequired()
1141 @view_config(
1142 route_name='pullrequest_comment_delete', request_method='POST',
1143 renderer='json_ext')
1144 def pull_request_comment_delete(self):
1145 commit_id = self.request.matchdict['commit_id']
1146 comment_id = self.request.matchdict['comment_id']
1147 pull_request_id = self.request.matchdict['pull_request_id']
1148
1149 pull_request = PullRequest.get_or_404(pull_request_id)
1150 if pull_request.is_closed():
1151 log.debug('comment: forbidden because pull request is closed')
1152 raise HTTPForbidden()
1153
1154 comment = ChangesetComment.get_or_404(comment_id)
1155 if not comment:
1156 log.debug('Comment with id:%s not found, skipping', comment_id)
1157 # comment already deleted in another call probably
1158 return True
1159
1160 if comment.pull_request.is_closed():
1161 # don't allow deleting comments on closed pull request
1162 raise HTTPForbidden()
1163
1164 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1165 super_admin = h.HasPermissionAny('hg.admin')()
1166 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1167 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1168 comment_repo_admin = is_repo_admin and is_repo_comment
1169
1170 if super_admin or comment_owner or comment_repo_admin:
1171 old_calculated_status = comment.pull_request.calculated_review_status()
1172 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1173 Session().commit()
1174 calculated_status = comment.pull_request.calculated_review_status()
1175 if old_calculated_status != calculated_status:
1176 PullRequestModel()._trigger_pull_request_hook(
1177 comment.pull_request, self._rhodecode_user, 'review_status_change')
1178 return True
1179 else:
1180 log.warning('No permissions for user %s to delete comment_id: %s',
1181 self._rhodecode_db_user, comment_id)
1182 raise HTTPNotFound()
@@ -1,588 +1,521 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 class JSRoutesMapper(Mapper):
55 55 """
56 56 Wrapper for routes.Mapper to make pyroutes compatible url definitions
57 57 """
58 58 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
59 59 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
60 60 def __init__(self, *args, **kw):
61 61 super(JSRoutesMapper, self).__init__(*args, **kw)
62 62 self._jsroutes = []
63 63
64 64 def connect(self, *args, **kw):
65 65 """
66 66 Wrapper for connect to take an extra argument jsroute=True
67 67
68 68 :param jsroute: boolean, if True will add the route to the pyroutes list
69 69 """
70 70 if kw.pop('jsroute', False):
71 71 if not self._named_route_regex.match(args[0]):
72 72 raise Exception('only named routes can be added to pyroutes')
73 73 self._jsroutes.append(args[0])
74 74
75 75 super(JSRoutesMapper, self).connect(*args, **kw)
76 76
77 77 def _extract_route_information(self, route):
78 78 """
79 79 Convert a route into tuple(name, path, args), eg:
80 80 ('show_user', '/profile/%(username)s', ['username'])
81 81 """
82 82 routepath = route.routepath
83 83 def replace(matchobj):
84 84 if matchobj.group(1):
85 85 return "%%(%s)s" % matchobj.group(1).split(':')[0]
86 86 else:
87 87 return "%%(%s)s" % matchobj.group(2)
88 88
89 89 routepath = self._argument_prog.sub(replace, routepath)
90 90 return (
91 91 route.name,
92 92 routepath,
93 93 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
94 94 for arg in self._argument_prog.findall(route.routepath)]
95 95 )
96 96
97 97 def jsroutes(self):
98 98 """
99 99 Return a list of pyroutes.js compatible routes
100 100 """
101 101 for route_name in self._jsroutes:
102 102 yield self._extract_route_information(self._routenames[route_name])
103 103
104 104
105 105 def make_map(config):
106 106 """Create, configure and return the routes Mapper"""
107 107 rmap = JSRoutesMapper(
108 108 directory=config['pylons.paths']['controllers'],
109 109 always_scan=config['debug'])
110 110 rmap.minimization = False
111 111 rmap.explicit = False
112 112
113 113 from rhodecode.lib.utils2 import str2bool
114 114 from rhodecode.model import repo, repo_group
115 115
116 116 def check_repo(environ, match_dict):
117 117 """
118 118 check for valid repository for proper 404 handling
119 119
120 120 :param environ:
121 121 :param match_dict:
122 122 """
123 123 repo_name = match_dict.get('repo_name')
124 124
125 125 if match_dict.get('f_path'):
126 126 # fix for multiple initial slashes that causes errors
127 127 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
128 128 repo_model = repo.RepoModel()
129 129 by_name_match = repo_model.get_by_repo_name(repo_name)
130 130 # if we match quickly from database, short circuit the operation,
131 131 # and validate repo based on the type.
132 132 if by_name_match:
133 133 return True
134 134
135 135 by_id_match = repo_model.get_repo_by_id(repo_name)
136 136 if by_id_match:
137 137 repo_name = by_id_match.repo_name
138 138 match_dict['repo_name'] = repo_name
139 139 return True
140 140
141 141 return False
142 142
143 143 def check_group(environ, match_dict):
144 144 """
145 145 check for valid repository group path for proper 404 handling
146 146
147 147 :param environ:
148 148 :param match_dict:
149 149 """
150 150 repo_group_name = match_dict.get('group_name')
151 151 repo_group_model = repo_group.RepoGroupModel()
152 152 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
153 153 if by_name_match:
154 154 return True
155 155
156 156 return False
157 157
158 158 def check_user_group(environ, match_dict):
159 159 """
160 160 check for valid user group for proper 404 handling
161 161
162 162 :param environ:
163 163 :param match_dict:
164 164 """
165 165 return True
166 166
167 167 def check_int(environ, match_dict):
168 168 return match_dict.get('id').isdigit()
169 169
170 170
171 171 #==========================================================================
172 172 # CUSTOM ROUTES HERE
173 173 #==========================================================================
174 174
175 175 # ping and pylons error test
176 176 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
177 177 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
178 178
179 179 # ADMIN REPOSITORY ROUTES
180 180 with rmap.submapper(path_prefix=ADMIN_PREFIX,
181 181 controller='admin/repos') as m:
182 182 m.connect('repos', '/repos',
183 183 action='create', conditions={'method': ['POST']})
184 184 m.connect('repos', '/repos',
185 185 action='index', conditions={'method': ['GET']})
186 186 m.connect('new_repo', '/create_repository', jsroute=True,
187 187 action='create_repository', conditions={'method': ['GET']})
188 188 m.connect('delete_repo', '/repos/{repo_name}',
189 189 action='delete', conditions={'method': ['DELETE']},
190 190 requirements=URL_NAME_REQUIREMENTS)
191 191 m.connect('repo', '/repos/{repo_name}',
192 192 action='show', conditions={'method': ['GET'],
193 193 'function': check_repo},
194 194 requirements=URL_NAME_REQUIREMENTS)
195 195
196 196 # ADMIN REPOSITORY GROUPS ROUTES
197 197 with rmap.submapper(path_prefix=ADMIN_PREFIX,
198 198 controller='admin/repo_groups') as m:
199 199 m.connect('repo_groups', '/repo_groups',
200 200 action='create', conditions={'method': ['POST']})
201 201 m.connect('repo_groups', '/repo_groups',
202 202 action='index', conditions={'method': ['GET']})
203 203 m.connect('new_repo_group', '/repo_groups/new',
204 204 action='new', conditions={'method': ['GET']})
205 205 m.connect('update_repo_group', '/repo_groups/{group_name}',
206 206 action='update', conditions={'method': ['PUT'],
207 207 'function': check_group},
208 208 requirements=URL_NAME_REQUIREMENTS)
209 209
210 210 # EXTRAS REPO GROUP ROUTES
211 211 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
212 212 action='edit',
213 213 conditions={'method': ['GET'], 'function': check_group},
214 214 requirements=URL_NAME_REQUIREMENTS)
215 215 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
216 216 action='edit',
217 217 conditions={'method': ['PUT'], 'function': check_group},
218 218 requirements=URL_NAME_REQUIREMENTS)
219 219
220 220 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
221 221 action='edit_repo_group_advanced',
222 222 conditions={'method': ['GET'], 'function': check_group},
223 223 requirements=URL_NAME_REQUIREMENTS)
224 224 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
225 225 action='edit_repo_group_advanced',
226 226 conditions={'method': ['PUT'], 'function': check_group},
227 227 requirements=URL_NAME_REQUIREMENTS)
228 228
229 229 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
230 230 action='edit_repo_group_perms',
231 231 conditions={'method': ['GET'], 'function': check_group},
232 232 requirements=URL_NAME_REQUIREMENTS)
233 233 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
234 234 action='update_perms',
235 235 conditions={'method': ['PUT'], 'function': check_group},
236 236 requirements=URL_NAME_REQUIREMENTS)
237 237
238 238 m.connect('delete_repo_group', '/repo_groups/{group_name}',
239 239 action='delete', conditions={'method': ['DELETE'],
240 240 'function': check_group},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242
243 243 # ADMIN USER ROUTES
244 244 with rmap.submapper(path_prefix=ADMIN_PREFIX,
245 245 controller='admin/users') as m:
246 246 m.connect('users', '/users',
247 247 action='create', conditions={'method': ['POST']})
248 248 m.connect('new_user', '/users/new',
249 249 action='new', conditions={'method': ['GET']})
250 250 m.connect('update_user', '/users/{user_id}',
251 251 action='update', conditions={'method': ['PUT']})
252 252 m.connect('delete_user', '/users/{user_id}',
253 253 action='delete', conditions={'method': ['DELETE']})
254 254 m.connect('edit_user', '/users/{user_id}/edit',
255 255 action='edit', conditions={'method': ['GET']}, jsroute=True)
256 256 m.connect('user', '/users/{user_id}',
257 257 action='show', conditions={'method': ['GET']})
258 258 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
259 259 action='reset_password', conditions={'method': ['POST']})
260 260 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
261 261 action='create_personal_repo_group', conditions={'method': ['POST']})
262 262
263 263 # EXTRAS USER ROUTES
264 264 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
265 265 action='edit_advanced', conditions={'method': ['GET']})
266 266 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
267 267 action='update_advanced', conditions={'method': ['PUT']})
268 268
269 269 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
270 270 action='edit_global_perms', conditions={'method': ['GET']})
271 271 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
272 272 action='update_global_perms', conditions={'method': ['PUT']})
273 273
274 274 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
275 275 action='edit_perms_summary', conditions={'method': ['GET']})
276 276
277 277 # ADMIN USER GROUPS REST ROUTES
278 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 279 controller='admin/user_groups') as m:
280 280 m.connect('users_groups', '/user_groups',
281 281 action='create', conditions={'method': ['POST']})
282 282 m.connect('users_groups', '/user_groups',
283 283 action='index', conditions={'method': ['GET']})
284 284 m.connect('new_users_group', '/user_groups/new',
285 285 action='new', conditions={'method': ['GET']})
286 286 m.connect('update_users_group', '/user_groups/{user_group_id}',
287 287 action='update', conditions={'method': ['PUT']})
288 288 m.connect('delete_users_group', '/user_groups/{user_group_id}',
289 289 action='delete', conditions={'method': ['DELETE']})
290 290 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
291 291 action='edit', conditions={'method': ['GET']},
292 292 function=check_user_group)
293 293
294 294 # EXTRAS USER GROUP ROUTES
295 295 m.connect('edit_user_group_global_perms',
296 296 '/user_groups/{user_group_id}/edit/global_permissions',
297 297 action='edit_global_perms', conditions={'method': ['GET']})
298 298 m.connect('edit_user_group_global_perms',
299 299 '/user_groups/{user_group_id}/edit/global_permissions',
300 300 action='update_global_perms', conditions={'method': ['PUT']})
301 301 m.connect('edit_user_group_perms_summary',
302 302 '/user_groups/{user_group_id}/edit/permissions_summary',
303 303 action='edit_perms_summary', conditions={'method': ['GET']})
304 304
305 305 m.connect('edit_user_group_perms',
306 306 '/user_groups/{user_group_id}/edit/permissions',
307 307 action='edit_perms', conditions={'method': ['GET']})
308 308 m.connect('edit_user_group_perms',
309 309 '/user_groups/{user_group_id}/edit/permissions',
310 310 action='update_perms', conditions={'method': ['PUT']})
311 311
312 312 m.connect('edit_user_group_advanced',
313 313 '/user_groups/{user_group_id}/edit/advanced',
314 314 action='edit_advanced', conditions={'method': ['GET']})
315 315
316 316 m.connect('edit_user_group_advanced_sync',
317 317 '/user_groups/{user_group_id}/edit/advanced/sync',
318 318 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
319 319
320 320 m.connect('edit_user_group_members',
321 321 '/user_groups/{user_group_id}/edit/members', jsroute=True,
322 322 action='user_group_members', conditions={'method': ['GET']})
323 323
324 324 # ADMIN DEFAULTS REST ROUTES
325 325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
326 326 controller='admin/defaults') as m:
327 327 m.connect('admin_defaults_repositories', '/defaults/repositories',
328 328 action='update_repository_defaults', conditions={'method': ['POST']})
329 329 m.connect('admin_defaults_repositories', '/defaults/repositories',
330 330 action='index', conditions={'method': ['GET']})
331 331
332 332 # ADMIN SETTINGS ROUTES
333 333 with rmap.submapper(path_prefix=ADMIN_PREFIX,
334 334 controller='admin/settings') as m:
335 335
336 336 # default
337 337 m.connect('admin_settings', '/settings',
338 338 action='settings_global_update',
339 339 conditions={'method': ['POST']})
340 340 m.connect('admin_settings', '/settings',
341 341 action='settings_global', conditions={'method': ['GET']})
342 342
343 343 m.connect('admin_settings_vcs', '/settings/vcs',
344 344 action='settings_vcs_update',
345 345 conditions={'method': ['POST']})
346 346 m.connect('admin_settings_vcs', '/settings/vcs',
347 347 action='settings_vcs',
348 348 conditions={'method': ['GET']})
349 349 m.connect('admin_settings_vcs', '/settings/vcs',
350 350 action='delete_svn_pattern',
351 351 conditions={'method': ['DELETE']})
352 352
353 353 m.connect('admin_settings_mapping', '/settings/mapping',
354 354 action='settings_mapping_update',
355 355 conditions={'method': ['POST']})
356 356 m.connect('admin_settings_mapping', '/settings/mapping',
357 357 action='settings_mapping', conditions={'method': ['GET']})
358 358
359 359 m.connect('admin_settings_global', '/settings/global',
360 360 action='settings_global_update',
361 361 conditions={'method': ['POST']})
362 362 m.connect('admin_settings_global', '/settings/global',
363 363 action='settings_global', conditions={'method': ['GET']})
364 364
365 365 m.connect('admin_settings_visual', '/settings/visual',
366 366 action='settings_visual_update',
367 367 conditions={'method': ['POST']})
368 368 m.connect('admin_settings_visual', '/settings/visual',
369 369 action='settings_visual', conditions={'method': ['GET']})
370 370
371 371 m.connect('admin_settings_issuetracker',
372 372 '/settings/issue-tracker', action='settings_issuetracker',
373 373 conditions={'method': ['GET']})
374 374 m.connect('admin_settings_issuetracker_save',
375 375 '/settings/issue-tracker/save',
376 376 action='settings_issuetracker_save',
377 377 conditions={'method': ['POST']})
378 378 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
379 379 action='settings_issuetracker_test',
380 380 conditions={'method': ['POST']})
381 381 m.connect('admin_issuetracker_delete',
382 382 '/settings/issue-tracker/delete',
383 383 action='settings_issuetracker_delete',
384 384 conditions={'method': ['DELETE']})
385 385
386 386 m.connect('admin_settings_email', '/settings/email',
387 387 action='settings_email_update',
388 388 conditions={'method': ['POST']})
389 389 m.connect('admin_settings_email', '/settings/email',
390 390 action='settings_email', conditions={'method': ['GET']})
391 391
392 392 m.connect('admin_settings_hooks', '/settings/hooks',
393 393 action='settings_hooks_update',
394 394 conditions={'method': ['POST', 'DELETE']})
395 395 m.connect('admin_settings_hooks', '/settings/hooks',
396 396 action='settings_hooks', conditions={'method': ['GET']})
397 397
398 398 m.connect('admin_settings_search', '/settings/search',
399 399 action='settings_search', conditions={'method': ['GET']})
400 400
401 401 m.connect('admin_settings_supervisor', '/settings/supervisor',
402 402 action='settings_supervisor', conditions={'method': ['GET']})
403 403 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
404 404 action='settings_supervisor_log', conditions={'method': ['GET']})
405 405
406 406 m.connect('admin_settings_labs', '/settings/labs',
407 407 action='settings_labs_update',
408 408 conditions={'method': ['POST']})
409 409 m.connect('admin_settings_labs', '/settings/labs',
410 410 action='settings_labs', conditions={'method': ['GET']})
411 411
412 412 # ADMIN MY ACCOUNT
413 413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 414 controller='admin/my_account') as m:
415 415
416 416 # NOTE(marcink): this needs to be kept for password force flag to be
417 417 # handled in pylons controllers, remove after full migration to pyramid
418 418 m.connect('my_account_password', '/my_account/password',
419 419 action='my_account_password', conditions={'method': ['GET']})
420 420
421 421 #==========================================================================
422 422 # REPOSITORY ROUTES
423 423 #==========================================================================
424 424
425 425 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
426 426 controller='admin/repos', action='repo_creating',
427 427 requirements=URL_NAME_REQUIREMENTS)
428 428 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
429 429 controller='admin/repos', action='repo_check',
430 430 requirements=URL_NAME_REQUIREMENTS)
431 431
432 432 # repo edit options
433 433 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
434 434 controller='admin/repos', action='edit_fields',
435 435 conditions={'method': ['GET'], 'function': check_repo},
436 436 requirements=URL_NAME_REQUIREMENTS)
437 437 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
438 438 controller='admin/repos', action='create_repo_field',
439 439 conditions={'method': ['PUT'], 'function': check_repo},
440 440 requirements=URL_NAME_REQUIREMENTS)
441 441 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
442 442 controller='admin/repos', action='delete_repo_field',
443 443 conditions={'method': ['DELETE'], 'function': check_repo},
444 444 requirements=URL_NAME_REQUIREMENTS)
445 445
446 446 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
447 447 controller='admin/repos', action='toggle_locking',
448 448 conditions={'method': ['GET'], 'function': check_repo},
449 449 requirements=URL_NAME_REQUIREMENTS)
450 450
451 451 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
452 452 controller='admin/repos', action='edit_remote_form',
453 453 conditions={'method': ['GET'], 'function': check_repo},
454 454 requirements=URL_NAME_REQUIREMENTS)
455 455 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
456 456 controller='admin/repos', action='edit_remote',
457 457 conditions={'method': ['PUT'], 'function': check_repo},
458 458 requirements=URL_NAME_REQUIREMENTS)
459 459
460 460 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
461 461 controller='admin/repos', action='edit_statistics_form',
462 462 conditions={'method': ['GET'], 'function': check_repo},
463 463 requirements=URL_NAME_REQUIREMENTS)
464 464 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
465 465 controller='admin/repos', action='edit_statistics',
466 466 conditions={'method': ['PUT'], 'function': check_repo},
467 467 requirements=URL_NAME_REQUIREMENTS)
468 468 rmap.connect('repo_settings_issuetracker',
469 469 '/{repo_name}/settings/issue-tracker',
470 470 controller='admin/repos', action='repo_issuetracker',
471 471 conditions={'method': ['GET'], 'function': check_repo},
472 472 requirements=URL_NAME_REQUIREMENTS)
473 473 rmap.connect('repo_issuetracker_test',
474 474 '/{repo_name}/settings/issue-tracker/test',
475 475 controller='admin/repos', action='repo_issuetracker_test',
476 476 conditions={'method': ['POST'], 'function': check_repo},
477 477 requirements=URL_NAME_REQUIREMENTS)
478 478 rmap.connect('repo_issuetracker_delete',
479 479 '/{repo_name}/settings/issue-tracker/delete',
480 480 controller='admin/repos', action='repo_issuetracker_delete',
481 481 conditions={'method': ['DELETE'], 'function': check_repo},
482 482 requirements=URL_NAME_REQUIREMENTS)
483 483 rmap.connect('repo_issuetracker_save',
484 484 '/{repo_name}/settings/issue-tracker/save',
485 485 controller='admin/repos', action='repo_issuetracker_save',
486 486 conditions={'method': ['POST'], 'function': check_repo},
487 487 requirements=URL_NAME_REQUIREMENTS)
488 488 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
489 489 controller='admin/repos', action='repo_settings_vcs_update',
490 490 conditions={'method': ['POST'], 'function': check_repo},
491 491 requirements=URL_NAME_REQUIREMENTS)
492 492 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
493 493 controller='admin/repos', action='repo_settings_vcs',
494 494 conditions={'method': ['GET'], 'function': check_repo},
495 495 requirements=URL_NAME_REQUIREMENTS)
496 496 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
497 497 controller='admin/repos', action='repo_delete_svn_pattern',
498 498 conditions={'method': ['DELETE'], 'function': check_repo},
499 499 requirements=URL_NAME_REQUIREMENTS)
500 500 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
501 501 controller='admin/repos', action='repo_settings_pullrequest',
502 502 conditions={'method': ['GET', 'POST'], 'function': check_repo},
503 503 requirements=URL_NAME_REQUIREMENTS)
504 504
505 505
506 rmap.connect('pullrequest_home',
507 '/{repo_name}/pull-request/new', controller='pullrequests',
508 action='index', conditions={'function': check_repo,
509 'method': ['GET']},
510 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
511
512 rmap.connect('pullrequest',
513 '/{repo_name}/pull-request/new', controller='pullrequests',
514 action='create', conditions={'function': check_repo,
515 'method': ['POST']},
516 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
517
518 rmap.connect('pullrequest_repo_refs',
519 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
520 controller='pullrequests',
521 action='get_repo_refs',
522 conditions={'function': check_repo, 'method': ['GET']},
523 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
524
525 rmap.connect('pullrequest_repo_destinations',
526 '/{repo_name}/pull-request/repo-destinations',
527 controller='pullrequests',
528 action='get_repo_destinations',
529 conditions={'function': check_repo, 'method': ['GET']},
530 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
531
532 rmap.connect('pullrequest_show',
533 '/{repo_name}/pull-request/{pull_request_id}',
534 controller='pullrequests',
535 action='show', conditions={'function': check_repo,
536 'method': ['GET']},
537 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
538
539 rmap.connect('pullrequest_update',
540 '/{repo_name}/pull-request/{pull_request_id}',
541 controller='pullrequests',
542 action='update', conditions={'function': check_repo,
543 'method': ['PUT']},
544 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
545
546 rmap.connect('pullrequest_merge',
547 '/{repo_name}/pull-request/{pull_request_id}',
548 controller='pullrequests',
549 action='merge', conditions={'function': check_repo,
550 'method': ['POST']},
551 requirements=URL_NAME_REQUIREMENTS)
552
553 rmap.connect('pullrequest_delete',
554 '/{repo_name}/pull-request/{pull_request_id}',
555 controller='pullrequests',
556 action='delete', conditions={'function': check_repo,
557 'method': ['DELETE']},
558 requirements=URL_NAME_REQUIREMENTS)
559
560 rmap.connect('pullrequest_comment',
561 '/{repo_name}/pull-request-comment/{pull_request_id}',
562 controller='pullrequests',
563 action='comment', conditions={'function': check_repo,
564 'method': ['POST']},
565 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
566
567 rmap.connect('pullrequest_comment_delete',
568 '/{repo_name}/pull-request-comment/{comment_id}/delete',
569 controller='pullrequests', action='delete_comment',
570 conditions={'function': check_repo, 'method': ['DELETE']},
571 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
572
573 506 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
574 507 controller='forks', action='fork_create',
575 508 conditions={'function': check_repo, 'method': ['POST']},
576 509 requirements=URL_NAME_REQUIREMENTS)
577 510
578 511 rmap.connect('repo_fork_home', '/{repo_name}/fork',
579 512 controller='forks', action='fork',
580 513 conditions={'function': check_repo},
581 514 requirements=URL_NAME_REQUIREMENTS)
582 515
583 516 rmap.connect('repo_forks_home', '/{repo_name}/forks',
584 517 controller='forks', action='forks',
585 518 conditions={'function': check_repo},
586 519 requirements=URL_NAME_REQUIREMENTS)
587 520
588 521 return rmap
@@ -1,656 +1,655 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry, get_current_request
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs, channelstream
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.channelstream import channelstream_request
39 39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47 from rhodecode.model.validation_schema.schemas import comment_schema
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class CommentsModel(BaseModel):
54 54
55 55 cls = ChangesetComment
56 56
57 57 DIFF_CONTEXT_BEFORE = 3
58 58 DIFF_CONTEXT_AFTER = 3
59 59
60 60 def __get_commit_comment(self, changeset_comment):
61 61 return self._get_instance(ChangesetComment, changeset_comment)
62 62
63 63 def __get_pull_request(self, pull_request):
64 64 return self._get_instance(PullRequest, pull_request)
65 65
66 66 def _extract_mentions(self, s):
67 67 user_objects = []
68 68 for username in extract_mentioned_users(s):
69 69 user_obj = User.get_by_username(username, case_insensitive=True)
70 70 if user_obj:
71 71 user_objects.append(user_obj)
72 72 return user_objects
73 73
74 74 def _get_renderer(self, global_renderer='rst'):
75 75 try:
76 76 # try reading from visual context
77 77 from pylons import tmpl_context
78 78 global_renderer = tmpl_context.visual.default_renderer
79 79 except AttributeError:
80 80 log.debug("Renderer not set, falling back "
81 81 "to default renderer '%s'", global_renderer)
82 82 except Exception:
83 83 log.error(traceback.format_exc())
84 84 return global_renderer
85 85
86 86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 87 # group by versions, and count until, and display objects
88 88
89 89 comment_groups = collections.defaultdict(list)
90 90 [comment_groups[
91 91 _co.pull_request_version_id].append(_co) for _co in comments]
92 92
93 93 def yield_comments(pos):
94 94 for co in comment_groups[pos]:
95 95 yield co
96 96
97 97 comment_versions = collections.defaultdict(
98 98 lambda: collections.defaultdict(list))
99 99 prev_prvid = -1
100 100 # fake last entry with None, to aggregate on "latest" version which
101 101 # doesn't have an pull_request_version_id
102 102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 103 prvid = ver.pull_request_version_id
104 104 if prev_prvid == -1:
105 105 prev_prvid = prvid
106 106
107 107 for co in yield_comments(prvid):
108 108 comment_versions[prvid]['at'].append(co)
109 109
110 110 # save until
111 111 current = comment_versions[prvid]['at']
112 112 prev_until = comment_versions[prev_prvid]['until']
113 113 cur_until = prev_until + current
114 114 comment_versions[prvid]['until'].extend(cur_until)
115 115
116 116 # save outdated
117 117 if inline:
118 118 outdated = [x for x in cur_until
119 119 if x.outdated_at_version(show_version)]
120 120 else:
121 121 outdated = [x for x in cur_until
122 122 if x.older_than_version(show_version)]
123 123 display = [x for x in cur_until if x not in outdated]
124 124
125 125 comment_versions[prvid]['outdated'] = outdated
126 126 comment_versions[prvid]['display'] = display
127 127
128 128 prev_prvid = prvid
129 129
130 130 return comment_versions
131 131
132 132 def get_unresolved_todos(self, pull_request, show_outdated=True):
133 133
134 134 todos = Session().query(ChangesetComment) \
135 135 .filter(ChangesetComment.pull_request == pull_request) \
136 136 .filter(ChangesetComment.resolved_by == None) \
137 137 .filter(ChangesetComment.comment_type
138 138 == ChangesetComment.COMMENT_TYPE_TODO)
139 139
140 140 if not show_outdated:
141 141 todos = todos.filter(
142 142 coalesce(ChangesetComment.display_state, '') !=
143 143 ChangesetComment.COMMENT_OUTDATED)
144 144
145 145 todos = todos.all()
146 146
147 147 return todos
148 148
149 149 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
150 150
151 151 todos = Session().query(ChangesetComment) \
152 152 .filter(ChangesetComment.revision == commit_id) \
153 153 .filter(ChangesetComment.resolved_by == None) \
154 154 .filter(ChangesetComment.comment_type
155 155 == ChangesetComment.COMMENT_TYPE_TODO)
156 156
157 157 if not show_outdated:
158 158 todos = todos.filter(
159 159 coalesce(ChangesetComment.display_state, '') !=
160 160 ChangesetComment.COMMENT_OUTDATED)
161 161
162 162 todos = todos.all()
163 163
164 164 return todos
165 165
166 166 def _log_audit_action(self, action, action_data, user, comment):
167 167 audit_logger.store(
168 168 action=action,
169 169 action_data=action_data,
170 170 user=user,
171 171 repo=comment.repo)
172 172
173 173 def create(self, text, repo, user, commit_id=None, pull_request=None,
174 174 f_path=None, line_no=None, status_change=None,
175 175 status_change_type=None, comment_type=None,
176 176 resolves_comment_id=None, closing_pr=False, send_email=True,
177 177 renderer=None):
178 178 """
179 179 Creates new comment for commit or pull request.
180 180 IF status_change is not none this comment is associated with a
181 181 status change of commit or commit associated with pull request
182 182
183 183 :param text:
184 184 :param repo:
185 185 :param user:
186 186 :param commit_id:
187 187 :param pull_request:
188 188 :param f_path:
189 189 :param line_no:
190 190 :param status_change: Label for status change
191 191 :param comment_type: Type of comment
192 192 :param status_change_type: type of status change
193 193 :param closing_pr:
194 194 :param send_email:
195 195 :param renderer: pick renderer for this comment
196 196 """
197 197 if not text:
198 198 log.warning('Missing text for comment, skipping...')
199 199 return
200 200
201 201 if not renderer:
202 202 renderer = self._get_renderer()
203 203
204 204 repo = self._get_repo(repo)
205 205 user = self._get_user(user)
206 206
207 207 schema = comment_schema.CommentSchema()
208 208 validated_kwargs = schema.deserialize(dict(
209 209 comment_body=text,
210 210 comment_type=comment_type,
211 211 comment_file=f_path,
212 212 comment_line=line_no,
213 213 renderer_type=renderer,
214 214 status_change=status_change_type,
215 215 resolves_comment_id=resolves_comment_id,
216 216 repo=repo.repo_id,
217 217 user=user.user_id,
218 218 ))
219 219
220 220 comment = ChangesetComment()
221 221 comment.renderer = validated_kwargs['renderer_type']
222 222 comment.text = validated_kwargs['comment_body']
223 223 comment.f_path = validated_kwargs['comment_file']
224 224 comment.line_no = validated_kwargs['comment_line']
225 225 comment.comment_type = validated_kwargs['comment_type']
226 226
227 227 comment.repo = repo
228 228 comment.author = user
229 229 comment.resolved_comment = self.__get_commit_comment(
230 230 validated_kwargs['resolves_comment_id'])
231 231
232 232 pull_request_id = pull_request
233 233
234 234 commit_obj = None
235 235 pull_request_obj = None
236 236
237 237 if commit_id:
238 238 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
239 239 # do a lookup, so we don't pass something bad here
240 240 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
241 241 comment.revision = commit_obj.raw_id
242 242
243 243 elif pull_request_id:
244 244 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
245 245 pull_request_obj = self.__get_pull_request(pull_request_id)
246 246 comment.pull_request = pull_request_obj
247 247 else:
248 248 raise Exception('Please specify commit or pull_request_id')
249 249
250 250 Session().add(comment)
251 251 Session().flush()
252 252 kwargs = {
253 253 'user': user,
254 254 'renderer_type': renderer,
255 255 'repo_name': repo.repo_name,
256 256 'status_change': status_change,
257 257 'status_change_type': status_change_type,
258 258 'comment_body': text,
259 259 'comment_file': f_path,
260 260 'comment_line': line_no,
261 261 'comment_type': comment_type or 'note'
262 262 }
263 263
264 264 if commit_obj:
265 265 recipients = ChangesetComment.get_users(
266 266 revision=commit_obj.raw_id)
267 267 # add commit author if it's in RhodeCode system
268 268 cs_author = User.get_from_cs_author(commit_obj.author)
269 269 if not cs_author:
270 270 # use repo owner if we cannot extract the author correctly
271 271 cs_author = repo.user
272 272 recipients += [cs_author]
273 273
274 274 commit_comment_url = self.get_url(comment)
275 275
276 276 target_repo_url = h.link_to(
277 277 repo.repo_name,
278 278 h.route_url('repo_summary', repo_name=repo.repo_name))
279 279
280 280 # commit specifics
281 281 kwargs.update({
282 282 'commit': commit_obj,
283 283 'commit_message': commit_obj.message,
284 284 'commit_target_repo': target_repo_url,
285 285 'commit_comment_url': commit_comment_url,
286 286 })
287 287
288 288 elif pull_request_obj:
289 289 # get the current participants of this pull request
290 290 recipients = ChangesetComment.get_users(
291 291 pull_request_id=pull_request_obj.pull_request_id)
292 292 # add pull request author
293 293 recipients += [pull_request_obj.author]
294 294
295 295 # add the reviewers to notification
296 296 recipients += [x.user for x in pull_request_obj.reviewers]
297 297
298 298 pr_target_repo = pull_request_obj.target_repo
299 299 pr_source_repo = pull_request_obj.source_repo
300 300
301 pr_comment_url = h.url(
301 pr_comment_url = h.route_url(
302 302 'pullrequest_show',
303 303 repo_name=pr_target_repo.repo_name,
304 304 pull_request_id=pull_request_obj.pull_request_id,
305 anchor='comment-%s' % comment.comment_id,
306 qualified=True,)
305 anchor='comment-%s' % comment.comment_id)
307 306
308 307 # set some variables for email notification
309 308 pr_target_repo_url = h.route_url(
310 309 'repo_summary', repo_name=pr_target_repo.repo_name)
311 310
312 311 pr_source_repo_url = h.route_url(
313 312 'repo_summary', repo_name=pr_source_repo.repo_name)
314 313
315 314 # pull request specifics
316 315 kwargs.update({
317 316 'pull_request': pull_request_obj,
318 317 'pr_id': pull_request_obj.pull_request_id,
319 318 'pr_target_repo': pr_target_repo,
320 319 'pr_target_repo_url': pr_target_repo_url,
321 320 'pr_source_repo': pr_source_repo,
322 321 'pr_source_repo_url': pr_source_repo_url,
323 322 'pr_comment_url': pr_comment_url,
324 323 'pr_closing': closing_pr,
325 324 })
326 325 if send_email:
327 326 # pre-generate the subject for notification itself
328 327 (subject,
329 328 _h, _e, # we don't care about those
330 329 body_plaintext) = EmailNotificationModel().render_email(
331 330 notification_type, **kwargs)
332 331
333 332 mention_recipients = set(
334 333 self._extract_mentions(text)).difference(recipients)
335 334
336 335 # create notification objects, and emails
337 336 NotificationModel().create(
338 337 created_by=user,
339 338 notification_subject=subject,
340 339 notification_body=body_plaintext,
341 340 notification_type=notification_type,
342 341 recipients=recipients,
343 342 mention_recipients=mention_recipients,
344 343 email_kwargs=kwargs,
345 344 )
346 345
347 346 Session().flush()
348 347 if comment.pull_request:
349 348 action = 'repo.pull_request.comment.create'
350 349 else:
351 350 action = 'repo.commit.comment.create'
352 351
353 352 comment_data = comment.get_api_data()
354 353 self._log_audit_action(
355 354 action, {'data': comment_data}, user, comment)
356 355
357 356 msg_url = ''
358 357 channel = None
359 358 if commit_obj:
360 359 msg_url = commit_comment_url
361 360 repo_name = repo.repo_name
362 361 channel = u'/repo${}$/commit/{}'.format(
363 362 repo_name,
364 363 commit_obj.raw_id
365 364 )
366 365 elif pull_request_obj:
367 366 msg_url = pr_comment_url
368 367 repo_name = pr_target_repo.repo_name
369 368 channel = u'/repo${}$/pr/{}'.format(
370 369 repo_name,
371 370 pull_request_id
372 371 )
373 372
374 373 message = '<strong>{}</strong> {} - ' \
375 374 '<a onclick="window.location=\'{}\';' \
376 375 'window.location.reload()">' \
377 376 '<strong>{}</strong></a>'
378 377 message = message.format(
379 378 user.username, _('made a comment'), msg_url,
380 379 _('Show it now'))
381 380
382 381 channelstream.post_message(
383 382 channel, message, user.username,
384 383 registry=get_current_registry())
385 384
386 385 return comment
387 386
388 387 def delete(self, comment, user):
389 388 """
390 389 Deletes given comment
391 390 """
392 391 comment = self.__get_commit_comment(comment)
393 392 old_data = comment.get_api_data()
394 393 Session().delete(comment)
395 394
396 395 if comment.pull_request:
397 396 action = 'repo.pull_request.comment.delete'
398 397 else:
399 398 action = 'repo.commit.comment.delete'
400 399
401 400 self._log_audit_action(
402 401 action, {'old_data': old_data}, user, comment)
403 402
404 403 return comment
405 404
406 405 def get_all_comments(self, repo_id, revision=None, pull_request=None):
407 406 q = ChangesetComment.query()\
408 407 .filter(ChangesetComment.repo_id == repo_id)
409 408 if revision:
410 409 q = q.filter(ChangesetComment.revision == revision)
411 410 elif pull_request:
412 411 pull_request = self.__get_pull_request(pull_request)
413 412 q = q.filter(ChangesetComment.pull_request == pull_request)
414 413 else:
415 414 raise Exception('Please specify commit or pull_request')
416 415 q = q.order_by(ChangesetComment.created_on)
417 416 return q.all()
418 417
419 418 def get_url(self, comment, request=None, permalink=False):
420 419 if not request:
421 420 request = get_current_request()
422 421
423 422 comment = self.__get_commit_comment(comment)
424 423 if comment.pull_request:
425 424 pull_request = comment.pull_request
426 425 if permalink:
427 426 return request.route_url(
428 427 'pull_requests_global',
429 428 pull_request_id=pull_request.pull_request_id,
430 429 _anchor='comment-%s' % comment.comment_id)
431 430 else:
432 431 return request.route_url('pullrequest_show',
433 432 repo_name=safe_str(pull_request.target_repo.repo_name),
434 433 pull_request_id=pull_request.pull_request_id,
435 434 _anchor='comment-%s' % comment.comment_id)
436 435
437 436 else:
438 437 repo = comment.repo
439 438 commit_id = comment.revision
440 439
441 440 if permalink:
442 441 return request.route_url(
443 442 'repo_commit', repo_name=safe_str(repo.repo_id),
444 443 commit_id=commit_id,
445 444 _anchor='comment-%s' % comment.comment_id)
446 445
447 446 else:
448 447 return request.route_url(
449 448 'repo_commit', repo_name=safe_str(repo.repo_name),
450 449 commit_id=commit_id,
451 450 _anchor='comment-%s' % comment.comment_id)
452 451
453 452 def get_comments(self, repo_id, revision=None, pull_request=None):
454 453 """
455 454 Gets main comments based on revision or pull_request_id
456 455
457 456 :param repo_id:
458 457 :param revision:
459 458 :param pull_request:
460 459 """
461 460
462 461 q = ChangesetComment.query()\
463 462 .filter(ChangesetComment.repo_id == repo_id)\
464 463 .filter(ChangesetComment.line_no == None)\
465 464 .filter(ChangesetComment.f_path == None)
466 465 if revision:
467 466 q = q.filter(ChangesetComment.revision == revision)
468 467 elif pull_request:
469 468 pull_request = self.__get_pull_request(pull_request)
470 469 q = q.filter(ChangesetComment.pull_request == pull_request)
471 470 else:
472 471 raise Exception('Please specify commit or pull_request')
473 472 q = q.order_by(ChangesetComment.created_on)
474 473 return q.all()
475 474
476 475 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
477 476 q = self._get_inline_comments_query(repo_id, revision, pull_request)
478 477 return self._group_comments_by_path_and_line_number(q)
479 478
480 479 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
481 480 version=None):
482 481 inline_cnt = 0
483 482 for fname, per_line_comments in inline_comments.iteritems():
484 483 for lno, comments in per_line_comments.iteritems():
485 484 for comm in comments:
486 485 if not comm.outdated_at_version(version) and skip_outdated:
487 486 inline_cnt += 1
488 487
489 488 return inline_cnt
490 489
491 490 def get_outdated_comments(self, repo_id, pull_request):
492 491 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
493 492 # of a pull request.
494 493 q = self._all_inline_comments_of_pull_request(pull_request)
495 494 q = q.filter(
496 495 ChangesetComment.display_state ==
497 496 ChangesetComment.COMMENT_OUTDATED
498 497 ).order_by(ChangesetComment.comment_id.asc())
499 498
500 499 return self._group_comments_by_path_and_line_number(q)
501 500
502 501 def _get_inline_comments_query(self, repo_id, revision, pull_request):
503 502 # TODO: johbo: Split this into two methods: One for PR and one for
504 503 # commit.
505 504 if revision:
506 505 q = Session().query(ChangesetComment).filter(
507 506 ChangesetComment.repo_id == repo_id,
508 507 ChangesetComment.line_no != null(),
509 508 ChangesetComment.f_path != null(),
510 509 ChangesetComment.revision == revision)
511 510
512 511 elif pull_request:
513 512 pull_request = self.__get_pull_request(pull_request)
514 513 if not CommentsModel.use_outdated_comments(pull_request):
515 514 q = self._visible_inline_comments_of_pull_request(pull_request)
516 515 else:
517 516 q = self._all_inline_comments_of_pull_request(pull_request)
518 517
519 518 else:
520 519 raise Exception('Please specify commit or pull_request_id')
521 520 q = q.order_by(ChangesetComment.comment_id.asc())
522 521 return q
523 522
524 523 def _group_comments_by_path_and_line_number(self, q):
525 524 comments = q.all()
526 525 paths = collections.defaultdict(lambda: collections.defaultdict(list))
527 526 for co in comments:
528 527 paths[co.f_path][co.line_no].append(co)
529 528 return paths
530 529
531 530 @classmethod
532 531 def needed_extra_diff_context(cls):
533 532 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
534 533
535 534 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
536 535 if not CommentsModel.use_outdated_comments(pull_request):
537 536 return
538 537
539 538 comments = self._visible_inline_comments_of_pull_request(pull_request)
540 539 comments_to_outdate = comments.all()
541 540
542 541 for comment in comments_to_outdate:
543 542 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
544 543
545 544 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
546 545 diff_line = _parse_comment_line_number(comment.line_no)
547 546
548 547 try:
549 548 old_context = old_diff_proc.get_context_of_line(
550 549 path=comment.f_path, diff_line=diff_line)
551 550 new_context = new_diff_proc.get_context_of_line(
552 551 path=comment.f_path, diff_line=diff_line)
553 552 except (diffs.LineNotInDiffException,
554 553 diffs.FileNotInDiffException):
555 554 comment.display_state = ChangesetComment.COMMENT_OUTDATED
556 555 return
557 556
558 557 if old_context == new_context:
559 558 return
560 559
561 560 if self._should_relocate_diff_line(diff_line):
562 561 new_diff_lines = new_diff_proc.find_context(
563 562 path=comment.f_path, context=old_context,
564 563 offset=self.DIFF_CONTEXT_BEFORE)
565 564 if not new_diff_lines:
566 565 comment.display_state = ChangesetComment.COMMENT_OUTDATED
567 566 else:
568 567 new_diff_line = self._choose_closest_diff_line(
569 568 diff_line, new_diff_lines)
570 569 comment.line_no = _diff_to_comment_line_number(new_diff_line)
571 570 else:
572 571 comment.display_state = ChangesetComment.COMMENT_OUTDATED
573 572
574 573 def _should_relocate_diff_line(self, diff_line):
575 574 """
576 575 Checks if relocation shall be tried for the given `diff_line`.
577 576
578 577 If a comment points into the first lines, then we can have a situation
579 578 that after an update another line has been added on top. In this case
580 579 we would find the context still and move the comment around. This
581 580 would be wrong.
582 581 """
583 582 should_relocate = (
584 583 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
585 584 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
586 585 return should_relocate
587 586
588 587 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
589 588 candidate = new_diff_lines[0]
590 589 best_delta = _diff_line_delta(diff_line, candidate)
591 590 for new_diff_line in new_diff_lines[1:]:
592 591 delta = _diff_line_delta(diff_line, new_diff_line)
593 592 if delta < best_delta:
594 593 candidate = new_diff_line
595 594 best_delta = delta
596 595 return candidate
597 596
598 597 def _visible_inline_comments_of_pull_request(self, pull_request):
599 598 comments = self._all_inline_comments_of_pull_request(pull_request)
600 599 comments = comments.filter(
601 600 coalesce(ChangesetComment.display_state, '') !=
602 601 ChangesetComment.COMMENT_OUTDATED)
603 602 return comments
604 603
605 604 def _all_inline_comments_of_pull_request(self, pull_request):
606 605 comments = Session().query(ChangesetComment)\
607 606 .filter(ChangesetComment.line_no != None)\
608 607 .filter(ChangesetComment.f_path != None)\
609 608 .filter(ChangesetComment.pull_request == pull_request)
610 609 return comments
611 610
612 611 def _all_general_comments_of_pull_request(self, pull_request):
613 612 comments = Session().query(ChangesetComment)\
614 613 .filter(ChangesetComment.line_no == None)\
615 614 .filter(ChangesetComment.f_path == None)\
616 615 .filter(ChangesetComment.pull_request == pull_request)
617 616 return comments
618 617
619 618 @staticmethod
620 619 def use_outdated_comments(pull_request):
621 620 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
622 621 settings = settings_model.get_general_settings()
623 622 return settings.get('rhodecode_use_outdated_comments', False)
624 623
625 624
626 625 def _parse_comment_line_number(line_no):
627 626 """
628 627 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
629 628 """
630 629 old_line = None
631 630 new_line = None
632 631 if line_no.startswith('o'):
633 632 old_line = int(line_no[1:])
634 633 elif line_no.startswith('n'):
635 634 new_line = int(line_no[1:])
636 635 else:
637 636 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
638 637 return diffs.DiffLineNumber(old_line, new_line)
639 638
640 639
641 640 def _diff_to_comment_line_number(diff_line):
642 641 if diff_line.new is not None:
643 642 return u'n{}'.format(diff_line.new)
644 643 elif diff_line.old is not None:
645 644 return u'o{}'.format(diff_line.old)
646 645 return u''
647 646
648 647
649 648 def _diff_line_delta(a, b):
650 649 if None not in (a.new, b.new):
651 650 return abs(a.new - b.new)
652 651 elif None not in (a.old, b.old):
653 652 return abs(a.old - b.old)
654 653 else:
655 654 raise ValueError(
656 655 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1551 +1,1552 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 34 from pyramid.threadlocal import get_current_request
35 35 from sqlalchemy import or_
36 36
37 37 from rhodecode import events
38 38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 39 from rhodecode.lib import audit_logger
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 42 from rhodecode.lib.markup_renderer import (
43 43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 45 from rhodecode.lib.vcs.backends.base import (
46 46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 48 from rhodecode.lib.vcs.exceptions import (
49 49 CommitDoesNotExistError, EmptyRepositoryError)
50 50 from rhodecode.model import BaseModel
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.comment import CommentsModel
53 53 from rhodecode.model.db import (
54 54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 55 PullRequestVersion, ChangesetComment, Repository)
56 56 from rhodecode.model.meta import Session
57 57 from rhodecode.model.notification import NotificationModel, \
58 58 EmailNotificationModel
59 59 from rhodecode.model.scm import ScmModel
60 60 from rhodecode.model.settings import VcsSettingsModel
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # Data structure to hold the response data when updating commits during a pull
67 67 # request update.
68 68 UpdateResponse = namedtuple('UpdateResponse', [
69 69 'executed', 'reason', 'new', 'old', 'changes',
70 70 'source_changed', 'target_changed'])
71 71
72 72
73 73 class PullRequestModel(BaseModel):
74 74
75 75 cls = PullRequest
76 76
77 77 DIFF_CONTEXT = 3
78 78
79 79 MERGE_STATUS_MESSAGES = {
80 80 MergeFailureReason.NONE: lazy_ugettext(
81 81 'This pull request can be automatically merged.'),
82 82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 83 'This pull request cannot be merged because of an unhandled'
84 84 ' exception.'),
85 85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 86 'This pull request cannot be merged because of merge conflicts.'),
87 87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 88 'This pull request could not be merged because push to target'
89 89 ' failed.'),
90 90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 91 'This pull request cannot be merged because the target is not a'
92 92 ' head.'),
93 93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 94 'This pull request cannot be merged because the source contains'
95 95 ' more branches than the target.'),
96 96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 97 'This pull request cannot be merged because the target has'
98 98 ' multiple heads.'),
99 99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 100 'This pull request cannot be merged because the target repository'
101 101 ' is locked.'),
102 102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 103 'This pull request cannot be merged because the target or the '
104 104 'source reference is missing.'),
105 105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 106 'This pull request cannot be merged because the target '
107 107 'reference is missing.'),
108 108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 109 'This pull request cannot be merged because the source '
110 110 'reference is missing.'),
111 111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 112 'This pull request cannot be merged because of conflicts related '
113 113 'to sub repositories.'),
114 114 }
115 115
116 116 UPDATE_STATUS_MESSAGES = {
117 117 UpdateFailureReason.NONE: lazy_ugettext(
118 118 'Pull request update successful.'),
119 119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 120 'Pull request update failed because of an unknown error.'),
121 121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 122 'No update needed because the source and target have not changed.'),
123 123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 124 'Pull request cannot be updated because the reference type is '
125 125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 127 'This pull request cannot be updated because the target '
128 128 'reference is missing.'),
129 129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 130 'This pull request cannot be updated because the source '
131 131 'reference is missing.'),
132 132 }
133 133
134 134 def __get_pull_request(self, pull_request):
135 135 return self._get_instance((
136 136 PullRequest, PullRequestVersion), pull_request)
137 137
138 138 def _check_perms(self, perms, pull_request, user, api=False):
139 139 if not api:
140 140 return h.HasRepoPermissionAny(*perms)(
141 141 user=user, repo_name=pull_request.target_repo.repo_name)
142 142 else:
143 143 return h.HasRepoPermissionAnyApi(*perms)(
144 144 user=user, repo_name=pull_request.target_repo.repo_name)
145 145
146 146 def check_user_read(self, pull_request, user, api=False):
147 147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 148 return self._check_perms(_perms, pull_request, user, api)
149 149
150 150 def check_user_merge(self, pull_request, user, api=False):
151 151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 152 return self._check_perms(_perms, pull_request, user, api)
153 153
154 154 def check_user_update(self, pull_request, user, api=False):
155 155 owner = user.user_id == pull_request.user_id
156 156 return self.check_user_merge(pull_request, user, api) or owner
157 157
158 158 def check_user_delete(self, pull_request, user):
159 159 owner = user.user_id == pull_request.user_id
160 160 _perms = ('repository.admin',)
161 161 return self._check_perms(_perms, pull_request, user) or owner
162 162
163 163 def check_user_change_status(self, pull_request, user, api=False):
164 164 reviewer = user.user_id in [x.user_id for x in
165 165 pull_request.reviewers]
166 166 return self.check_user_update(pull_request, user, api) or reviewer
167 167
168 168 def get(self, pull_request):
169 169 return self.__get_pull_request(pull_request)
170 170
171 171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 172 opened_by=None, order_by=None,
173 173 order_dir='desc'):
174 174 repo = None
175 175 if repo_name:
176 176 repo = self._get_repo(repo_name)
177 177
178 178 q = PullRequest.query()
179 179
180 180 # source or target
181 181 if repo and source:
182 182 q = q.filter(PullRequest.source_repo == repo)
183 183 elif repo:
184 184 q = q.filter(PullRequest.target_repo == repo)
185 185
186 186 # closed,opened
187 187 if statuses:
188 188 q = q.filter(PullRequest.status.in_(statuses))
189 189
190 190 # opened by filter
191 191 if opened_by:
192 192 q = q.filter(PullRequest.user_id.in_(opened_by))
193 193
194 194 if order_by:
195 195 order_map = {
196 196 'name_raw': PullRequest.pull_request_id,
197 197 'title': PullRequest.title,
198 198 'updated_on_raw': PullRequest.updated_on,
199 199 'target_repo': PullRequest.target_repo_id
200 200 }
201 201 if order_dir == 'asc':
202 202 q = q.order_by(order_map[order_by].asc())
203 203 else:
204 204 q = q.order_by(order_map[order_by].desc())
205 205
206 206 return q
207 207
208 208 def count_all(self, repo_name, source=False, statuses=None,
209 209 opened_by=None):
210 210 """
211 211 Count the number of pull requests for a specific repository.
212 212
213 213 :param repo_name: target or source repo
214 214 :param source: boolean flag to specify if repo_name refers to source
215 215 :param statuses: list of pull request statuses
216 216 :param opened_by: author user of the pull request
217 217 :returns: int number of pull requests
218 218 """
219 219 q = self._prepare_get_all_query(
220 220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221 221
222 222 return q.count()
223 223
224 224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 225 offset=0, length=None, order_by=None, order_dir='desc'):
226 226 """
227 227 Get all pull requests for a specific repository.
228 228
229 229 :param repo_name: target or source repo
230 230 :param source: boolean flag to specify if repo_name refers to source
231 231 :param statuses: list of pull request statuses
232 232 :param opened_by: author user of the pull request
233 233 :param offset: pagination offset
234 234 :param length: length of returned list
235 235 :param order_by: order of the returned list
236 236 :param order_dir: 'asc' or 'desc' ordering direction
237 237 :returns: list of pull requests
238 238 """
239 239 q = self._prepare_get_all_query(
240 240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 241 order_by=order_by, order_dir=order_dir)
242 242
243 243 if length:
244 244 pull_requests = q.limit(length).offset(offset).all()
245 245 else:
246 246 pull_requests = q.all()
247 247
248 248 return pull_requests
249 249
250 250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 251 opened_by=None):
252 252 """
253 253 Count the number of pull requests for a specific repository that are
254 254 awaiting review.
255 255
256 256 :param repo_name: target or source repo
257 257 :param source: boolean flag to specify if repo_name refers to source
258 258 :param statuses: list of pull request statuses
259 259 :param opened_by: author user of the pull request
260 260 :returns: int number of pull requests
261 261 """
262 262 pull_requests = self.get_awaiting_review(
263 263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264 264
265 265 return len(pull_requests)
266 266
267 267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 268 opened_by=None, offset=0, length=None,
269 269 order_by=None, order_dir='desc'):
270 270 """
271 271 Get all pull requests for a specific repository that are awaiting
272 272 review.
273 273
274 274 :param repo_name: target or source repo
275 275 :param source: boolean flag to specify if repo_name refers to source
276 276 :param statuses: list of pull request statuses
277 277 :param opened_by: author user of the pull request
278 278 :param offset: pagination offset
279 279 :param length: length of returned list
280 280 :param order_by: order of the returned list
281 281 :param order_dir: 'asc' or 'desc' ordering direction
282 282 :returns: list of pull requests
283 283 """
284 284 pull_requests = self.get_all(
285 285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 286 order_by=order_by, order_dir=order_dir)
287 287
288 288 _filtered_pull_requests = []
289 289 for pr in pull_requests:
290 290 status = pr.calculated_review_status()
291 291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 293 _filtered_pull_requests.append(pr)
294 294 if length:
295 295 return _filtered_pull_requests[offset:offset+length]
296 296 else:
297 297 return _filtered_pull_requests
298 298
299 299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 300 opened_by=None, user_id=None):
301 301 """
302 302 Count the number of pull requests for a specific repository that are
303 303 awaiting review from a specific user.
304 304
305 305 :param repo_name: target or source repo
306 306 :param source: boolean flag to specify if repo_name refers to source
307 307 :param statuses: list of pull request statuses
308 308 :param opened_by: author user of the pull request
309 309 :param user_id: reviewer user of the pull request
310 310 :returns: int number of pull requests
311 311 """
312 312 pull_requests = self.get_awaiting_my_review(
313 313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 314 user_id=user_id)
315 315
316 316 return len(pull_requests)
317 317
318 318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 319 opened_by=None, user_id=None, offset=0,
320 320 length=None, order_by=None, order_dir='desc'):
321 321 """
322 322 Get all pull requests for a specific repository that are awaiting
323 323 review from a specific user.
324 324
325 325 :param repo_name: target or source repo
326 326 :param source: boolean flag to specify if repo_name refers to source
327 327 :param statuses: list of pull request statuses
328 328 :param opened_by: author user of the pull request
329 329 :param user_id: reviewer user of the pull request
330 330 :param offset: pagination offset
331 331 :param length: length of returned list
332 332 :param order_by: order of the returned list
333 333 :param order_dir: 'asc' or 'desc' ordering direction
334 334 :returns: list of pull requests
335 335 """
336 336 pull_requests = self.get_all(
337 337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 338 order_by=order_by, order_dir=order_dir)
339 339
340 340 _my = PullRequestModel().get_not_reviewed(user_id)
341 341 my_participation = []
342 342 for pr in pull_requests:
343 343 if pr in _my:
344 344 my_participation.append(pr)
345 345 _filtered_pull_requests = my_participation
346 346 if length:
347 347 return _filtered_pull_requests[offset:offset+length]
348 348 else:
349 349 return _filtered_pull_requests
350 350
351 351 def get_not_reviewed(self, user_id):
352 352 return [
353 353 x.pull_request for x in PullRequestReviewers.query().filter(
354 354 PullRequestReviewers.user_id == user_id).all()
355 355 ]
356 356
357 357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 358 order_by=None, order_dir='desc'):
359 359 q = PullRequest.query()
360 360 if user_id:
361 361 reviewers_subquery = Session().query(
362 362 PullRequestReviewers.pull_request_id).filter(
363 363 PullRequestReviewers.user_id == user_id).subquery()
364 364 user_filter= or_(
365 365 PullRequest.user_id == user_id,
366 366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 367 )
368 368 q = PullRequest.query().filter(user_filter)
369 369
370 370 # closed,opened
371 371 if statuses:
372 372 q = q.filter(PullRequest.status.in_(statuses))
373 373
374 374 if order_by:
375 375 order_map = {
376 376 'name_raw': PullRequest.pull_request_id,
377 377 'title': PullRequest.title,
378 378 'updated_on_raw': PullRequest.updated_on,
379 379 'target_repo': PullRequest.target_repo_id
380 380 }
381 381 if order_dir == 'asc':
382 382 q = q.order_by(order_map[order_by].asc())
383 383 else:
384 384 q = q.order_by(order_map[order_by].desc())
385 385
386 386 return q
387 387
388 388 def count_im_participating_in(self, user_id=None, statuses=None):
389 389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 390 return q.count()
391 391
392 392 def get_im_participating_in(
393 393 self, user_id=None, statuses=None, offset=0,
394 394 length=None, order_by=None, order_dir='desc'):
395 395 """
396 396 Get all Pull requests that i'm participating in, or i have opened
397 397 """
398 398
399 399 q = self._prepare_participating_query(
400 400 user_id, statuses=statuses, order_by=order_by,
401 401 order_dir=order_dir)
402 402
403 403 if length:
404 404 pull_requests = q.limit(length).offset(offset).all()
405 405 else:
406 406 pull_requests = q.all()
407 407
408 408 return pull_requests
409 409
410 410 def get_versions(self, pull_request):
411 411 """
412 412 returns version of pull request sorted by ID descending
413 413 """
414 414 return PullRequestVersion.query()\
415 415 .filter(PullRequestVersion.pull_request == pull_request)\
416 416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 417 .all()
418 418
419 419 def create(self, created_by, source_repo, source_ref, target_repo,
420 420 target_ref, revisions, reviewers, title, description=None,
421 421 reviewer_data=None):
422 422
423 423 created_by_user = self._get_user(created_by)
424 424 source_repo = self._get_repo(source_repo)
425 425 target_repo = self._get_repo(target_repo)
426 426
427 427 pull_request = PullRequest()
428 428 pull_request.source_repo = source_repo
429 429 pull_request.source_ref = source_ref
430 430 pull_request.target_repo = target_repo
431 431 pull_request.target_ref = target_ref
432 432 pull_request.revisions = revisions
433 433 pull_request.title = title
434 434 pull_request.description = description
435 435 pull_request.author = created_by_user
436 436 pull_request.reviewer_data = reviewer_data
437 437
438 438 Session().add(pull_request)
439 439 Session().flush()
440 440
441 441 reviewer_ids = set()
442 442 # members / reviewers
443 443 for reviewer_object in reviewers:
444 444 user_id, reasons, mandatory = reviewer_object
445 445 user = self._get_user(user_id)
446 446
447 447 # skip duplicates
448 448 if user.user_id in reviewer_ids:
449 449 continue
450 450
451 451 reviewer_ids.add(user.user_id)
452 452
453 453 reviewer = PullRequestReviewers()
454 454 reviewer.user = user
455 455 reviewer.pull_request = pull_request
456 456 reviewer.reasons = reasons
457 457 reviewer.mandatory = mandatory
458 458 Session().add(reviewer)
459 459
460 460 # Set approval status to "Under Review" for all commits which are
461 461 # part of this pull request.
462 462 ChangesetStatusModel().set_status(
463 463 repo=target_repo,
464 464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 465 user=created_by_user,
466 466 pull_request=pull_request
467 467 )
468 468
469 469 self.notify_reviewers(pull_request, reviewer_ids)
470 470 self._trigger_pull_request_hook(
471 471 pull_request, created_by_user, 'create')
472 472
473 473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 474 self._log_audit_action(
475 475 'repo.pull_request.create', {'data': creation_data},
476 476 created_by_user, pull_request)
477 477
478 478 return pull_request
479 479
480 480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 481 pull_request = self.__get_pull_request(pull_request)
482 482 target_scm = pull_request.target_repo.scm_instance()
483 483 if action == 'create':
484 484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 485 elif action == 'merge':
486 486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 487 elif action == 'close':
488 488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 489 elif action == 'review_status_change':
490 490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 491 elif action == 'update':
492 492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 493 else:
494 494 return
495 495
496 496 trigger_hook(
497 497 username=user.username,
498 498 repo_name=pull_request.target_repo.repo_name,
499 499 repo_alias=target_scm.alias,
500 500 pull_request=pull_request)
501 501
502 502 def _get_commit_ids(self, pull_request):
503 503 """
504 504 Return the commit ids of the merged pull request.
505 505
506 506 This method is not dealing correctly yet with the lack of autoupdates
507 507 nor with the implicit target updates.
508 508 For example: if a commit in the source repo is already in the target it
509 509 will be reported anyways.
510 510 """
511 511 merge_rev = pull_request.merge_rev
512 512 if merge_rev is None:
513 513 raise ValueError('This pull request was not merged yet')
514 514
515 515 commit_ids = list(pull_request.revisions)
516 516 if merge_rev not in commit_ids:
517 517 commit_ids.append(merge_rev)
518 518
519 519 return commit_ids
520 520
521 521 def merge(self, pull_request, user, extras):
522 522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 524 if merge_state.executed:
525 525 log.debug(
526 526 "Merge was successful, updating the pull request comments.")
527 527 self._comment_and_close_pr(pull_request, user, merge_state)
528 528
529 529 self._log_audit_action(
530 530 'repo.pull_request.merge',
531 531 {'merge_state': merge_state.__dict__},
532 532 user, pull_request)
533 533
534 534 else:
535 535 log.warn("Merge failed, not updating the pull request.")
536 536 return merge_state
537 537
538 538 def _merge_pull_request(self, pull_request, user, extras):
539 539 target_vcs = pull_request.target_repo.scm_instance()
540 540 source_vcs = pull_request.source_repo.scm_instance()
541 541 target_ref = self._refresh_reference(
542 542 pull_request.target_ref_parts, target_vcs)
543 543
544 544 message = _(
545 545 'Merge pull request #%(pr_id)s from '
546 546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 547 'pr_id': pull_request.pull_request_id,
548 548 'source_repo': source_vcs.name,
549 549 'source_ref_name': pull_request.source_ref_parts.name,
550 550 'pr_title': pull_request.title
551 551 }
552 552
553 553 workspace_id = self._workspace_id(pull_request)
554 554 use_rebase = self._use_rebase_for_merging(pull_request)
555 555
556 556 callback_daemon, extras = prepare_callback_daemon(
557 557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559 559
560 560 with callback_daemon:
561 561 # TODO: johbo: Implement a clean way to run a config_override
562 562 # for a single call.
563 563 target_vcs.config.set(
564 564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 565 merge_state = target_vcs.merge(
566 566 target_ref, source_vcs, pull_request.source_ref_parts,
567 567 workspace_id, user_name=user.username,
568 568 user_email=user.email, message=message, use_rebase=use_rebase)
569 569 return merge_state
570 570
571 571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 573 pull_request.updated_on = datetime.datetime.now()
574 574
575 575 CommentsModel().create(
576 576 text=unicode(_('Pull request merged and closed')),
577 577 repo=pull_request.target_repo.repo_id,
578 578 user=user.user_id,
579 579 pull_request=pull_request.pull_request_id,
580 580 f_path=None,
581 581 line_no=None,
582 582 closing_pr=True
583 583 )
584 584
585 585 Session().add(pull_request)
586 586 Session().flush()
587 587 # TODO: paris: replace invalidation with less radical solution
588 588 ScmModel().mark_for_invalidation(
589 589 pull_request.target_repo.repo_name)
590 590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591 591
592 592 def has_valid_update_type(self, pull_request):
593 593 source_ref_type = pull_request.source_ref_parts.type
594 594 return source_ref_type in ['book', 'branch', 'tag']
595 595
596 596 def update_commits(self, pull_request):
597 597 """
598 598 Get the updated list of commits for the pull request
599 599 and return the new pull request version and the list
600 600 of commits processed by this update action
601 601 """
602 602 pull_request = self.__get_pull_request(pull_request)
603 603 source_ref_type = pull_request.source_ref_parts.type
604 604 source_ref_name = pull_request.source_ref_parts.name
605 605 source_ref_id = pull_request.source_ref_parts.commit_id
606 606
607 607 target_ref_type = pull_request.target_ref_parts.type
608 608 target_ref_name = pull_request.target_ref_parts.name
609 609 target_ref_id = pull_request.target_ref_parts.commit_id
610 610
611 611 if not self.has_valid_update_type(pull_request):
612 612 log.debug(
613 613 "Skipping update of pull request %s due to ref type: %s",
614 614 pull_request, source_ref_type)
615 615 return UpdateResponse(
616 616 executed=False,
617 617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 618 old=pull_request, new=None, changes=None,
619 619 source_changed=False, target_changed=False)
620 620
621 621 # source repo
622 622 source_repo = pull_request.source_repo.scm_instance()
623 623 try:
624 624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 625 except CommitDoesNotExistError:
626 626 return UpdateResponse(
627 627 executed=False,
628 628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 629 old=pull_request, new=None, changes=None,
630 630 source_changed=False, target_changed=False)
631 631
632 632 source_changed = source_ref_id != source_commit.raw_id
633 633
634 634 # target repo
635 635 target_repo = pull_request.target_repo.scm_instance()
636 636 try:
637 637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 638 except CommitDoesNotExistError:
639 639 return UpdateResponse(
640 640 executed=False,
641 641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 642 old=pull_request, new=None, changes=None,
643 643 source_changed=False, target_changed=False)
644 644 target_changed = target_ref_id != target_commit.raw_id
645 645
646 646 if not (source_changed or target_changed):
647 647 log.debug("Nothing changed in pull request %s", pull_request)
648 648 return UpdateResponse(
649 649 executed=False,
650 650 reason=UpdateFailureReason.NO_CHANGE,
651 651 old=pull_request, new=None, changes=None,
652 652 source_changed=target_changed, target_changed=source_changed)
653 653
654 654 change_in_found = 'target repo' if target_changed else 'source repo'
655 655 log.debug('Updating pull request because of change in %s detected',
656 656 change_in_found)
657 657
658 658 # Finally there is a need for an update, in case of source change
659 659 # we create a new version, else just an update
660 660 if source_changed:
661 661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 662 self._link_comments_to_version(pull_request_version)
663 663 else:
664 664 try:
665 665 ver = pull_request.versions[-1]
666 666 except IndexError:
667 667 ver = None
668 668
669 669 pull_request.pull_request_version_id = \
670 670 ver.pull_request_version_id if ver else None
671 671 pull_request_version = pull_request
672 672
673 673 try:
674 674 if target_ref_type in ('tag', 'branch', 'book'):
675 675 target_commit = target_repo.get_commit(target_ref_name)
676 676 else:
677 677 target_commit = target_repo.get_commit(target_ref_id)
678 678 except CommitDoesNotExistError:
679 679 return UpdateResponse(
680 680 executed=False,
681 681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 682 old=pull_request, new=None, changes=None,
683 683 source_changed=source_changed, target_changed=target_changed)
684 684
685 685 # re-compute commit ids
686 686 old_commit_ids = pull_request.revisions
687 687 pre_load = ["author", "branch", "date", "message"]
688 688 commit_ranges = target_repo.compare(
689 689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 690 pre_load=pre_load)
691 691
692 692 ancestor = target_repo.get_common_ancestor(
693 693 target_commit.raw_id, source_commit.raw_id, source_repo)
694 694
695 695 pull_request.source_ref = '%s:%s:%s' % (
696 696 source_ref_type, source_ref_name, source_commit.raw_id)
697 697 pull_request.target_ref = '%s:%s:%s' % (
698 698 target_ref_type, target_ref_name, ancestor)
699 699
700 700 pull_request.revisions = [
701 701 commit.raw_id for commit in reversed(commit_ranges)]
702 702 pull_request.updated_on = datetime.datetime.now()
703 703 Session().add(pull_request)
704 704 new_commit_ids = pull_request.revisions
705 705
706 706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 707 pull_request, pull_request_version)
708 708
709 709 # calculate commit and file changes
710 710 changes = self._calculate_commit_id_changes(
711 711 old_commit_ids, new_commit_ids)
712 712 file_changes = self._calculate_file_changes(
713 713 old_diff_data, new_diff_data)
714 714
715 715 # set comments as outdated if DIFFS changed
716 716 CommentsModel().outdate_comments(
717 717 pull_request, old_diff_data=old_diff_data,
718 718 new_diff_data=new_diff_data)
719 719
720 720 commit_changes = (changes.added or changes.removed)
721 721 file_node_changes = (
722 722 file_changes.added or file_changes.modified or file_changes.removed)
723 723 pr_has_changes = commit_changes or file_node_changes
724 724
725 725 # Add an automatic comment to the pull request, in case
726 726 # anything has changed
727 727 if pr_has_changes:
728 728 update_comment = CommentsModel().create(
729 729 text=self._render_update_message(changes, file_changes),
730 730 repo=pull_request.target_repo,
731 731 user=pull_request.author,
732 732 pull_request=pull_request,
733 733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734 734
735 735 # Update status to "Under Review" for added commits
736 736 for commit_id in changes.added:
737 737 ChangesetStatusModel().set_status(
738 738 repo=pull_request.source_repo,
739 739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 740 comment=update_comment,
741 741 user=pull_request.author,
742 742 pull_request=pull_request,
743 743 revision=commit_id)
744 744
745 745 log.debug(
746 746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 747 'removed_ids: %s', pull_request.pull_request_id,
748 748 changes.added, changes.common, changes.removed)
749 749 log.debug(
750 750 'Updated pull request with the following file changes: %s',
751 751 file_changes)
752 752
753 753 log.info(
754 754 "Updated pull request %s from commit %s to commit %s, "
755 755 "stored new version %s of this pull request.",
756 756 pull_request.pull_request_id, source_ref_id,
757 757 pull_request.source_ref_parts.commit_id,
758 758 pull_request_version.pull_request_version_id)
759 759 Session().commit()
760 760 self._trigger_pull_request_hook(
761 761 pull_request, pull_request.author, 'update')
762 762
763 763 return UpdateResponse(
764 764 executed=True, reason=UpdateFailureReason.NONE,
765 765 old=pull_request, new=pull_request_version, changes=changes,
766 766 source_changed=source_changed, target_changed=target_changed)
767 767
768 768 def _create_version_from_snapshot(self, pull_request):
769 769 version = PullRequestVersion()
770 770 version.title = pull_request.title
771 771 version.description = pull_request.description
772 772 version.status = pull_request.status
773 773 version.created_on = datetime.datetime.now()
774 774 version.updated_on = pull_request.updated_on
775 775 version.user_id = pull_request.user_id
776 776 version.source_repo = pull_request.source_repo
777 777 version.source_ref = pull_request.source_ref
778 778 version.target_repo = pull_request.target_repo
779 779 version.target_ref = pull_request.target_ref
780 780
781 781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 783 version.last_merge_status = pull_request.last_merge_status
784 784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 785 version.merge_rev = pull_request.merge_rev
786 786 version.reviewer_data = pull_request.reviewer_data
787 787
788 788 version.revisions = pull_request.revisions
789 789 version.pull_request = pull_request
790 790 Session().add(version)
791 791 Session().flush()
792 792
793 793 return version
794 794
795 795 def _generate_update_diffs(self, pull_request, pull_request_version):
796 796
797 797 diff_context = (
798 798 self.DIFF_CONTEXT +
799 799 CommentsModel.needed_extra_diff_context())
800 800
801 801 source_repo = pull_request_version.source_repo
802 802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 804 old_diff = self._get_diff_from_pr_or_version(
805 805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806 806
807 807 source_repo = pull_request.source_repo
808 808 source_ref_id = pull_request.source_ref_parts.commit_id
809 809 target_ref_id = pull_request.target_ref_parts.commit_id
810 810
811 811 new_diff = self._get_diff_from_pr_or_version(
812 812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813 813
814 814 old_diff_data = diffs.DiffProcessor(old_diff)
815 815 old_diff_data.prepare()
816 816 new_diff_data = diffs.DiffProcessor(new_diff)
817 817 new_diff_data.prepare()
818 818
819 819 return old_diff_data, new_diff_data
820 820
821 821 def _link_comments_to_version(self, pull_request_version):
822 822 """
823 823 Link all unlinked comments of this pull request to the given version.
824 824
825 825 :param pull_request_version: The `PullRequestVersion` to which
826 826 the comments shall be linked.
827 827
828 828 """
829 829 pull_request = pull_request_version.pull_request
830 830 comments = ChangesetComment.query()\
831 831 .filter(
832 832 # TODO: johbo: Should we query for the repo at all here?
833 833 # Pending decision on how comments of PRs are to be related
834 834 # to either the source repo, the target repo or no repo at all.
835 835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 836 ChangesetComment.pull_request == pull_request,
837 837 ChangesetComment.pull_request_version == None)\
838 838 .order_by(ChangesetComment.comment_id.asc())
839 839
840 840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 841 # operation.
842 842 for comment in comments:
843 843 comment.pull_request_version_id = (
844 844 pull_request_version.pull_request_version_id)
845 845 Session().add(comment)
846 846
847 847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 848 added = [x for x in new_ids if x not in old_ids]
849 849 common = [x for x in new_ids if x in old_ids]
850 850 removed = [x for x in old_ids if x not in new_ids]
851 851 total = new_ids
852 852 return ChangeTuple(added, common, removed, total)
853 853
854 854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855 855
856 856 old_files = OrderedDict()
857 857 for diff_data in old_diff_data.parsed_diff:
858 858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859 859
860 860 added_files = []
861 861 modified_files = []
862 862 removed_files = []
863 863 for diff_data in new_diff_data.parsed_diff:
864 864 new_filename = diff_data['filename']
865 865 new_hash = md5_safe(diff_data['raw_diff'])
866 866
867 867 old_hash = old_files.get(new_filename)
868 868 if not old_hash:
869 869 # file is not present in old diff, means it's added
870 870 added_files.append(new_filename)
871 871 else:
872 872 if new_hash != old_hash:
873 873 modified_files.append(new_filename)
874 874 # now remove a file from old, since we have seen it already
875 875 del old_files[new_filename]
876 876
877 877 # removed files is when there are present in old, but not in NEW,
878 878 # since we remove old files that are present in new diff, left-overs
879 879 # if any should be the removed files
880 880 removed_files.extend(old_files.keys())
881 881
882 882 return FileChangeTuple(added_files, modified_files, removed_files)
883 883
884 884 def _render_update_message(self, changes, file_changes):
885 885 """
886 886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 887 so it's always looking the same disregarding on which default
888 888 renderer system is using.
889 889
890 890 :param changes: changes named tuple
891 891 :param file_changes: file changes named tuple
892 892
893 893 """
894 894 new_status = ChangesetStatus.get_status_lbl(
895 895 ChangesetStatus.STATUS_UNDER_REVIEW)
896 896
897 897 changed_files = (
898 898 file_changes.added + file_changes.modified + file_changes.removed)
899 899
900 900 params = {
901 901 'under_review_label': new_status,
902 902 'added_commits': changes.added,
903 903 'removed_commits': changes.removed,
904 904 'changed_files': changed_files,
905 905 'added_files': file_changes.added,
906 906 'modified_files': file_changes.modified,
907 907 'removed_files': file_changes.removed,
908 908 }
909 909 renderer = RstTemplateRenderer()
910 910 return renderer.render('pull_request_update.mako', **params)
911 911
912 912 def edit(self, pull_request, title, description, user):
913 913 pull_request = self.__get_pull_request(pull_request)
914 914 old_data = pull_request.get_api_data(with_merge_state=False)
915 915 if pull_request.is_closed():
916 916 raise ValueError('This pull request is closed')
917 917 if title:
918 918 pull_request.title = title
919 919 pull_request.description = description
920 920 pull_request.updated_on = datetime.datetime.now()
921 921 Session().add(pull_request)
922 922 self._log_audit_action(
923 923 'repo.pull_request.edit', {'old_data': old_data},
924 924 user, pull_request)
925 925
926 926 def update_reviewers(self, pull_request, reviewer_data, user):
927 927 """
928 928 Update the reviewers in the pull request
929 929
930 930 :param pull_request: the pr to update
931 931 :param reviewer_data: list of tuples
932 932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 933 """
934 934
935 935 reviewers = {}
936 936 for user_id, reasons, mandatory in reviewer_data:
937 937 if isinstance(user_id, (int, basestring)):
938 938 user_id = self._get_user(user_id).user_id
939 939 reviewers[user_id] = {
940 940 'reasons': reasons, 'mandatory': mandatory}
941 941
942 942 reviewers_ids = set(reviewers.keys())
943 943 pull_request = self.__get_pull_request(pull_request)
944 944 current_reviewers = PullRequestReviewers.query()\
945 945 .filter(PullRequestReviewers.pull_request ==
946 946 pull_request).all()
947 947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948 948
949 949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951 951
952 952 log.debug("Adding %s reviewers", ids_to_add)
953 953 log.debug("Removing %s reviewers", ids_to_remove)
954 954 changed = False
955 955 for uid in ids_to_add:
956 956 changed = True
957 957 _usr = self._get_user(uid)
958 958 reviewer = PullRequestReviewers()
959 959 reviewer.user = _usr
960 960 reviewer.pull_request = pull_request
961 961 reviewer.reasons = reviewers[uid]['reasons']
962 962 # NOTE(marcink): mandatory shouldn't be changed now
963 963 # reviewer.mandatory = reviewers[uid]['reasons']
964 964 Session().add(reviewer)
965 965 self._log_audit_action(
966 966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 967 user, pull_request)
968 968
969 969 for uid in ids_to_remove:
970 970 changed = True
971 971 reviewers = PullRequestReviewers.query()\
972 972 .filter(PullRequestReviewers.user_id == uid,
973 973 PullRequestReviewers.pull_request == pull_request)\
974 974 .all()
975 975 # use .all() in case we accidentally added the same person twice
976 976 # this CAN happen due to the lack of DB checks
977 977 for obj in reviewers:
978 978 old_data = obj.get_dict()
979 979 Session().delete(obj)
980 980 self._log_audit_action(
981 981 'repo.pull_request.reviewer.delete',
982 982 {'old_data': old_data}, user, pull_request)
983 983
984 984 if changed:
985 985 pull_request.updated_on = datetime.datetime.now()
986 986 Session().add(pull_request)
987 987
988 988 self.notify_reviewers(pull_request, ids_to_add)
989 989 return ids_to_add, ids_to_remove
990 990
991 991 def get_url(self, pull_request, request=None, permalink=False):
992 992 if not request:
993 993 request = get_current_request()
994 994
995 995 if permalink:
996 996 return request.route_url(
997 997 'pull_requests_global',
998 998 pull_request_id=pull_request.pull_request_id,)
999 999 else:
1000 1000 return request.route_url('pullrequest_show',
1001 1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 1002 pull_request_id=pull_request.pull_request_id,)
1003 1003
1004 1004 def get_shadow_clone_url(self, pull_request):
1005 1005 """
1006 1006 Returns qualified url pointing to the shadow repository. If this pull
1007 1007 request is closed there is no shadow repository and ``None`` will be
1008 1008 returned.
1009 1009 """
1010 1010 if pull_request.is_closed():
1011 1011 return None
1012 1012 else:
1013 1013 pr_url = urllib.unquote(self.get_url(pull_request))
1014 1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015 1015
1016 1016 def notify_reviewers(self, pull_request, reviewers_ids):
1017 1017 # notification to reviewers
1018 1018 if not reviewers_ids:
1019 1019 return
1020 1020
1021 1021 pull_request_obj = pull_request
1022 1022 # get the current participants of this pull request
1023 1023 recipients = reviewers_ids
1024 1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025 1025
1026 1026 pr_source_repo = pull_request_obj.source_repo
1027 1027 pr_target_repo = pull_request_obj.target_repo
1028 1028
1029 1029 pr_url = h.route_url('pullrequest_show',
1030 1030 repo_name=pr_target_repo.repo_name,
1031 1031 pull_request_id=pull_request_obj.pull_request_id,)
1032 1032
1033 1033 # set some variables for email notification
1034 1034 pr_target_repo_url = h.route_url(
1035 1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1036 1036
1037 1037 pr_source_repo_url = h.route_url(
1038 1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1039 1039
1040 1040 # pull request specifics
1041 1041 pull_request_commits = [
1042 1042 (x.raw_id, x.message)
1043 1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1044 1044
1045 1045 kwargs = {
1046 1046 'user': pull_request.author,
1047 1047 'pull_request': pull_request_obj,
1048 1048 'pull_request_commits': pull_request_commits,
1049 1049
1050 1050 'pull_request_target_repo': pr_target_repo,
1051 1051 'pull_request_target_repo_url': pr_target_repo_url,
1052 1052
1053 1053 'pull_request_source_repo': pr_source_repo,
1054 1054 'pull_request_source_repo_url': pr_source_repo_url,
1055 1055
1056 1056 'pull_request_url': pr_url,
1057 1057 }
1058 1058
1059 1059 # pre-generate the subject for notification itself
1060 1060 (subject,
1061 1061 _h, _e, # we don't care about those
1062 1062 body_plaintext) = EmailNotificationModel().render_email(
1063 1063 notification_type, **kwargs)
1064 1064
1065 1065 # create notification objects, and emails
1066 1066 NotificationModel().create(
1067 1067 created_by=pull_request.author,
1068 1068 notification_subject=subject,
1069 1069 notification_body=body_plaintext,
1070 1070 notification_type=notification_type,
1071 1071 recipients=recipients,
1072 1072 email_kwargs=kwargs,
1073 1073 )
1074 1074
1075 1075 def delete(self, pull_request, user):
1076 1076 pull_request = self.__get_pull_request(pull_request)
1077 1077 old_data = pull_request.get_api_data(with_merge_state=False)
1078 1078 self._cleanup_merge_workspace(pull_request)
1079 1079 self._log_audit_action(
1080 1080 'repo.pull_request.delete', {'old_data': old_data},
1081 1081 user, pull_request)
1082 1082 Session().delete(pull_request)
1083 1083
1084 1084 def close_pull_request(self, pull_request, user):
1085 1085 pull_request = self.__get_pull_request(pull_request)
1086 1086 self._cleanup_merge_workspace(pull_request)
1087 1087 pull_request.status = PullRequest.STATUS_CLOSED
1088 1088 pull_request.updated_on = datetime.datetime.now()
1089 1089 Session().add(pull_request)
1090 1090 self._trigger_pull_request_hook(
1091 1091 pull_request, pull_request.author, 'close')
1092 1092 self._log_audit_action(
1093 1093 'repo.pull_request.close', {}, user, pull_request)
1094 1094
1095 1095 def close_pull_request_with_comment(
1096 1096 self, pull_request, user, repo, message=None):
1097 1097
1098 1098 pull_request_review_status = pull_request.calculated_review_status()
1099 1099
1100 1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1101 1101 # approved only if we have voting consent
1102 1102 status = ChangesetStatus.STATUS_APPROVED
1103 1103 else:
1104 1104 status = ChangesetStatus.STATUS_REJECTED
1105 1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1106 1106
1107 1107 default_message = (
1108 1108 _('Closing with status change {transition_icon} {status}.')
1109 1109 ).format(transition_icon='>', status=status_lbl)
1110 1110 text = message or default_message
1111 1111
1112 1112 # create a comment, and link it to new status
1113 1113 comment = CommentsModel().create(
1114 1114 text=text,
1115 1115 repo=repo.repo_id,
1116 1116 user=user.user_id,
1117 1117 pull_request=pull_request.pull_request_id,
1118 1118 status_change=status_lbl,
1119 1119 status_change_type=status,
1120 1120 closing_pr=True
1121 1121 )
1122 1122
1123 1123 # calculate old status before we change it
1124 1124 old_calculated_status = pull_request.calculated_review_status()
1125 1125 ChangesetStatusModel().set_status(
1126 1126 repo.repo_id,
1127 1127 status,
1128 1128 user.user_id,
1129 1129 comment=comment,
1130 1130 pull_request=pull_request.pull_request_id
1131 1131 )
1132 1132
1133 1133 Session().flush()
1134 1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1135 1135 # we now calculate the status of pull request again, and based on that
1136 1136 # calculation trigger status change. This might happen in cases
1137 1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1138 1138 # change the status, while if he's a reviewer this might change it.
1139 1139 calculated_status = pull_request.calculated_review_status()
1140 1140 if old_calculated_status != calculated_status:
1141 1141 self._trigger_pull_request_hook(
1142 1142 pull_request, user, 'review_status_change')
1143 1143
1144 1144 # finally close the PR
1145 1145 PullRequestModel().close_pull_request(
1146 1146 pull_request.pull_request_id, user)
1147 1147
1148 1148 return comment, status
1149 1149
1150 1150 def merge_status(self, pull_request):
1151 1151 if not self._is_merge_enabled(pull_request):
1152 1152 return False, _('Server-side pull request merging is disabled.')
1153 1153 if pull_request.is_closed():
1154 1154 return False, _('This pull request is closed.')
1155 1155 merge_possible, msg = self._check_repo_requirements(
1156 1156 target=pull_request.target_repo, source=pull_request.source_repo)
1157 1157 if not merge_possible:
1158 1158 return merge_possible, msg
1159 1159
1160 1160 try:
1161 1161 resp = self._try_merge(pull_request)
1162 1162 log.debug("Merge response: %s", resp)
1163 1163 status = resp.possible, self.merge_status_message(
1164 1164 resp.failure_reason)
1165 1165 except NotImplementedError:
1166 1166 status = False, _('Pull request merging is not supported.')
1167 1167
1168 1168 return status
1169 1169
1170 1170 def _check_repo_requirements(self, target, source):
1171 1171 """
1172 1172 Check if `target` and `source` have compatible requirements.
1173 1173
1174 1174 Currently this is just checking for largefiles.
1175 1175 """
1176 1176 target_has_largefiles = self._has_largefiles(target)
1177 1177 source_has_largefiles = self._has_largefiles(source)
1178 1178 merge_possible = True
1179 1179 message = u''
1180 1180
1181 1181 if target_has_largefiles != source_has_largefiles:
1182 1182 merge_possible = False
1183 1183 if source_has_largefiles:
1184 1184 message = _(
1185 1185 'Target repository large files support is disabled.')
1186 1186 else:
1187 1187 message = _(
1188 1188 'Source repository large files support is disabled.')
1189 1189
1190 1190 return merge_possible, message
1191 1191
1192 1192 def _has_largefiles(self, repo):
1193 1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1194 1194 'extensions', 'largefiles')
1195 1195 return largefiles_ui and largefiles_ui[0].active
1196 1196
1197 1197 def _try_merge(self, pull_request):
1198 1198 """
1199 1199 Try to merge the pull request and return the merge status.
1200 1200 """
1201 1201 log.debug(
1202 1202 "Trying out if the pull request %s can be merged.",
1203 1203 pull_request.pull_request_id)
1204 1204 target_vcs = pull_request.target_repo.scm_instance()
1205 1205
1206 1206 # Refresh the target reference.
1207 1207 try:
1208 1208 target_ref = self._refresh_reference(
1209 1209 pull_request.target_ref_parts, target_vcs)
1210 1210 except CommitDoesNotExistError:
1211 1211 merge_state = MergeResponse(
1212 1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1213 1213 return merge_state
1214 1214
1215 1215 target_locked = pull_request.target_repo.locked
1216 1216 if target_locked and target_locked[0]:
1217 1217 log.debug("The target repository is locked.")
1218 1218 merge_state = MergeResponse(
1219 1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1220 1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1221 1221 log.debug("Refreshing the merge status of the repository.")
1222 1222 merge_state = self._refresh_merge_state(
1223 1223 pull_request, target_vcs, target_ref)
1224 1224 else:
1225 1225 possible = pull_request.\
1226 1226 last_merge_status == MergeFailureReason.NONE
1227 1227 merge_state = MergeResponse(
1228 1228 possible, False, None, pull_request.last_merge_status)
1229 1229
1230 1230 return merge_state
1231 1231
1232 1232 def _refresh_reference(self, reference, vcs_repository):
1233 1233 if reference.type in ('branch', 'book'):
1234 1234 name_or_id = reference.name
1235 1235 else:
1236 1236 name_or_id = reference.commit_id
1237 1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1238 1238 refreshed_reference = Reference(
1239 1239 reference.type, reference.name, refreshed_commit.raw_id)
1240 1240 return refreshed_reference
1241 1241
1242 1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1243 1243 return not(
1244 1244 pull_request.revisions and
1245 1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1246 1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1247 1247
1248 1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1249 1249 workspace_id = self._workspace_id(pull_request)
1250 1250 source_vcs = pull_request.source_repo.scm_instance()
1251 1251 use_rebase = self._use_rebase_for_merging(pull_request)
1252 1252 merge_state = target_vcs.merge(
1253 1253 target_reference, source_vcs, pull_request.source_ref_parts,
1254 1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1255 1255
1256 1256 # Do not store the response if there was an unknown error.
1257 1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1258 1258 pull_request._last_merge_source_rev = \
1259 1259 pull_request.source_ref_parts.commit_id
1260 1260 pull_request._last_merge_target_rev = target_reference.commit_id
1261 1261 pull_request.last_merge_status = merge_state.failure_reason
1262 1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1263 1263 Session().add(pull_request)
1264 1264 Session().commit()
1265 1265
1266 1266 return merge_state
1267 1267
1268 1268 def _workspace_id(self, pull_request):
1269 1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270 1270 return workspace_id
1271 1271
1272 1272 def merge_status_message(self, status_code):
1273 1273 """
1274 1274 Return a human friendly error message for the given merge status code.
1275 1275 """
1276 1276 return self.MERGE_STATUS_MESSAGES[status_code]
1277 1277
1278 1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1279 1279 bookmark=None):
1280 1280 all_refs, selected_ref = \
1281 1281 self._get_repo_pullrequest_sources(
1282 1282 repo.scm_instance(), commit_id=commit_id,
1283 1283 branch=branch, bookmark=bookmark)
1284 1284
1285 1285 refs_select2 = []
1286 1286 for element in all_refs:
1287 1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1288 1288 refs_select2.append({'text': element[1], 'children': children})
1289 1289
1290 1290 return {
1291 1291 'user': {
1292 1292 'user_id': repo.user.user_id,
1293 1293 'username': repo.user.username,
1294 1294 'firstname': repo.user.first_name,
1295 1295 'lastname': repo.user.last_name,
1296 1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1297 1297 },
1298 1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1299 1299 'refs': {
1300 1300 'all_refs': all_refs,
1301 1301 'selected_ref': selected_ref,
1302 1302 'select2_refs': refs_select2
1303 1303 }
1304 1304 }
1305 1305
1306 1306 def generate_pullrequest_title(self, source, source_ref, target):
1307 1307 return u'{source}#{at_ref} to {target}'.format(
1308 1308 source=source,
1309 1309 at_ref=source_ref,
1310 1310 target=target,
1311 1311 )
1312 1312
1313 1313 def _cleanup_merge_workspace(self, pull_request):
1314 1314 # Merging related cleanup
1315 1315 target_scm = pull_request.target_repo.scm_instance()
1316 1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1317 1317
1318 1318 try:
1319 1319 target_scm.cleanup_merge_workspace(workspace_id)
1320 1320 except NotImplementedError:
1321 1321 pass
1322 1322
1323 1323 def _get_repo_pullrequest_sources(
1324 1324 self, repo, commit_id=None, branch=None, bookmark=None):
1325 1325 """
1326 1326 Return a structure with repo's interesting commits, suitable for
1327 1327 the selectors in pullrequest controller
1328 1328
1329 1329 :param commit_id: a commit that must be in the list somehow
1330 1330 and selected by default
1331 1331 :param branch: a branch that must be in the list and selected
1332 1332 by default - even if closed
1333 1333 :param bookmark: a bookmark that must be in the list and selected
1334 1334 """
1335 1335
1336 1336 commit_id = safe_str(commit_id) if commit_id else None
1337 1337 branch = safe_str(branch) if branch else None
1338 1338 bookmark = safe_str(bookmark) if bookmark else None
1339 1339
1340 1340 selected = None
1341 1341
1342 1342 # order matters: first source that has commit_id in it will be selected
1343 1343 sources = []
1344 1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1345 1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1346 1346
1347 1347 if commit_id:
1348 1348 ref_commit = (h.short_id(commit_id), commit_id)
1349 1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1350 1350
1351 1351 sources.append(
1352 1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1353 1353 )
1354 1354
1355 1355 groups = []
1356 1356 for group_key, ref_list, group_name, match in sources:
1357 1357 group_refs = []
1358 1358 for ref_name, ref_id in ref_list:
1359 1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1360 1360 group_refs.append((ref_key, ref_name))
1361 1361
1362 1362 if not selected:
1363 1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1364 1364 selected = ref_key
1365 1365
1366 1366 if group_refs:
1367 1367 groups.append((group_refs, group_name))
1368 1368
1369 1369 if not selected:
1370 1370 ref = commit_id or branch or bookmark
1371 1371 if ref:
1372 1372 raise CommitDoesNotExistError(
1373 1373 'No commit refs could be found matching: %s' % ref)
1374 1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1375 1375 selected = 'branch:%s:%s' % (
1376 1376 repo.DEFAULT_BRANCH_NAME,
1377 1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1378 1378 )
1379 1379 elif repo.commit_ids:
1380 1380 rev = repo.commit_ids[0]
1381 1381 selected = 'rev:%s:%s' % (rev, rev)
1382 1382 else:
1383 1383 raise EmptyRepositoryError()
1384 1384 return groups, selected
1385 1385
1386 1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1387 1387 return self._get_diff_from_pr_or_version(
1388 1388 source_repo, source_ref_id, target_ref_id, context=context)
1389 1389
1390 1390 def _get_diff_from_pr_or_version(
1391 1391 self, source_repo, source_ref_id, target_ref_id, context):
1392 1392 target_commit = source_repo.get_commit(
1393 1393 commit_id=safe_str(target_ref_id))
1394 1394 source_commit = source_repo.get_commit(
1395 1395 commit_id=safe_str(source_ref_id))
1396 1396 if isinstance(source_repo, Repository):
1397 1397 vcs_repo = source_repo.scm_instance()
1398 1398 else:
1399 1399 vcs_repo = source_repo
1400 1400
1401 1401 # TODO: johbo: In the context of an update, we cannot reach
1402 1402 # the old commit anymore with our normal mechanisms. It needs
1403 1403 # some sort of special support in the vcs layer to avoid this
1404 1404 # workaround.
1405 1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1406 1406 vcs_repo.alias == 'git'):
1407 1407 source_commit.raw_id = safe_str(source_ref_id)
1408 1408
1409 1409 log.debug('calculating diff between '
1410 1410 'source_ref:%s and target_ref:%s for repo `%s`',
1411 1411 target_ref_id, source_ref_id,
1412 1412 safe_unicode(vcs_repo.path))
1413 1413
1414 1414 vcs_diff = vcs_repo.get_diff(
1415 1415 commit1=target_commit, commit2=source_commit, context=context)
1416 1416 return vcs_diff
1417 1417
1418 1418 def _is_merge_enabled(self, pull_request):
1419 1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1420 1420 settings = settings_model.get_general_settings()
1421 1421 return settings.get('rhodecode_pr_merge_enabled', False)
1422 1422
1423 1423 def _use_rebase_for_merging(self, pull_request):
1424 1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1425 1425 settings = settings_model.get_general_settings()
1426 1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1427 1427
1428 1428 def _log_audit_action(self, action, action_data, user, pull_request):
1429 1429 audit_logger.store(
1430 1430 action=action,
1431 1431 action_data=action_data,
1432 1432 user=user,
1433 1433 repo=pull_request.target_repo)
1434 1434
1435 1435 def get_reviewer_functions(self):
1436 1436 """
1437 1437 Fetches functions for validation and fetching default reviewers.
1438 1438 If available we use the EE package, else we fallback to CE
1439 1439 package functions
1440 1440 """
1441 1441 try:
1442 1442 from rc_reviewers.utils import get_default_reviewers_data
1443 1443 from rc_reviewers.utils import validate_default_reviewers
1444 1444 except ImportError:
1445 1445 from rhodecode.apps.repository.utils import \
1446 1446 get_default_reviewers_data
1447 1447 from rhodecode.apps.repository.utils import \
1448 1448 validate_default_reviewers
1449 1449
1450 1450 return get_default_reviewers_data, validate_default_reviewers
1451 1451
1452 1452
1453 1453 class MergeCheck(object):
1454 1454 """
1455 1455 Perform Merge Checks and returns a check object which stores information
1456 1456 about merge errors, and merge conditions
1457 1457 """
1458 1458 TODO_CHECK = 'todo'
1459 1459 PERM_CHECK = 'perm'
1460 1460 REVIEW_CHECK = 'review'
1461 1461 MERGE_CHECK = 'merge'
1462 1462
1463 1463 def __init__(self):
1464 1464 self.review_status = None
1465 1465 self.merge_possible = None
1466 1466 self.merge_msg = ''
1467 1467 self.failed = None
1468 1468 self.errors = []
1469 1469 self.error_details = OrderedDict()
1470 1470
1471 1471 def push_error(self, error_type, message, error_key, details):
1472 1472 self.failed = True
1473 1473 self.errors.append([error_type, message])
1474 1474 self.error_details[error_key] = dict(
1475 1475 details=details,
1476 1476 error_type=error_type,
1477 1477 message=message
1478 1478 )
1479 1479
1480 1480 @classmethod
1481 1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1482 1482 # if migrated to pyramid...
1483 1483 # _ = lambda: translator or _ # use passed in translator if any
1484 1484
1485 1485 merge_check = cls()
1486 1486
1487 1487 # permissions to merge
1488 1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1489 1489 pull_request, user)
1490 1490 if not user_allowed_to_merge:
1491 1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1492 1492
1493 1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1494 1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1495 1495 if fail_early:
1496 1496 return merge_check
1497 1497
1498 1498 # review status, must be always present
1499 1499 review_status = pull_request.calculated_review_status()
1500 1500 merge_check.review_status = review_status
1501 1501
1502 1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1503 1503 if not status_approved:
1504 1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1505 1505
1506 1506 msg = _('Pull request reviewer approval is pending.')
1507 1507
1508 1508 merge_check.push_error(
1509 1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1510 1510
1511 1511 if fail_early:
1512 1512 return merge_check
1513 1513
1514 1514 # left over TODOs
1515 1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1516 1516 if todos:
1517 1517 log.debug("MergeCheck: cannot merge, {} "
1518 1518 "unresolved todos left.".format(len(todos)))
1519 1519
1520 1520 if len(todos) == 1:
1521 1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1522 1522 len(todos))
1523 1523 else:
1524 1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1525 1525 len(todos))
1526 1526
1527 1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1528 1528
1529 1529 if fail_early:
1530 1530 return merge_check
1531 1531
1532 1532 # merge possible
1533 1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1534 1534 merge_check.merge_possible = merge_status
1535 1535 merge_check.merge_msg = msg
1536 1536 if not merge_status:
1537 1537 log.debug(
1538 1538 "MergeCheck: cannot merge, pull request merge not possible.")
1539 1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1540 1540
1541 1541 if fail_early:
1542 1542 return merge_check
1543 1543
1544 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1544 1545 return merge_check
1545 1546
1546 1547
1547 1548 ChangeTuple = namedtuple('ChangeTuple',
1548 1549 ['added', 'common', 'removed', 'total'])
1549 1550
1550 1551 FileChangeTuple = namedtuple('FileChangeTuple',
1551 1552 ['added', 'modified', 'removed'])
@@ -1,217 +1,218 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('new_repo', '/_admin/create_repository', []);
16 16 pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']);
17 17 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
18 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
19 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
20 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
21 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
22 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
23 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
24 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
25 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
26 18 pyroutes.register('favicon', '/favicon.ico', []);
27 19 pyroutes.register('robots', '/robots.txt', []);
28 20 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
29 21 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
30 22 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
31 23 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
32 24 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
33 25 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
34 26 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/settings/integrations', ['repo_group_name']);
35 27 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
36 28 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/settings/integrations/new', ['repo_group_name']);
37 29 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
38 30 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
39 31 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
40 32 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
41 33 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
42 34 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
43 35 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
44 36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
45 37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
46 38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
47 39 pyroutes.register('admin_home', '/_admin', []);
48 40 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
49 41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
50 42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
51 43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
52 44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
53 45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
54 46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
55 47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
56 48 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
57 49 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
58 50 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
59 51 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
60 52 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
61 53 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
62 54 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
63 55 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
64 56 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
65 57 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
66 58 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
67 59 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
68 60 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
69 61 pyroutes.register('users', '/_admin/users', []);
70 62 pyroutes.register('users_data', '/_admin/users_data', []);
71 63 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
72 64 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
73 65 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
74 66 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
75 67 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
76 68 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
77 69 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
78 70 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
79 71 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
80 72 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
81 73 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
82 74 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
83 75 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
84 76 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
85 77 pyroutes.register('channelstream_proxy', '/_channelstream', []);
86 78 pyroutes.register('login', '/_admin/login', []);
87 79 pyroutes.register('logout', '/_admin/logout', []);
88 80 pyroutes.register('register', '/_admin/register', []);
89 81 pyroutes.register('reset_password', '/_admin/password_reset', []);
90 82 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
91 83 pyroutes.register('home', '/', []);
92 84 pyroutes.register('user_autocomplete_data', '/_users', []);
93 85 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
94 86 pyroutes.register('repo_list_data', '/_repos', []);
95 87 pyroutes.register('goto_switcher_data', '/_goto_data', []);
96 88 pyroutes.register('journal', '/_admin/journal', []);
97 89 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
98 90 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
99 91 pyroutes.register('journal_public', '/_admin/public_journal', []);
100 92 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
101 93 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
102 94 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
103 95 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
104 96 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
105 97 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
106 98 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
107 99 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
108 100 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
109 101 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
110 102 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
111 103 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
112 104 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
113 105 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
114 106 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
115 107 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
116 108 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
117 109 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
118 110 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
119 111 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
120 112 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
121 113 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
122 114 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
123 115 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
124 116 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
125 117 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
126 118 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
127 119 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
128 120 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
129 121 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
130 122 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
131 123 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
132 124 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
133 125 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
134 126 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
135 127 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
136 128 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
137 129 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
138 130 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
139 131 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
140 132 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
141 133 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
142 134 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
143 135 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
144 136 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
145 137 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
146 138 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
147 139 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
148 140 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
149 141 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
150 142 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
151 143 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
152 144 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
153 145 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
154 146 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
147 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
148 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
149 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
150 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
151 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
152 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
153 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
154 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
155 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
155 156 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
156 157 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
157 158 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
158 159 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
159 160 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
160 161 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
161 162 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
162 163 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
163 164 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
164 165 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
165 166 pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
166 167 pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
167 168 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
168 169 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
169 170 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
170 171 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
171 172 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
172 173 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
173 174 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
174 175 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
175 176 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
176 177 pyroutes.register('search', '/_admin/search', []);
177 178 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
178 179 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
179 180 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
180 181 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
181 182 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
182 183 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
183 184 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
184 185 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
185 186 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
186 187 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
187 188 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
188 189 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
189 190 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
190 191 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
191 192 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
192 193 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
193 194 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
194 195 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
195 196 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
196 197 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
197 198 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
198 199 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
199 200 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
200 201 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
201 202 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
202 203 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
203 204 pyroutes.register('gists_show', '/_admin/gists', []);
204 205 pyroutes.register('gists_new', '/_admin/gists/new', []);
205 206 pyroutes.register('gists_create', '/_admin/gists/create', []);
206 207 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
207 208 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
208 209 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
209 210 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
210 211 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
211 212 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
212 213 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
213 214 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
214 215 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
215 216 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
216 217 pyroutes.register('apiv2', '/_admin/api', []);
217 218 }
@@ -1,831 +1,831 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45 /* Comment form for main and inline comments */
46 46 (function(mod) {
47 47
48 48 if (typeof exports == "object" && typeof module == "object") {
49 49 // CommonJS
50 50 module.exports = mod();
51 51 }
52 52 else {
53 53 // Plain browser env
54 54 (this || window).CommentForm = mod();
55 55 }
56 56
57 57 })(function() {
58 58 "use strict";
59 59
60 60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 61 if (!(this instanceof CommentForm)) {
62 62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 63 }
64 64
65 65 // bind the element instance to our Form
66 66 $(formElement).get(0).CommentForm = this;
67 67
68 68 this.withLineNo = function(selector) {
69 69 var lineNo = this.lineNo;
70 70 if (lineNo === undefined) {
71 71 return selector
72 72 } else {
73 73 return selector + '_' + lineNo;
74 74 }
75 75 };
76 76
77 77 this.commitId = commitId;
78 78 this.pullRequestId = pullRequestId;
79 79 this.lineNo = lineNo;
80 80 this.initAutocompleteActions = initAutocompleteActions;
81 81
82 82 this.previewButton = this.withLineNo('#preview-btn');
83 83 this.previewContainer = this.withLineNo('#preview-container');
84 84
85 85 this.previewBoxSelector = this.withLineNo('#preview-box');
86 86
87 87 this.editButton = this.withLineNo('#edit-btn');
88 88 this.editContainer = this.withLineNo('#edit-container');
89 89 this.cancelButton = this.withLineNo('#cancel-btn');
90 90 this.commentType = this.withLineNo('#comment_type');
91 91
92 92 this.resolvesId = null;
93 93 this.resolvesActionId = null;
94 94
95 95 this.closesPr = '#close_pull_request';
96 96
97 97 this.cmBox = this.withLineNo('#text');
98 98 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
99 99
100 100 this.statusChange = this.withLineNo('#change_status');
101 101
102 102 this.submitForm = formElement;
103 103 this.submitButton = $(this.submitForm).find('input[type="submit"]');
104 104 this.submitButtonText = this.submitButton.val();
105 105
106 106 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
107 107 {'repo_name': templateContext.repo_name,
108 108 'commit_id': templateContext.commit_data.commit_id});
109 109
110 110 if (resolvesCommentId){
111 111 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
112 112 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
113 113 $(this.commentType).prop('disabled', true);
114 114 $(this.commentType).addClass('disabled');
115 115
116 116 // disable select
117 117 setTimeout(function() {
118 118 $(self.statusChange).select2('readonly', true);
119 119 }, 10);
120 120
121 121 var resolvedInfo = (
122 122 '<li class="resolve-action">' +
123 123 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
124 124 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
125 125 '</li>'
126 126 ).format(resolvesCommentId, _gettext('resolve comment'));
127 127 $(resolvedInfo).insertAfter($(this.commentType).parent());
128 128 }
129 129
130 130 // based on commitId, or pullRequestId decide where do we submit
131 131 // out data
132 132 if (this.commitId){
133 133 this.submitUrl = pyroutes.url('repo_commit_comment_create',
134 134 {'repo_name': templateContext.repo_name,
135 135 'commit_id': this.commitId});
136 136 this.selfUrl = pyroutes.url('repo_commit',
137 137 {'repo_name': templateContext.repo_name,
138 138 'commit_id': this.commitId});
139 139
140 140 } else if (this.pullRequestId) {
141 this.submitUrl = pyroutes.url('pullrequest_comment',
141 this.submitUrl = pyroutes.url('pullrequest_comment_create',
142 142 {'repo_name': templateContext.repo_name,
143 143 'pull_request_id': this.pullRequestId});
144 144 this.selfUrl = pyroutes.url('pullrequest_show',
145 145 {'repo_name': templateContext.repo_name,
146 146 'pull_request_id': this.pullRequestId});
147 147
148 148 } else {
149 149 throw new Error(
150 150 'CommentForm requires pullRequestId, or commitId to be specified.')
151 151 }
152 152
153 153 // FUNCTIONS and helpers
154 154 var self = this;
155 155
156 156 this.isInline = function(){
157 157 return this.lineNo && this.lineNo != 'general';
158 158 };
159 159
160 160 this.getCmInstance = function(){
161 161 return this.cm
162 162 };
163 163
164 164 this.setPlaceholder = function(placeholder) {
165 165 var cm = this.getCmInstance();
166 166 if (cm){
167 167 cm.setOption('placeholder', placeholder);
168 168 }
169 169 };
170 170
171 171 this.getCommentStatus = function() {
172 172 return $(this.submitForm).find(this.statusChange).val();
173 173 };
174 174 this.getCommentType = function() {
175 175 return $(this.submitForm).find(this.commentType).val();
176 176 };
177 177
178 178 this.getResolvesId = function() {
179 179 return $(this.submitForm).find(this.resolvesId).val() || null;
180 180 };
181 181
182 182 this.getClosePr = function() {
183 183 return $(this.submitForm).find(this.closesPr).val() || null;
184 184 };
185 185
186 186 this.markCommentResolved = function(resolvedCommentId){
187 187 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
188 188 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
189 189 };
190 190
191 191 this.isAllowedToSubmit = function() {
192 192 return !$(this.submitButton).prop('disabled');
193 193 };
194 194
195 195 this.initStatusChangeSelector = function(){
196 196 var formatChangeStatus = function(state, escapeMarkup) {
197 197 var originalOption = state.element;
198 198 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
199 199 '<span>' + escapeMarkup(state.text) + '</span>';
200 200 };
201 201 var formatResult = function(result, container, query, escapeMarkup) {
202 202 return formatChangeStatus(result, escapeMarkup);
203 203 };
204 204
205 205 var formatSelection = function(data, container, escapeMarkup) {
206 206 return formatChangeStatus(data, escapeMarkup);
207 207 };
208 208
209 209 $(this.submitForm).find(this.statusChange).select2({
210 210 placeholder: _gettext('Status Review'),
211 211 formatResult: formatResult,
212 212 formatSelection: formatSelection,
213 213 containerCssClass: "drop-menu status_box_menu",
214 214 dropdownCssClass: "drop-menu-dropdown",
215 215 dropdownAutoWidth: true,
216 216 minimumResultsForSearch: -1
217 217 });
218 218 $(this.submitForm).find(this.statusChange).on('change', function() {
219 219 var status = self.getCommentStatus();
220 220
221 221 if (status && !self.isInline()) {
222 222 $(self.submitButton).prop('disabled', false);
223 223 }
224 224
225 225 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
226 226 self.setPlaceholder(placeholderText)
227 227 })
228 228 };
229 229
230 230 // reset the comment form into it's original state
231 231 this.resetCommentFormState = function(content) {
232 232 content = content || '';
233 233
234 234 $(this.editContainer).show();
235 235 $(this.editButton).parent().addClass('active');
236 236
237 237 $(this.previewContainer).hide();
238 238 $(this.previewButton).parent().removeClass('active');
239 239
240 240 this.setActionButtonsDisabled(true);
241 241 self.cm.setValue(content);
242 242 self.cm.setOption("readOnly", false);
243 243
244 244 if (this.resolvesId) {
245 245 // destroy the resolve action
246 246 $(this.resolvesId).parent().remove();
247 247 }
248 248 // reset closingPR flag
249 249 $('.close-pr-input').remove();
250 250
251 251 $(this.statusChange).select2('readonly', false);
252 252 };
253 253
254 254 this.globalSubmitSuccessCallback = function(){
255 255 // default behaviour is to call GLOBAL hook, if it's registered.
256 256 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
257 257 commentFormGlobalSubmitSuccessCallback()
258 258 }
259 259 };
260 260
261 261 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
262 262 failHandler = failHandler || function() {};
263 263 var postData = toQueryString(postData);
264 264 var request = $.ajax({
265 265 url: url,
266 266 type: 'POST',
267 267 data: postData,
268 268 headers: {'X-PARTIAL-XHR': true}
269 269 })
270 270 .done(function(data) {
271 271 successHandler(data);
272 272 })
273 273 .fail(function(data, textStatus, errorThrown){
274 274 alert(
275 275 "Error while submitting comment.\n" +
276 276 "Error code {0} ({1}).".format(data.status, data.statusText));
277 277 failHandler()
278 278 });
279 279 return request;
280 280 };
281 281
282 282 // overwrite a submitHandler, we need to do it for inline comments
283 283 this.setHandleFormSubmit = function(callback) {
284 284 this.handleFormSubmit = callback;
285 285 };
286 286
287 287 // overwrite a submitSuccessHandler
288 288 this.setGlobalSubmitSuccessCallback = function(callback) {
289 289 this.globalSubmitSuccessCallback = callback;
290 290 };
291 291
292 292 // default handler for for submit for main comments
293 293 this.handleFormSubmit = function() {
294 294 var text = self.cm.getValue();
295 295 var status = self.getCommentStatus();
296 296 var commentType = self.getCommentType();
297 297 var resolvesCommentId = self.getResolvesId();
298 298 var closePullRequest = self.getClosePr();
299 299
300 300 if (text === "" && !status) {
301 301 return;
302 302 }
303 303
304 304 var excludeCancelBtn = false;
305 305 var submitEvent = true;
306 306 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
307 307 self.cm.setOption("readOnly", true);
308 308
309 309 var postData = {
310 310 'text': text,
311 311 'changeset_status': status,
312 312 'comment_type': commentType,
313 313 'csrf_token': CSRF_TOKEN
314 314 };
315 315
316 316 if (resolvesCommentId) {
317 317 postData['resolves_comment_id'] = resolvesCommentId;
318 318 }
319 319
320 320 if (closePullRequest) {
321 321 postData['close_pull_request'] = true;
322 322 }
323 323
324 324 var submitSuccessCallback = function(o) {
325 325 // reload page if we change status for single commit.
326 326 if (status && self.commitId) {
327 327 location.reload(true);
328 328 } else {
329 329 $('#injected_page_comments').append(o.rendered_text);
330 330 self.resetCommentFormState();
331 331 timeagoActivate();
332 332
333 333 // mark visually which comment was resolved
334 334 if (resolvesCommentId) {
335 335 self.markCommentResolved(resolvesCommentId);
336 336 }
337 337 }
338 338
339 339 // run global callback on submit
340 340 self.globalSubmitSuccessCallback();
341 341
342 342 };
343 343 var submitFailCallback = function(){
344 344 self.resetCommentFormState(text);
345 345 };
346 346 self.submitAjaxPOST(
347 347 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
348 348 };
349 349
350 350 this.previewSuccessCallback = function(o) {
351 351 $(self.previewBoxSelector).html(o);
352 352 $(self.previewBoxSelector).removeClass('unloaded');
353 353
354 354 // swap buttons, making preview active
355 355 $(self.previewButton).parent().addClass('active');
356 356 $(self.editButton).parent().removeClass('active');
357 357
358 358 // unlock buttons
359 359 self.setActionButtonsDisabled(false);
360 360 };
361 361
362 362 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
363 363 excludeCancelBtn = excludeCancelBtn || false;
364 364 submitEvent = submitEvent || false;
365 365
366 366 $(this.editButton).prop('disabled', state);
367 367 $(this.previewButton).prop('disabled', state);
368 368
369 369 if (!excludeCancelBtn) {
370 370 $(this.cancelButton).prop('disabled', state);
371 371 }
372 372
373 373 var submitState = state;
374 374 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
375 375 // if the value of commit review status is set, we allow
376 376 // submit button, but only on Main form, isInline means inline
377 377 submitState = false
378 378 }
379 379
380 380 $(this.submitButton).prop('disabled', submitState);
381 381 if (submitEvent) {
382 382 $(this.submitButton).val(_gettext('Submitting...'));
383 383 } else {
384 384 $(this.submitButton).val(this.submitButtonText);
385 385 }
386 386
387 387 };
388 388
389 389 // lock preview/edit/submit buttons on load, but exclude cancel button
390 390 var excludeCancelBtn = true;
391 391 this.setActionButtonsDisabled(true, excludeCancelBtn);
392 392
393 393 // anonymous users don't have access to initialized CM instance
394 394 if (this.cm !== undefined){
395 395 this.cm.on('change', function(cMirror) {
396 396 if (cMirror.getValue() === "") {
397 397 self.setActionButtonsDisabled(true, excludeCancelBtn)
398 398 } else {
399 399 self.setActionButtonsDisabled(false, excludeCancelBtn)
400 400 }
401 401 });
402 402 }
403 403
404 404 $(this.editButton).on('click', function(e) {
405 405 e.preventDefault();
406 406
407 407 $(self.previewButton).parent().removeClass('active');
408 408 $(self.previewContainer).hide();
409 409
410 410 $(self.editButton).parent().addClass('active');
411 411 $(self.editContainer).show();
412 412
413 413 });
414 414
415 415 $(this.previewButton).on('click', function(e) {
416 416 e.preventDefault();
417 417 var text = self.cm.getValue();
418 418
419 419 if (text === "") {
420 420 return;
421 421 }
422 422
423 423 var postData = {
424 424 'text': text,
425 425 'renderer': templateContext.visual.default_renderer,
426 426 'csrf_token': CSRF_TOKEN
427 427 };
428 428
429 429 // lock ALL buttons on preview
430 430 self.setActionButtonsDisabled(true);
431 431
432 432 $(self.previewBoxSelector).addClass('unloaded');
433 433 $(self.previewBoxSelector).html(_gettext('Loading ...'));
434 434
435 435 $(self.editContainer).hide();
436 436 $(self.previewContainer).show();
437 437
438 438 // by default we reset state of comment preserving the text
439 439 var previewFailCallback = function(){
440 440 self.resetCommentFormState(text)
441 441 };
442 442 self.submitAjaxPOST(
443 443 self.previewUrl, postData, self.previewSuccessCallback,
444 444 previewFailCallback);
445 445
446 446 $(self.previewButton).parent().addClass('active');
447 447 $(self.editButton).parent().removeClass('active');
448 448 });
449 449
450 450 $(this.submitForm).submit(function(e) {
451 451 e.preventDefault();
452 452 var allowedToSubmit = self.isAllowedToSubmit();
453 453 if (!allowedToSubmit){
454 454 return false;
455 455 }
456 456 self.handleFormSubmit();
457 457 });
458 458
459 459 }
460 460
461 461 return CommentForm;
462 462 });
463 463
464 464 /* comments controller */
465 465 var CommentsController = function() {
466 466 var mainComment = '#text';
467 467 var self = this;
468 468
469 469 this.cancelComment = function(node) {
470 470 var $node = $(node);
471 471 var $td = $node.closest('td');
472 472 $node.closest('.comment-inline-form').remove();
473 473 return false;
474 474 };
475 475
476 476 this.getLineNumber = function(node) {
477 477 var $node = $(node);
478 478 return $node.closest('td').attr('data-line-number');
479 479 };
480 480
481 481 this.scrollToComment = function(node, offset, outdated) {
482 482 if (offset === undefined) {
483 483 offset = 0;
484 484 }
485 485 var outdated = outdated || false;
486 486 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
487 487
488 488 if (!node) {
489 489 node = $('.comment-selected');
490 490 if (!node.length) {
491 491 node = $('comment-current')
492 492 }
493 493 }
494 494 $wrapper = $(node).closest('div.comment');
495 495 $comment = $(node).closest(klass);
496 496 $comments = $(klass);
497 497
498 498 // show hidden comment when referenced.
499 499 if (!$wrapper.is(':visible')){
500 500 $wrapper.show();
501 501 }
502 502
503 503 $('.comment-selected').removeClass('comment-selected');
504 504
505 505 var nextIdx = $(klass).index($comment) + offset;
506 506 if (nextIdx >= $comments.length) {
507 507 nextIdx = 0;
508 508 }
509 509 var $next = $(klass).eq(nextIdx);
510 510
511 511 var $cb = $next.closest('.cb');
512 512 $cb.removeClass('cb-collapsed');
513 513
514 514 var $filediffCollapseState = $cb.closest('.filediff').prev();
515 515 $filediffCollapseState.prop('checked', false);
516 516 $next.addClass('comment-selected');
517 517 scrollToElement($next);
518 518 return false;
519 519 };
520 520
521 521 this.nextComment = function(node) {
522 522 return self.scrollToComment(node, 1);
523 523 };
524 524
525 525 this.prevComment = function(node) {
526 526 return self.scrollToComment(node, -1);
527 527 };
528 528
529 529 this.nextOutdatedComment = function(node) {
530 530 return self.scrollToComment(node, 1, true);
531 531 };
532 532
533 533 this.prevOutdatedComment = function(node) {
534 534 return self.scrollToComment(node, -1, true);
535 535 };
536 536
537 537 this.deleteComment = function(node) {
538 538 if (!confirm(_gettext('Delete this comment?'))) {
539 539 return false;
540 540 }
541 541 var $node = $(node);
542 542 var $td = $node.closest('td');
543 543 var $comment = $node.closest('.comment');
544 544 var comment_id = $comment.attr('data-comment-id');
545 545 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
546 546 var postData = {
547 547 '_method': 'delete',
548 548 'csrf_token': CSRF_TOKEN
549 549 };
550 550
551 551 $comment.addClass('comment-deleting');
552 552 $comment.hide('fast');
553 553
554 554 var success = function(response) {
555 555 $comment.remove();
556 556 return false;
557 557 };
558 558 var failure = function(data, textStatus, xhr) {
559 559 alert("error processing request: " + textStatus);
560 560 $comment.show('fast');
561 561 $comment.removeClass('comment-deleting');
562 562 return false;
563 563 };
564 564 ajaxPOST(url, postData, success, failure);
565 565 };
566 566
567 567 this.toggleWideMode = function (node) {
568 568 if ($('#content').hasClass('wrapper')) {
569 569 $('#content').removeClass("wrapper");
570 570 $('#content').addClass("wide-mode-wrapper");
571 571 $(node).addClass('btn-success');
572 572 } else {
573 573 $('#content').removeClass("wide-mode-wrapper");
574 574 $('#content').addClass("wrapper");
575 575 $(node).removeClass('btn-success');
576 576 }
577 577 return false;
578 578 };
579 579
580 580 this.toggleComments = function(node, show) {
581 581 var $filediff = $(node).closest('.filediff');
582 582 if (show === true) {
583 583 $filediff.removeClass('hide-comments');
584 584 } else if (show === false) {
585 585 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
586 586 $filediff.addClass('hide-comments');
587 587 } else {
588 588 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
589 589 $filediff.toggleClass('hide-comments');
590 590 }
591 591 return false;
592 592 };
593 593
594 594 this.toggleLineComments = function(node) {
595 595 self.toggleComments(node, true);
596 596 var $node = $(node);
597 597 $node.closest('tr').toggleClass('hide-line-comments');
598 598 };
599 599
600 600 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
601 601 var pullRequestId = templateContext.pull_request_data.pull_request_id;
602 602 var commitId = templateContext.commit_data.commit_id;
603 603
604 604 var commentForm = new CommentForm(
605 605 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
606 606 var cm = commentForm.getCmInstance();
607 607
608 608 if (resolvesCommentId){
609 609 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
610 610 }
611 611
612 612 setTimeout(function() {
613 613 // callbacks
614 614 if (cm !== undefined) {
615 615 commentForm.setPlaceholder(placeholderText);
616 616 if (commentForm.isInline()) {
617 617 cm.focus();
618 618 cm.refresh();
619 619 }
620 620 }
621 621 }, 10);
622 622
623 623 // trigger scrolldown to the resolve comment, since it might be away
624 624 // from the clicked
625 625 if (resolvesCommentId){
626 626 var actionNode = $(commentForm.resolvesActionId).offset();
627 627
628 628 setTimeout(function() {
629 629 if (actionNode) {
630 630 $('body, html').animate({scrollTop: actionNode.top}, 10);
631 631 }
632 632 }, 100);
633 633 }
634 634
635 635 return commentForm;
636 636 };
637 637
638 638 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
639 639
640 640 var tmpl = $('#cb-comment-general-form-template').html();
641 641 tmpl = tmpl.format(null, 'general');
642 642 var $form = $(tmpl);
643 643
644 644 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
645 645 var curForm = $formPlaceholder.find('form');
646 646 if (curForm){
647 647 curForm.remove();
648 648 }
649 649 $formPlaceholder.append($form);
650 650
651 651 var _form = $($form[0]);
652 652 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
653 653 var commentForm = this.createCommentForm(
654 654 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
655 655 commentForm.initStatusChangeSelector();
656 656
657 657 return commentForm;
658 658 };
659 659
660 660 this.createComment = function(node, resolutionComment) {
661 661 var resolvesCommentId = resolutionComment || null;
662 662 var $node = $(node);
663 663 var $td = $node.closest('td');
664 664 var $form = $td.find('.comment-inline-form');
665 665
666 666 if (!$form.length) {
667 667
668 668 var $filediff = $node.closest('.filediff');
669 669 $filediff.removeClass('hide-comments');
670 670 var f_path = $filediff.attr('data-f-path');
671 671 var lineno = self.getLineNumber(node);
672 672 // create a new HTML from template
673 673 var tmpl = $('#cb-comment-inline-form-template').html();
674 674 tmpl = tmpl.format(f_path, lineno);
675 675 $form = $(tmpl);
676 676
677 677 var $comments = $td.find('.inline-comments');
678 678 if (!$comments.length) {
679 679 $comments = $(
680 680 $('#cb-comments-inline-container-template').html());
681 681 $td.append($comments);
682 682 }
683 683
684 684 $td.find('.cb-comment-add-button').before($form);
685 685
686 686 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
687 687 var _form = $($form[0]).find('form');
688 688 var autocompleteActions = ['as_note', 'as_todo'];
689 689 var commentForm = this.createCommentForm(
690 690 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
691 691
692 692 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
693 693 form: _form,
694 694 parent: $td[0],
695 695 lineno: lineno,
696 696 f_path: f_path}
697 697 );
698 698
699 699 // set a CUSTOM submit handler for inline comments.
700 700 commentForm.setHandleFormSubmit(function(o) {
701 701 var text = commentForm.cm.getValue();
702 702 var commentType = commentForm.getCommentType();
703 703 var resolvesCommentId = commentForm.getResolvesId();
704 704
705 705 if (text === "") {
706 706 return;
707 707 }
708 708
709 709 if (lineno === undefined) {
710 710 alert('missing line !');
711 711 return;
712 712 }
713 713 if (f_path === undefined) {
714 714 alert('missing file path !');
715 715 return;
716 716 }
717 717
718 718 var excludeCancelBtn = false;
719 719 var submitEvent = true;
720 720 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
721 721 commentForm.cm.setOption("readOnly", true);
722 722 var postData = {
723 723 'text': text,
724 724 'f_path': f_path,
725 725 'line': lineno,
726 726 'comment_type': commentType,
727 727 'csrf_token': CSRF_TOKEN
728 728 };
729 729 if (resolvesCommentId){
730 730 postData['resolves_comment_id'] = resolvesCommentId;
731 731 }
732 732
733 733 var submitSuccessCallback = function(json_data) {
734 734 $form.remove();
735 735 try {
736 736 var html = json_data.rendered_text;
737 737 var lineno = json_data.line_no;
738 738 var target_id = json_data.target_id;
739 739
740 740 $comments.find('.cb-comment-add-button').before(html);
741 741
742 742 //mark visually which comment was resolved
743 743 if (resolvesCommentId) {
744 744 commentForm.markCommentResolved(resolvesCommentId);
745 745 }
746 746
747 747 // run global callback on submit
748 748 commentForm.globalSubmitSuccessCallback();
749 749
750 750 } catch (e) {
751 751 console.error(e);
752 752 }
753 753
754 754 // re trigger the linkification of next/prev navigation
755 755 linkifyComments($('.inline-comment-injected'));
756 756 timeagoActivate();
757 757 commentForm.setActionButtonsDisabled(false);
758 758
759 759 };
760 760 var submitFailCallback = function(){
761 761 commentForm.resetCommentFormState(text)
762 762 };
763 763 commentForm.submitAjaxPOST(
764 764 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
765 765 });
766 766 }
767 767
768 768 $form.addClass('comment-inline-form-open');
769 769 };
770 770
771 771 this.createResolutionComment = function(commentId){
772 772 // hide the trigger text
773 773 $('#resolve-comment-{0}'.format(commentId)).hide();
774 774
775 775 var comment = $('#comment-'+commentId);
776 776 var commentData = comment.data();
777 777 if (commentData.commentInline) {
778 778 this.createComment(comment, commentId)
779 779 } else {
780 780 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
781 781 }
782 782
783 783 return false;
784 784 };
785 785
786 786 this.submitResolution = function(commentId){
787 787 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
788 788 var commentForm = form.get(0).CommentForm;
789 789
790 790 var cm = commentForm.getCmInstance();
791 791 var renderer = templateContext.visual.default_renderer;
792 792 if (renderer == 'rst'){
793 793 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
794 794 } else if (renderer == 'markdown') {
795 795 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
796 796 } else {
797 797 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
798 798 }
799 799
800 800 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
801 801 form.submit();
802 802 return false;
803 803 };
804 804
805 805 this.renderInlineComments = function(file_comments) {
806 806 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
807 807
808 808 for (var i = 0; i < file_comments.length; i++) {
809 809 var box = file_comments[i];
810 810
811 811 var target_id = $(box).attr('target_id');
812 812
813 813 // actually comments with line numbers
814 814 var comments = box.children;
815 815
816 816 for (var j = 0; j < comments.length; j++) {
817 817 var data = {
818 818 'rendered_text': comments[j].outerHTML,
819 819 'line_no': $(comments[j]).attr('line'),
820 820 'target_id': target_id
821 821 };
822 822 }
823 823 }
824 824
825 825 // since order of injection is random, we're now re-iterating
826 826 // from correct order and filling in links
827 827 linkifyComments($('.inline-comment-injected'));
828 828 firefoxAnchorFix();
829 829 };
830 830
831 831 };
@@ -1,605 +1,604 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#save').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#save').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0){
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158 if (data.rules.use_code_authors_for_review) {
159 159 self.$rulesList.append(
160 160 self.addRule(
161 161 _gettext('Reviewers picked from source code changes.'))
162 162 )
163 163 }
164 164 if (data.rules.forbid_adding_reviewers) {
165 165 $('#add_reviewer_input').remove();
166 166 self.$rulesList.append(
167 167 self.addRule(
168 168 _gettext('Adding new reviewers is forbidden.'))
169 169 )
170 170 }
171 171 if (data.rules.forbid_author_to_review) {
172 172 self.forbidReviewUsers.push(data.rules_data.pr_author);
173 173 self.$rulesList.append(
174 174 self.addRule(
175 175 _gettext('Author is not allowed to be a reviewer.'))
176 176 )
177 177 }
178 178 if (data.rules.forbid_commit_author_to_review) {
179 179
180 180 if (data.rules_data.forbidden_users) {
181 181 $.each(data.rules_data.forbidden_users, function(index, member_data) {
182 182 self.forbidReviewUsers.push(member_data)
183 183 });
184 184
185 185 }
186 186
187 187 self.$rulesList.append(
188 188 self.addRule(
189 189 _gettext('Commit Authors are not allowed to be a reviewer.'))
190 190 )
191 191 }
192 192
193 193 return self.forbidReviewUsers
194 194 };
195 195
196 196 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
197 197
198 198 if (self.currentRequest) {
199 199 // make sure we cleanup old running requests before triggering this
200 200 // again
201 201 self.currentRequest.abort();
202 202 }
203 203
204 204 $('.calculate-reviewers').show();
205 205 // reset reviewer members
206 206 self.$reviewMembers.empty();
207 207
208 208 prButtonLock(true, null, 'reviewers');
209 209 $('#user').hide(); // hide user autocomplete before load
210 210
211 211 var url = pyroutes.url('repo_default_reviewers_data',
212 212 {
213 213 'repo_name': templateContext.repo_name,
214 214 'source_repo': sourceRepo,
215 215 'source_ref': sourceRef[2],
216 216 'target_repo': targetRepo,
217 217 'target_ref': targetRef[2]
218 218 });
219 219
220 220 self.currentRequest = $.get(url)
221 221 .done(function(data) {
222 222 self.currentRequest = null;
223 223
224 224 // review rules
225 225 self.loadReviewRules(data);
226 226
227 227 for (var i = 0; i < data.reviewers.length; i++) {
228 228 var reviewer = data.reviewers[i];
229 229 self.addReviewMember(
230 230 reviewer.user_id, reviewer.first_name,
231 231 reviewer.last_name, reviewer.username,
232 232 reviewer.gravatar_link, reviewer.reasons,
233 233 reviewer.mandatory);
234 234 }
235 235 $('.calculate-reviewers').hide();
236 236 prButtonLock(false, null, 'reviewers');
237 237 $('#user').show(); // show user autocomplete after load
238 238 });
239 239 };
240 240
241 241 // check those, refactor
242 242 this.removeReviewMember = function(reviewer_id, mark_delete) {
243 243 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
244 244
245 245 if(typeof(mark_delete) === undefined){
246 246 mark_delete = false;
247 247 }
248 248
249 249 if(mark_delete === true){
250 250 if (reviewer){
251 251 // now delete the input
252 252 $('#reviewer_{0} input'.format(reviewer_id)).remove();
253 253 // mark as to-delete
254 254 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
255 255 obj.addClass('to-delete');
256 256 obj.css({"text-decoration":"line-through", "opacity": 0.5});
257 257 }
258 258 }
259 259 else{
260 260 $('#reviewer_{0}'.format(reviewer_id)).remove();
261 261 }
262 262 };
263 263
264 264 this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) {
265 265 var members = self.$reviewMembers.get(0);
266 266 var reasons_html = '';
267 267 var reasons_inputs = '';
268 268 var reasons = reasons || [];
269 269 var mandatory = mandatory || false;
270 270
271 271 if (reasons) {
272 272 for (var i = 0; i < reasons.length; i++) {
273 273 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
274 274 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
275 275 }
276 276 }
277 277 var tmpl = '' +
278 278 '<li id="reviewer_{2}" class="reviewer_entry">'+
279 279 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
280 280 '<div class="reviewer_status">'+
281 281 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
282 282 '</div>'+
283 283 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
284 284 '<span class="reviewer_name user">{1}</span>'+
285 285 reasons_html +
286 286 '<input type="hidden" name="user_id" value="{2}">'+
287 287 '<input type="hidden" name="__start__" value="reasons:sequence">'+
288 288 '{3}'+
289 289 '<input type="hidden" name="__end__" value="reasons:sequence">';
290 290
291 291 if (mandatory) {
292 292 tmpl += ''+
293 293 '<div class="reviewer_member_mandatory_remove">' +
294 294 '<i class="icon-remove-sign"></i>'+
295 295 '</div>' +
296 296 '<input type="hidden" name="mandatory" value="true">'+
297 297 '<div class="reviewer_member_mandatory">' +
298 298 '<i class="icon-lock" title="Mandatory reviewer"></i>'+
299 299 '</div>';
300 300
301 301 } else {
302 302 tmpl += ''+
303 303 '<input type="hidden" name="mandatory" value="false">'+
304 304 '<div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember({2})">' +
305 305 '<i class="icon-remove-sign"></i>'+
306 306 '</div>';
307 307 }
308 308 // continue template
309 309 tmpl += ''+
310 310 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
311 311 '</li>' ;
312 312
313 313 var displayname = "{0} ({1} {2})".format(
314 314 nname, escapeHtml(fname), escapeHtml(lname));
315 315 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
316 316 // check if we don't have this ID already in
317 317 var ids = [];
318 318 var _els = self.$reviewMembers.find('li').toArray();
319 319 for (el in _els){
320 320 ids.push(_els[el].id)
321 321 }
322 322
323 323 var userAllowedReview = function(userId) {
324 324 var allowed = true;
325 325 $.each(self.forbidReviewUsers, function(index, member_data) {
326 326 if (parseInt(userId) === member_data['user_id']) {
327 327 allowed = false;
328 328 return false // breaks the loop
329 329 }
330 330 });
331 331 return allowed
332 332 };
333 333
334 334 var userAllowed = userAllowedReview(id);
335 335 if (!userAllowed){
336 336 alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname));
337 337 }
338 338 var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1;
339 339
340 340 if(shouldAdd) {
341 341 // only add if it's not there
342 342 members.innerHTML += element;
343 343 }
344 344
345 345 };
346 346
347 347 this.updateReviewers = function(repo_name, pull_request_id){
348 348 var postData = '_method=put&' + $('#reviewers input').serialize();
349 349 _updatePullRequest(repo_name, pull_request_id, postData);
350 350 };
351 351
352 352 };
353 353
354 354
355 355 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
356 356 var url = pyroutes.url(
357 357 'pullrequest_update',
358 358 {"repo_name": repo_name, "pull_request_id": pull_request_id});
359 359 if (typeof postData === 'string' ) {
360 360 postData += '&csrf_token=' + CSRF_TOKEN;
361 361 } else {
362 362 postData.csrf_token = CSRF_TOKEN;
363 363 }
364 364 var success = function(o) {
365 365 window.location.reload();
366 366 };
367 367 ajaxPOST(url, postData, success);
368 368 };
369 369
370 370 /**
371 371 * PULL REQUEST update commits
372 372 */
373 373 var updateCommits = function(repo_name, pull_request_id) {
374 374 var postData = {
375 375 '_method': 'put',
376 376 'update_commits': true};
377 377 _updatePullRequest(repo_name, pull_request_id, postData);
378 378 };
379 379
380 380
381 381 /**
382 382 * PULL REQUEST edit info
383 383 */
384 384 var editPullRequest = function(repo_name, pull_request_id, title, description) {
385 385 var url = pyroutes.url(
386 386 'pullrequest_update',
387 387 {"repo_name": repo_name, "pull_request_id": pull_request_id});
388 388
389 389 var postData = {
390 '_method': 'put',
391 390 'title': title,
392 391 'description': description,
393 392 'edit_pull_request': true,
394 393 'csrf_token': CSRF_TOKEN
395 394 };
396 395 var success = function(o) {
397 396 window.location.reload();
398 397 };
399 398 ajaxPOST(url, postData, success);
400 399 };
401 400
402 401 var initPullRequestsCodeMirror = function (textAreaId) {
403 402 var ta = $(textAreaId).get(0);
404 403 var initialHeight = '100px';
405 404
406 405 // default options
407 406 var codeMirrorOptions = {
408 407 mode: "text",
409 408 lineNumbers: false,
410 409 indentUnit: 4,
411 410 theme: 'rc-input'
412 411 };
413 412
414 413 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
415 414 // marker for manually set description
416 415 codeMirrorInstance._userDefinedDesc = false;
417 416 codeMirrorInstance.setSize(null, initialHeight);
418 417 codeMirrorInstance.on("change", function(instance, changeObj) {
419 418 var height = initialHeight;
420 419 var lines = instance.lineCount();
421 420 if (lines > 6 && lines < 20) {
422 421 height = "auto"
423 422 }
424 423 else if (lines >= 20) {
425 424 height = 20 * 15;
426 425 }
427 426 instance.setSize(null, height);
428 427
429 428 // detect if the change was trigger by auto desc, or user input
430 429 changeOrigin = changeObj.origin;
431 430
432 431 if (changeOrigin === "setValue") {
433 432 cmLog.debug('Change triggered by setValue');
434 433 }
435 434 else {
436 435 cmLog.debug('user triggered change !');
437 436 // set special marker to indicate user has created an input.
438 437 instance._userDefinedDesc = true;
439 438 }
440 439
441 440 });
442 441
443 442 return codeMirrorInstance
444 443 };
445 444
446 445 /**
447 446 * Reviewer autocomplete
448 447 */
449 448 var ReviewerAutoComplete = function(inputId) {
450 449 $(inputId).autocomplete({
451 450 serviceUrl: pyroutes.url('user_autocomplete_data'),
452 451 minChars:2,
453 452 maxHeight:400,
454 453 deferRequestBy: 300, //miliseconds
455 454 showNoSuggestionNotice: true,
456 455 tabDisabled: true,
457 456 autoSelectFirst: true,
458 457 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
459 458 formatResult: autocompleteFormatResult,
460 459 lookupFilter: autocompleteFilterResult,
461 460 onSelect: function(element, data) {
462 461
463 462 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
464 463 if (data.value_type == 'user_group') {
465 464 reasons.push(_gettext('member of "{0}"').format(data.value_display));
466 465
467 466 $.each(data.members, function(index, member_data) {
468 467 reviewersController.addReviewMember(
469 468 member_data.id, member_data.first_name, member_data.last_name,
470 469 member_data.username, member_data.icon_link, reasons);
471 470 })
472 471
473 472 } else {
474 473 reviewersController.addReviewMember(
475 474 data.id, data.first_name, data.last_name,
476 475 data.username, data.icon_link, reasons);
477 476 }
478 477
479 478 $(inputId).val('');
480 479 }
481 480 });
482 481 };
483 482
484 483
485 484 VersionController = function () {
486 485 var self = this;
487 486 this.$verSource = $('input[name=ver_source]');
488 487 this.$verTarget = $('input[name=ver_target]');
489 488 this.$showVersionDiff = $('#show-version-diff');
490 489
491 490 this.adjustRadioSelectors = function (curNode) {
492 491 var getVal = function (item) {
493 492 if (item == 'latest') {
494 493 return Number.MAX_SAFE_INTEGER
495 494 }
496 495 else {
497 496 return parseInt(item)
498 497 }
499 498 };
500 499
501 500 var curVal = getVal($(curNode).val());
502 501 var cleared = false;
503 502
504 503 $.each(self.$verSource, function (index, value) {
505 504 var elVal = getVal($(value).val());
506 505
507 506 if (elVal > curVal) {
508 507 if ($(value).is(':checked')) {
509 508 cleared = true;
510 509 }
511 510 $(value).attr('disabled', 'disabled');
512 511 $(value).removeAttr('checked');
513 512 $(value).css({'opacity': 0.1});
514 513 }
515 514 else {
516 515 $(value).css({'opacity': 1});
517 516 $(value).removeAttr('disabled');
518 517 }
519 518 });
520 519
521 520 if (cleared) {
522 521 // if we unchecked an active, set the next one to same loc.
523 522 $(this.$verSource).filter('[value={0}]'.format(
524 523 curVal)).attr('checked', 'checked');
525 524 }
526 525
527 526 self.setLockAction(false,
528 527 $(curNode).data('verPos'),
529 528 $(this.$verSource).filter(':checked').data('verPos')
530 529 );
531 530 };
532 531
533 532
534 533 this.attachVersionListener = function () {
535 534 self.$verTarget.change(function (e) {
536 535 self.adjustRadioSelectors(this)
537 536 });
538 537 self.$verSource.change(function (e) {
539 538 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
540 539 });
541 540 };
542 541
543 542 this.init = function () {
544 543
545 544 var curNode = self.$verTarget.filter(':checked');
546 545 self.adjustRadioSelectors(curNode);
547 546 self.setLockAction(true);
548 547 self.attachVersionListener();
549 548
550 549 };
551 550
552 551 this.setLockAction = function (state, selectedVersion, otherVersion) {
553 552 var $showVersionDiff = this.$showVersionDiff;
554 553
555 554 if (state) {
556 555 $showVersionDiff.attr('disabled', 'disabled');
557 556 $showVersionDiff.addClass('disabled');
558 557 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
559 558 }
560 559 else {
561 560 $showVersionDiff.removeAttr('disabled');
562 561 $showVersionDiff.removeClass('disabled');
563 562
564 563 if (selectedVersion == otherVersion) {
565 564 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
566 565 } else {
567 566 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
568 567 }
569 568 }
570 569
571 570 };
572 571
573 572 this.showVersionDiff = function () {
574 573 var target = self.$verTarget.filter(':checked');
575 574 var source = self.$verSource.filter(':checked');
576 575
577 576 if (target.val() && source.val()) {
578 577 var params = {
579 578 'pull_request_id': templateContext.pull_request_data.pull_request_id,
580 579 'repo_name': templateContext.repo_name,
581 580 'version': target.val(),
582 581 'from_version': source.val()
583 582 };
584 583 window.location = pyroutes.url('pullrequest_show', params)
585 584 }
586 585
587 586 return false;
588 587 };
589 588
590 589 this.toggleVersionView = function (elem) {
591 590
592 591 if (this.$showVersionDiff.is(':visible')) {
593 592 $('.version-pr').hide();
594 593 this.$showVersionDiff.hide();
595 594 $(elem).html($(elem).data('toggleOn'))
596 595 } else {
597 596 $('.version-pr').show();
598 597 this.$showVersionDiff.show();
599 598 $(elem).html($(elem).data('toggleOff'))
600 599 }
601 600
602 601 return false
603 602 }
604 603
605 604 }; No newline at end of file
@@ -1,609 +1,609 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <div class="outerwrapper">
5 5 <!-- HEADER -->
6 6 <div class="header">
7 7 <div id="header-inner" class="wrapper">
8 8 <div id="logo">
9 9 <div class="logo-wrapper">
10 10 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-216x60.png')}" alt="RhodeCode"/></a>
11 11 </div>
12 12 %if c.rhodecode_name:
13 13 <div class="branding">- ${h.branding(c.rhodecode_name)}</div>
14 14 %endif
15 15 </div>
16 16 <!-- MENU BAR NAV -->
17 17 ${self.menu_bar_nav()}
18 18 <!-- END MENU BAR NAV -->
19 19 </div>
20 20 </div>
21 21 ${self.menu_bar_subnav()}
22 22 <!-- END HEADER -->
23 23
24 24 <!-- CONTENT -->
25 25 <div id="content" class="wrapper">
26 26
27 27 <rhodecode-toast id="notifications"></rhodecode-toast>
28 28
29 29 <div class="main">
30 30 ${next.main()}
31 31 </div>
32 32 </div>
33 33 <!-- END CONTENT -->
34 34
35 35 </div>
36 36 <!-- FOOTER -->
37 37 <div id="footer">
38 38 <div id="footer-inner" class="title wrapper">
39 39 <div>
40 40 <p class="footer-link-right">
41 41 % if c.visual.show_version:
42 42 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
43 43 % endif
44 44 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
45 45 % if c.visual.rhodecode_support_url:
46 46 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
47 47 % endif
48 48 </p>
49 49 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
50 50 <p class="server-instance" style="display:${sid}">
51 51 ## display hidden instance ID if specially defined
52 52 % if c.rhodecode_instanceid:
53 53 ${_('RhodeCode instance id: %s') % c.rhodecode_instanceid}
54 54 % endif
55 55 </p>
56 56 </div>
57 57 </div>
58 58 </div>
59 59
60 60 <!-- END FOOTER -->
61 61
62 62 ### MAKO DEFS ###
63 63
64 64 <%def name="menu_bar_subnav()">
65 65 </%def>
66 66
67 67 <%def name="breadcrumbs(class_='breadcrumbs')">
68 68 <div class="${class_}">
69 69 ${self.breadcrumbs_links()}
70 70 </div>
71 71 </%def>
72 72
73 73 <%def name="admin_menu()">
74 74 <ul class="admin_menu submenu">
75 75 <li><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
76 76 <li><a href="${h.url('repos')}">${_('Repositories')}</a></li>
77 77 <li><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
78 78 <li><a href="${h.route_path('users')}">${_('Users')}</a></li>
79 79 <li><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
80 80 <li><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
81 81 <li><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
82 82 <li><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
83 83 <li><a href="${h.url('admin_defaults_repositories')}">${_('Defaults')}</a></li>
84 84 <li class="last"><a href="${h.url('admin_settings')}">${_('Settings')}</a></li>
85 85 </ul>
86 86 </%def>
87 87
88 88
89 89 <%def name="dt_info_panel(elements)">
90 90 <dl class="dl-horizontal">
91 91 %for dt, dd, title, show_items in elements:
92 92 <dt>${dt}:</dt>
93 93 <dd title="${h.tooltip(title)}">
94 94 %if callable(dd):
95 95 ## allow lazy evaluation of elements
96 96 ${dd()}
97 97 %else:
98 98 ${dd}
99 99 %endif
100 100 %if show_items:
101 101 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
102 102 %endif
103 103 </dd>
104 104
105 105 %if show_items:
106 106 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
107 107 %for item in show_items:
108 108 <dt></dt>
109 109 <dd>${item}</dd>
110 110 %endfor
111 111 </div>
112 112 %endif
113 113
114 114 %endfor
115 115 </dl>
116 116 </%def>
117 117
118 118
119 119 <%def name="gravatar(email, size=16)">
120 120 <%
121 121 if (size > 16):
122 122 gravatar_class = 'gravatar gravatar-large'
123 123 else:
124 124 gravatar_class = 'gravatar'
125 125 %>
126 126 <%doc>
127 127 TODO: johbo: For now we serve double size images to make it smooth
128 128 for retina. This is how it worked until now. Should be replaced
129 129 with a better solution at some point.
130 130 </%doc>
131 131 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
132 132 </%def>
133 133
134 134
135 135 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
136 136 <% email = h.email_or_none(contact) %>
137 137 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
138 138 ${self.gravatar(email, size)}
139 139 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
140 140 </div>
141 141 </%def>
142 142
143 143
144 144 ## admin menu used for people that have some admin resources
145 145 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
146 146 <ul class="submenu">
147 147 %if repositories:
148 148 <li class="local-admin-repos"><a href="${h.url('repos')}">${_('Repositories')}</a></li>
149 149 %endif
150 150 %if repository_groups:
151 151 <li class="local-admin-repo-groups"><a href="${h.url('repo_groups')}">${_('Repository groups')}</a></li>
152 152 %endif
153 153 %if user_groups:
154 154 <li class="local-admin-user-groups"><a href="${h.url('users_groups')}">${_('User groups')}</a></li>
155 155 %endif
156 156 </ul>
157 157 </%def>
158 158
159 159 <%def name="repo_page_title(repo_instance)">
160 160 <div class="title-content">
161 161 <div class="title-main">
162 162 ## SVN/HG/GIT icons
163 163 %if h.is_hg(repo_instance):
164 164 <i class="icon-hg"></i>
165 165 %endif
166 166 %if h.is_git(repo_instance):
167 167 <i class="icon-git"></i>
168 168 %endif
169 169 %if h.is_svn(repo_instance):
170 170 <i class="icon-svn"></i>
171 171 %endif
172 172
173 173 ## public/private
174 174 %if repo_instance.private:
175 175 <i class="icon-repo-private"></i>
176 176 %else:
177 177 <i class="icon-repo-public"></i>
178 178 %endif
179 179
180 180 ## repo name with group name
181 181 ${h.breadcrumb_repo_link(c.rhodecode_db_repo)}
182 182
183 183 </div>
184 184
185 185 ## FORKED
186 186 %if repo_instance.fork:
187 187 <p>
188 188 <i class="icon-code-fork"></i> ${_('Fork of')}
189 189 <a href="${h.route_path('repo_summary',repo_name=repo_instance.fork.repo_name)}">${repo_instance.fork.repo_name}</a>
190 190 </p>
191 191 %endif
192 192
193 193 ## IMPORTED FROM REMOTE
194 194 %if repo_instance.clone_uri:
195 195 <p>
196 196 <i class="icon-code-fork"></i> ${_('Clone from')}
197 197 <a href="${h.url(h.safe_str(h.hide_credentials(repo_instance.clone_uri)))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
198 198 </p>
199 199 %endif
200 200
201 201 ## LOCKING STATUS
202 202 %if repo_instance.locked[0]:
203 203 <p class="locking_locked">
204 204 <i class="icon-repo-lock"></i>
205 205 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
206 206 </p>
207 207 %elif repo_instance.enable_locking:
208 208 <p class="locking_unlocked">
209 209 <i class="icon-repo-unlock"></i>
210 210 ${_('Repository not locked. Pull repository to lock it.')}
211 211 </p>
212 212 %endif
213 213
214 214 </div>
215 215 </%def>
216 216
217 217 <%def name="repo_menu(active=None)">
218 218 <%
219 219 def is_active(selected):
220 220 if selected == active:
221 221 return "active"
222 222 %>
223 223
224 224 <!--- CONTEXT BAR -->
225 225 <div id="context-bar">
226 226 <div class="wrapper">
227 227 <ul id="context-pages" class="horizontal-list navigation">
228 228 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
229 229 <li class="${is_active('changelog')}"><a class="menulink" href="${h.route_path('repo_changelog', repo_name=c.repo_name)}"><div class="menulabel">${_('Changelog')}</div></a></li>
230 230 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
231 231 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
232 232 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
233 233 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
234 234 <li class="${is_active('showpullrequest')}">
235 235 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
236 236 %if c.repository_pull_requests:
237 237 <span class="pr_notifications">${c.repository_pull_requests}</span>
238 238 %endif
239 239 <div class="menulabel">${_('Pull Requests')}</div>
240 240 </a>
241 241 </li>
242 242 %endif
243 243 <li class="${is_active('options')}">
244 244 <a class="menulink dropdown">
245 245 <div class="menulabel">${_('Options')} <div class="show_more"></div></div>
246 246 </a>
247 247 <ul class="submenu">
248 248 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
249 249 <li><a href="${h.route_path('edit_repo',repo_name=c.repo_name)}">${_('Settings')}</a></li>
250 250 %endif
251 251 %if c.rhodecode_db_repo.fork:
252 252 <li>
253 253 <a title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
254 254 href="${h.route_path('repo_compare',
255 255 repo_name=c.rhodecode_db_repo.fork.repo_name,
256 256 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
257 257 source_ref=c.rhodecode_db_repo.landing_rev[1],
258 258 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
259 259 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
260 260 _query=dict(merge=1))}"
261 261 >
262 262 ${_('Compare fork')}
263 263 </a>
264 264 </li>
265 265 %endif
266 266
267 267 <li><a href="${h.route_path('search_repo',repo_name=c.repo_name)}">${_('Search')}</a></li>
268 268
269 269 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking:
270 270 %if c.rhodecode_db_repo.locked[0]:
271 271 <li><a class="locking_del" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Unlock')}</a></li>
272 272 %else:
273 273 <li><a class="locking_add" href="${h.url('toggle_locking',repo_name=c.repo_name)}">${_('Lock')}</a></li>
274 274 %endif
275 275 %endif
276 276 %if c.rhodecode_user.username != h.DEFAULT_USER:
277 277 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
278 278 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}">${_('Fork')}</a></li>
279 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
279 <li><a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a></li>
280 280 %endif
281 281 %endif
282 282 </ul>
283 283 </li>
284 284 </ul>
285 285 </div>
286 286 <div class="clear"></div>
287 287 </div>
288 288 <!--- END CONTEXT BAR -->
289 289
290 290 </%def>
291 291
292 292 <%def name="usermenu(active=False)">
293 293 ## USER MENU
294 294 <li id="quick_login_li" class="${'active' if active else ''}">
295 295 <a id="quick_login_link" class="menulink childs">
296 296 ${gravatar(c.rhodecode_user.email, 20)}
297 297 <span class="user">
298 298 %if c.rhodecode_user.username != h.DEFAULT_USER:
299 299 <span class="menu_link_user">${c.rhodecode_user.username}</span><div class="show_more"></div>
300 300 %else:
301 301 <span>${_('Sign in')}</span>
302 302 %endif
303 303 </span>
304 304 </a>
305 305
306 306 <div class="user-menu submenu">
307 307 <div id="quick_login">
308 308 %if c.rhodecode_user.username == h.DEFAULT_USER:
309 309 <h4>${_('Sign in to your account')}</h4>
310 310 ${h.form(h.route_path('login', _query={'came_from': h.url.current()}), needs_csrf_token=False)}
311 311 <div class="form form-vertical">
312 312 <div class="fields">
313 313 <div class="field">
314 314 <div class="label">
315 315 <label for="username">${_('Username')}:</label>
316 316 </div>
317 317 <div class="input">
318 318 ${h.text('username',class_='focus',tabindex=1)}
319 319 </div>
320 320
321 321 </div>
322 322 <div class="field">
323 323 <div class="label">
324 324 <label for="password">${_('Password')}:</label>
325 325 %if h.HasPermissionAny('hg.password_reset.enabled')():
326 326 <span class="forgot_password">${h.link_to(_('(Forgot password?)'),h.route_path('reset_password'), class_='pwd_reset')}</span>
327 327 %endif
328 328 </div>
329 329 <div class="input">
330 330 ${h.password('password',class_='focus',tabindex=2)}
331 331 </div>
332 332 </div>
333 333 <div class="buttons">
334 334 <div class="register">
335 335 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
336 336 ${h.link_to(_("Don't have an account?"),h.route_path('register'))} <br/>
337 337 %endif
338 338 ${h.link_to(_("Using external auth? Sign In here."),h.route_path('login'))}
339 339 </div>
340 340 <div class="submit">
341 341 ${h.submit('sign_in',_('Sign In'),class_="btn btn-small",tabindex=3)}
342 342 </div>
343 343 </div>
344 344 </div>
345 345 </div>
346 346 ${h.end_form()}
347 347 %else:
348 348 <div class="">
349 349 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
350 350 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
351 351 <div class="email">${c.rhodecode_user.email}</div>
352 352 </div>
353 353 <div class="">
354 354 <ol class="links">
355 355 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
356 356 % if c.rhodecode_user.personal_repo_group:
357 357 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
358 358 % endif
359 359 <li class="logout">
360 360 ${h.secure_form(h.route_path('logout'), request=request)}
361 361 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
362 362 ${h.end_form()}
363 363 </li>
364 364 </ol>
365 365 </div>
366 366 %endif
367 367 </div>
368 368 </div>
369 369 %if c.rhodecode_user.username != h.DEFAULT_USER:
370 370 <div class="pill_container">
371 371 <a class="menu_link_notifications ${'empty' if c.unread_notifications == 0 else ''}" href="${h.route_path('notifications_show_all')}">${c.unread_notifications}</a>
372 372 </div>
373 373 % endif
374 374 </li>
375 375 </%def>
376 376
377 377 <%def name="menu_items(active=None)">
378 378 <%
379 379 def is_active(selected):
380 380 if selected == active:
381 381 return "active"
382 382 return ""
383 383 %>
384 384 <ul id="quick" class="main_nav navigation horizontal-list">
385 385 <!-- repo switcher -->
386 386 <li class="${is_active('repositories')} repo_switcher_li has_select2">
387 387 <input id="repo_switcher" name="repo_switcher" type="hidden">
388 388 </li>
389 389
390 390 ## ROOT MENU
391 391 %if c.rhodecode_user.username != h.DEFAULT_USER:
392 392 <li class="${is_active('journal')}">
393 393 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
394 394 <div class="menulabel">${_('Journal')}</div>
395 395 </a>
396 396 </li>
397 397 %else:
398 398 <li class="${is_active('journal')}">
399 399 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
400 400 <div class="menulabel">${_('Public journal')}</div>
401 401 </a>
402 402 </li>
403 403 %endif
404 404 <li class="${is_active('gists')}">
405 405 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
406 406 <div class="menulabel">${_('Gists')}</div>
407 407 </a>
408 408 </li>
409 409 <li class="${is_active('search')}">
410 410 <a class="menulink" title="${_('Search in repositories you have access to')}" href="${h.route_path('search')}">
411 411 <div class="menulabel">${_('Search')}</div>
412 412 </a>
413 413 </li>
414 414 % if h.HasPermissionAll('hg.admin')('access admin main page'):
415 415 <li class="${is_active('admin')}">
416 416 <a class="menulink childs" title="${_('Admin settings')}" href="#" onclick="return false;">
417 417 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
418 418 </a>
419 419 ${admin_menu()}
420 420 </li>
421 421 % elif c.rhodecode_user.repositories_admin or c.rhodecode_user.repository_groups_admin or c.rhodecode_user.user_groups_admin:
422 422 <li class="${is_active('admin')}">
423 423 <a class="menulink childs" title="${_('Delegated Admin settings')}">
424 424 <div class="menulabel">${_('Admin')} <div class="show_more"></div></div>
425 425 </a>
426 426 ${admin_menu_simple(c.rhodecode_user.repositories_admin,
427 427 c.rhodecode_user.repository_groups_admin,
428 428 c.rhodecode_user.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
429 429 </li>
430 430 % endif
431 431 % if c.debug_style:
432 432 <li class="${is_active('debug_style')}">
433 433 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
434 434 <div class="menulabel">${_('Style')}</div>
435 435 </a>
436 436 </li>
437 437 % endif
438 438 ## render extra user menu
439 439 ${usermenu(active=(active=='my_account'))}
440 440 </ul>
441 441
442 442 <script type="text/javascript">
443 443 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
444 444
445 445 /*format the look of items in the list*/
446 446 var format = function(state, escapeMarkup){
447 447 if (!state.id){
448 448 return state.text; // optgroup
449 449 }
450 450 var obj_dict = state.obj;
451 451 var tmpl = '';
452 452
453 453 if(obj_dict && state.type == 'repo'){
454 454 if(obj_dict['repo_type'] === 'hg'){
455 455 tmpl += '<i class="icon-hg"></i> ';
456 456 }
457 457 else if(obj_dict['repo_type'] === 'git'){
458 458 tmpl += '<i class="icon-git"></i> ';
459 459 }
460 460 else if(obj_dict['repo_type'] === 'svn'){
461 461 tmpl += '<i class="icon-svn"></i> ';
462 462 }
463 463 if(obj_dict['private']){
464 464 tmpl += '<i class="icon-lock" ></i> ';
465 465 }
466 466 else if(visual_show_public_icon){
467 467 tmpl += '<i class="icon-unlock-alt"></i> ';
468 468 }
469 469 }
470 470 if(obj_dict && state.type == 'commit') {
471 471 tmpl += '<i class="icon-tag"></i>';
472 472 }
473 473 if(obj_dict && state.type == 'group'){
474 474 tmpl += '<i class="icon-folder-close"></i> ';
475 475 }
476 476 tmpl += escapeMarkup(state.text);
477 477 return tmpl;
478 478 };
479 479
480 480 var formatResult = function(result, container, query, escapeMarkup) {
481 481 return format(result, escapeMarkup);
482 482 };
483 483
484 484 var formatSelection = function(data, container, escapeMarkup) {
485 485 return format(data, escapeMarkup);
486 486 };
487 487
488 488 $("#repo_switcher").select2({
489 489 cachedDataSource: {},
490 490 minimumInputLength: 2,
491 491 placeholder: '<div class="menulabel">${_('Go to')} <div class="show_more"></div></div>',
492 492 dropdownAutoWidth: true,
493 493 formatResult: formatResult,
494 494 formatSelection: formatSelection,
495 495 containerCssClass: "repo-switcher",
496 496 dropdownCssClass: "repo-switcher-dropdown",
497 497 escapeMarkup: function(m){
498 498 // don't escape our custom placeholder
499 499 if(m.substr(0,23) == '<div class="menulabel">'){
500 500 return m;
501 501 }
502 502
503 503 return Select2.util.escapeMarkup(m);
504 504 },
505 505 query: $.debounce(250, function(query){
506 506 self = this;
507 507 var cacheKey = query.term;
508 508 var cachedData = self.cachedDataSource[cacheKey];
509 509
510 510 if (cachedData) {
511 511 query.callback({results: cachedData.results});
512 512 } else {
513 513 $.ajax({
514 514 url: pyroutes.url('goto_switcher_data'),
515 515 data: {'query': query.term},
516 516 dataType: 'json',
517 517 type: 'GET',
518 518 success: function(data) {
519 519 self.cachedDataSource[cacheKey] = data;
520 520 query.callback({results: data.results});
521 521 },
522 522 error: function(data, textStatus, errorThrown) {
523 523 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
524 524 }
525 525 })
526 526 }
527 527 })
528 528 });
529 529
530 530 $("#repo_switcher").on('select2-selecting', function(e){
531 531 e.preventDefault();
532 532 window.location = e.choice.url;
533 533 });
534 534
535 535 </script>
536 536 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
537 537 </%def>
538 538
539 539 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
540 540 <div class="modal-dialog">
541 541 <div class="modal-content">
542 542 <div class="modal-header">
543 543 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
544 544 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
545 545 </div>
546 546 <div class="modal-body">
547 547 <div class="block-left">
548 548 <table class="keyboard-mappings">
549 549 <tbody>
550 550 <tr>
551 551 <th></th>
552 552 <th>${_('Site-wide shortcuts')}</th>
553 553 </tr>
554 554 <%
555 555 elems = [
556 556 ('/', 'Open quick search box'),
557 557 ('g h', 'Goto home page'),
558 558 ('g g', 'Goto my private gists page'),
559 559 ('g G', 'Goto my public gists page'),
560 560 ('n r', 'New repository page'),
561 561 ('n g', 'New gist page'),
562 562 ]
563 563 %>
564 564 %for key, desc in elems:
565 565 <tr>
566 566 <td class="keys">
567 567 <span class="key tag">${key}</span>
568 568 </td>
569 569 <td>${desc}</td>
570 570 </tr>
571 571 %endfor
572 572 </tbody>
573 573 </table>
574 574 </div>
575 575 <div class="block-left">
576 576 <table class="keyboard-mappings">
577 577 <tbody>
578 578 <tr>
579 579 <th></th>
580 580 <th>${_('Repositories')}</th>
581 581 </tr>
582 582 <%
583 583 elems = [
584 584 ('g s', 'Goto summary page'),
585 585 ('g c', 'Goto changelog page'),
586 586 ('g f', 'Goto files page'),
587 587 ('g F', 'Goto files page with file search activated'),
588 588 ('g p', 'Goto pull requests page'),
589 589 ('g o', 'Goto repository settings'),
590 590 ('g O', 'Goto repository permissions settings'),
591 591 ]
592 592 %>
593 593 %for key, desc in elems:
594 594 <tr>
595 595 <td class="keys">
596 596 <span class="key tag">${key}</span>
597 597 </td>
598 598 <td>${desc}</td>
599 599 </tr>
600 600 %endfor
601 601 </tbody>
602 602 </table>
603 603 </div>
604 604 </div>
605 605 <div class="modal-footer">
606 606 </div>
607 607 </div><!-- /.modal-content -->
608 608 </div><!-- /.modal-dialog -->
609 609 </div><!-- /.modal -->
@@ -1,297 +1,297 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='changelog')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30
31 31 <div class="box">
32 32 <div class="title">
33 33 ${self.repo_page_title(c.rhodecode_db_repo)}
34 34 <ul class="links">
35 35 <li>
36 36 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
37 37 %if c.rhodecode_db_repo.fork:
38 38 <span>
39 39 <a id="compare_fork_button"
40 40 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
41 41 class="btn btn-small"
42 42 href="${h.route_path('repo_compare',
43 43 repo_name=c.rhodecode_db_repo.fork.repo_name,
44 44 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
45 45 source_ref=c.rhodecode_db_repo.landing_rev[1],
46 46 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
47 47 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
48 48 _query=dict(merge=1, target_repo=c.repo_name))}"
49 49 >
50 50 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
51 51 </a>
52 52 </span>
53 53 %endif
54 54
55 55 ## pr open link
56 56 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
57 57 <span>
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
58 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
59 59 ${_('Open new pull request')}
60 60 </a>
61 61 </span>
62 62 %endif
63 63
64 64 ## clear selection
65 65 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
66 66 ${_('Clear selection')}
67 67 </div>
68 68
69 69 </li>
70 70 </ul>
71 71 </div>
72 72
73 73 % if c.pagination:
74 74 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
75 75
76 76 <div class="graph-header">
77 77 <div id="filter_changelog">
78 78 ${h.hidden('branch_filter')}
79 79 %if c.selected_name:
80 80 <div class="btn btn-default" id="clear_filter" >
81 81 ${_('Clear filter')}
82 82 </div>
83 83 %endif
84 84 </div>
85 85 ${self.breadcrumbs('breadcrumbs_light')}
86 86 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
87 87 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
88 88 </div>
89 89 </div>
90 90
91 91 <div id="graph">
92 92 <div class="graph-col-wrapper">
93 93 <div id="graph_nodes">
94 94 <div id="graph_canvas"></div>
95 95 </div>
96 96 <div id="graph_content" class="main-content graph_full_width">
97 97
98 98 <div class="table">
99 99 <table id="changesets" class="rctable">
100 100 <tr>
101 101 ## checkbox
102 102 <th></th>
103 103 <th colspan="2"></th>
104 104
105 105 <th>${_('Commit')}</th>
106 106 ## commit message expand arrow
107 107 <th></th>
108 108 <th>${_('Commit Message')}</th>
109 109
110 110 <th>${_('Age')}</th>
111 111 <th>${_('Author')}</th>
112 112
113 113 <th>${_('Refs')}</th>
114 114 </tr>
115 115
116 116 <tbody class="commits-range">
117 117 <%include file='changelog_elements.mako'/>
118 118 </tbody>
119 119 </table>
120 120 </div>
121 121 </div>
122 122 <div class="pagination-wh pagination-left">
123 123 ${c.pagination.pager('$link_previous ~2~ $link_next')}
124 124 </div>
125 125 </div>
126 126
127 127 <script type="text/javascript">
128 128 var cache = {};
129 129 $(function(){
130 130
131 131 // Create links to commit ranges when range checkboxes are selected
132 132 var $commitCheckboxes = $('.commit-range');
133 133 // cache elements
134 134 var $commitRangeContainer = $('#rev_range_container');
135 135 var $commitRangeClear = $('#rev_range_clear');
136 136
137 137 var checkboxRangeSelector = function(e){
138 138 var selectedCheckboxes = [];
139 139 for (pos in $commitCheckboxes){
140 140 if($commitCheckboxes[pos].checked){
141 141 selectedCheckboxes.push($commitCheckboxes[pos]);
142 142 }
143 143 }
144 144 var open_new_pull_request = $('#open_new_pull_request');
145 145 if(open_new_pull_request){
146 146 var selected_changes = selectedCheckboxes.length;
147 147 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
148 148 open_new_pull_request.hide();
149 149 } else {
150 150 if (selected_changes == 1) {
151 151 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
152 152 } else if (selected_changes == 0) {
153 153 open_new_pull_request.html(_gettext('Open new pull request'));
154 154 }
155 155 open_new_pull_request.show();
156 156 }
157 157 }
158 158
159 159 if (selectedCheckboxes.length>0){
160 160 var revEnd = selectedCheckboxes[0].name;
161 161 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
162 162 var url = pyroutes.url('repo_commit',
163 163 {'repo_name': '${c.repo_name}',
164 164 'commit_id': revStart+'...'+revEnd});
165 165
166 166 var link = (revStart == revEnd)
167 167 ? _gettext('Show selected commit __S')
168 168 : _gettext('Show selected commits __S ... __E');
169 169
170 170 link = link.replace('__S', revStart.substr(0,6));
171 171 link = link.replace('__E', revEnd.substr(0,6));
172 172
173 173 $commitRangeContainer
174 174 .attr('href',url)
175 175 .html(link)
176 176 .show();
177 177
178 178 $commitRangeClear.show();
179 var _url = pyroutes.url('pullrequest_home',
179 var _url = pyroutes.url('pullrequest_new',
180 180 {'repo_name': '${c.repo_name}',
181 181 'commit': revEnd});
182 182 open_new_pull_request.attr('href', _url);
183 183 $('#compare_fork_button').hide();
184 184 } else {
185 185 $commitRangeContainer.hide();
186 186 $commitRangeClear.hide();
187 187
188 188 %if c.branch_name:
189 var _url = pyroutes.url('pullrequest_home',
189 var _url = pyroutes.url('pullrequest_new',
190 190 {'repo_name': '${c.repo_name}',
191 191 'branch':'${c.branch_name}'});
192 192 open_new_pull_request.attr('href', _url);
193 193 %else:
194 var _url = pyroutes.url('pullrequest_home',
194 var _url = pyroutes.url('pullrequest_new',
195 195 {'repo_name': '${c.repo_name}'});
196 196 open_new_pull_request.attr('href', _url);
197 197 %endif
198 198 $('#compare_fork_button').show();
199 199 }
200 200 };
201 201
202 202 $commitCheckboxes.on('click', checkboxRangeSelector);
203 203
204 204 $commitRangeClear.on('click',function(e) {
205 205 $commitCheckboxes.attr('checked', false);
206 206 checkboxRangeSelector();
207 207 e.preventDefault();
208 208 });
209 209
210 210 // make sure the buttons are consistent when navigate back and forth
211 211 checkboxRangeSelector();
212 212
213 213 var msgs = $('.message');
214 214 // get first element height
215 215 var el = $('#graph_content .container')[0];
216 216 var row_h = el.clientHeight;
217 217 for (var i=0; i < msgs.length; i++) {
218 218 var m = msgs[i];
219 219
220 220 var h = m.clientHeight;
221 221 var pad = $(m).css('padding');
222 222 if (h > row_h) {
223 223 var offset = row_h - (h+12);
224 224 $(m.nextElementSibling).css('display','block');
225 225 $(m.nextElementSibling).css('margin-top',offset+'px');
226 226 }
227 227 }
228 228
229 229 $("#clear_filter").on("click", function() {
230 230 var filter = {'repo_name': '${c.repo_name}'};
231 231 window.location = pyroutes.url('repo_changelog', filter);
232 232 });
233 233
234 234 $("#branch_filter").select2({
235 235 'dropdownAutoWidth': true,
236 236 'width': 'resolve',
237 237 'placeholder': "${c.selected_name or _('Filter changelog')}",
238 238 containerCssClass: "drop-menu",
239 239 dropdownCssClass: "drop-menu-dropdown",
240 240 query: function(query){
241 241 var key = 'cache';
242 242 var cached = cache[key] ;
243 243 if(cached) {
244 244 var data = {results: []};
245 245 //filter results
246 246 $.each(cached.results, function(){
247 247 var section = this.text;
248 248 var children = [];
249 249 $.each(this.children, function(){
250 250 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
251 251 children.push({'id': this.id, 'text': this.text, 'type': this.type})
252 252 }
253 253 });
254 254 data.results.push({'text': section, 'children': children});
255 255 query.callback({results: data.results});
256 256 });
257 257 }else{
258 258 $.ajax({
259 259 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
260 260 data: {},
261 261 dataType: 'json',
262 262 type: 'GET',
263 263 success: function(data) {
264 264 cache[key] = data;
265 265 query.callback({results: data.results});
266 266 }
267 267 })
268 268 }
269 269 }
270 270 });
271 271 $('#branch_filter').on('change', function(e){
272 272 var data = $('#branch_filter').select2('data');
273 273 var selected = data.text;
274 274 var filter = {'repo_name': '${c.repo_name}'};
275 275 if(data.type == 'branch' || data.type == 'branch_closed'){
276 276 filter["branch"] = selected;
277 277 }
278 278 else if (data.type == 'book'){
279 279 filter["bookmark"] = selected;
280 280 }
281 281 window.location = pyroutes.url('repo_changelog', filter);
282 282 });
283 283
284 284 commitsController = new CommitsController();
285 285 % if not c.changelog_for_path:
286 286 commitsController.reloadGraph();
287 287 % endif
288 288
289 289 });
290 290
291 291 </script>
292 292 </div>
293 293 % else:
294 294 ${_('There are no changes yet')}
295 295 % endif
296 296 </div>
297 297 </%def>
@@ -1,75 +1,75 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Files Delete') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Delete file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <div class="title">
25 25 ${self.repo_page_title(c.rhodecode_db_repo)}
26 26 </div>
27 27 <div class="edit-file-title">
28 28 ${self.breadcrumbs()}
29 29 </div>
30 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', class_="form-horizontal")}
30 ${h.secure_form(h.route_path('repo_files_delete_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', class_="form-horizontal", request=request)}
31 31 <div class="edit-file-fieldset">
32 32 <div class="fieldset">
33 33 <div id="destination-label" class="left-label">
34 34 ${_('Path')}:
35 35 </div>
36 36 <div class="right-content">
37 37 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
38 38 </div>
39 39 </div>
40 40 </div>
41 41
42 42 <div id="codeblock" class="codeblock delete-file-preview">
43 43 <div class="code-body">
44 44 %if c.file.is_binary:
45 45 ${_('Binary file (%s)') % c.file.mimetype}
46 46 %else:
47 47 %if c.file.size < c.visual.cut_off_limit_file:
48 48 ${h.pygmentize(c.file,linenos=True,anchorlinenos=False,cssclass="code-highlight")}
49 49 %else:
50 50 ${_('File size {} is bigger then allowed limit {}. ').format(h.format_byte_size_binary(c.file.size), h.format_byte_size_binary(c.visual.cut_off_limit_file))} ${h.link_to(_('Show as raw'),
51 51 h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path))}
52 52 %endif
53 53 %endif
54 54 </div>
55 55 </div>
56 56
57 57 <div class="edit-file-fieldset">
58 58 <div class="fieldset">
59 59 <div id="commit-message-label" class="commit-message-label left-label">
60 60 ${_('Commit Message')}:
61 61 </div>
62 62 <div class="right-content">
63 63 <div class="message">
64 64 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
65 65 </div>
66 66 </div>
67 67 </div>
68 68 <div class="pull-right">
69 69 ${h.reset('reset',_('Cancel'),class_="btn btn-small btn-danger")}
70 70 ${h.submit('commit',_('Delete File'),class_="btn btn-small btn-danger-action")}
71 71 </div>
72 72 </div>
73 73 ${h.end_form()}
74 74 </div>
75 75 </%def>
@@ -1,197 +1,197 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s File Edit') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Edit file')} @ ${h.show_id(c.commit)}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <% renderer = h.renderer_from_filename(c.f_path)%>
24 24 <div class="box">
25 25 <div class="title">
26 26 ${self.repo_page_title(c.rhodecode_db_repo)}
27 27 </div>
28 28 <div class="edit-file-title">
29 29 ${self.breadcrumbs()}
30 30 </div>
31 31 <div class="edit-file-fieldset">
32 32 <div class="fieldset">
33 33 <div id="destination-label" class="left-label">
34 34 ${_('Path')}:
35 35 </div>
36 36 <div class="right-content">
37 37 <div id="specify-custom-path-container">
38 38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
39 39 </div>
40 40 </div>
41 41 </div>
42 42 </div>
43 43
44 44 <div class="table">
45 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST')}
45 ${h.secure_form(h.route_path('repo_files_update_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', method='POST', request=request)}
46 46 <div id="codeblock" class="codeblock" >
47 47 <div class="code-header">
48 48 <div class="stats">
49 49 <i class="icon-file"></i>
50 50 <span class="item">${h.link_to("r%s:%s" % (c.file.commit.idx,h.short_id(c.file.commit.raw_id)),h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.file.commit.raw_id))}</span>
51 51 <span class="item">${h.format_byte_size_binary(c.file.size)}</span>
52 52 <span class="item last">${c.file.mimetype}</span>
53 53 <div class="buttons">
54 54 <a class="btn btn-mini" href="${h.route_path('repo_changelog_file',repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">
55 55 <i class="icon-time"></i> ${_('history')}
56 56 </a>
57 57
58 58 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
59 59 % if not c.file.is_binary:
60 60 %if True:
61 61 ${h.link_to(_('source'), h.route_path('repo_files', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
62 62 %else:
63 63 ${h.link_to(_('annotation'),h.route_path('repo_files:annotated',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path),class_="btn btn-mini")}
64 64 %endif
65 65
66 66 <a class="btn btn-mini" href="${h.route_path('repo_file_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
67 67 ${_('raw')}
68 68 </a>
69 69 <a class="btn btn-mini" href="${h.route_path('repo_file_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
70 70 <i class="icon-archive"></i> ${_('download')}
71 71 </a>
72 72 % endif
73 73 % endif
74 74 </div>
75 75 </div>
76 76 <div class="form">
77 77 <label for="set_mode">${_('Editing file')}:</label>
78 78 ${'%s /' % c.file.dir_path if c.file.dir_path else c.file.dir_path}
79 79 <input id="filename" type="text" name="filename" value="${c.file.name}">
80 80
81 81 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
82 82 <label for="line_wrap">${_('line wraps')}</label>
83 83 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
84 84
85 85 <div id="render_preview" class="btn btn-small preview hidden">${_('Preview')}</div>
86 86 </div>
87 87 </div>
88 88 <div id="editor_container">
89 89 <pre id="editor_pre"></pre>
90 90 <textarea id="editor" name="content" >${h.escape(c.file.content)|n}</textarea>
91 91 <div id="editor_preview" ></div>
92 92 </div>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="edit-file-fieldset">
97 97 <div class="fieldset">
98 98 <div id="commit-message-label" class="commit-message-label left-label">
99 99 ${_('Commit Message')}:
100 100 </div>
101 101 <div class="right-content">
102 102 <div class="message">
103 103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
104 104 </div>
105 105 </div>
106 106 </div>
107 107 <div class="pull-right">
108 108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
109 109 ${h.submit('commit',_('Commit changes'),class_="btn btn-small btn-success")}
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 </div>
114 114
115 115 <script type="text/javascript">
116 116 $(document).ready(function(){
117 117 var renderer = "${renderer}";
118 118 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.file.path)}";
119 119 var myCodeMirror = initCodeMirror('editor', reset_url);
120 120
121 121 var modes_select = $('#set_mode');
122 122 fillCodeMirrorOptions(modes_select);
123 123
124 124 // try to detect the mode based on the file we edit
125 125 var mimetype = "${c.file.mimetype}";
126 126 var detected_mode = detectCodeMirrorMode(
127 127 "${c.file.name}", mimetype);
128 128
129 129 if(detected_mode){
130 130 setCodeMirrorMode(myCodeMirror, detected_mode);
131 131 $(modes_select).select2("val", mimetype);
132 132 $(modes_select).change();
133 133 setCodeMirrorMode(myCodeMirror, detected_mode);
134 134 }
135 135
136 136 var filename_selector = '#filename';
137 137 var callback = function(filename, mimetype, mode){
138 138 CodeMirrorPreviewEnable(mode);
139 139 };
140 140 // on change of select field set mode
141 141 setCodeMirrorModeFromSelect(
142 142 modes_select, filename_selector, myCodeMirror, callback);
143 143
144 144 // on entering the new filename set mode, from given extension
145 145 setCodeMirrorModeFromInput(
146 146 modes_select, filename_selector, myCodeMirror, callback);
147 147
148 148 // if the file is renderable set line wraps automatically
149 149 if (renderer !== ""){
150 150 var line_wrap = 'on';
151 151 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
152 152 setCodeMirrorLineWrap(myCodeMirror, true);
153 153 }
154 154 // on select line wraps change the editor
155 155 $('#line_wrap').on('change', function(e){
156 156 var selected = e.currentTarget;
157 157 var line_wraps = {'on': true, 'off': false}[selected.value];
158 158 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
159 159 });
160 160
161 161 // render preview/edit button
162 162 if (mimetype === 'text/x-rst' || mimetype === 'text/plain') {
163 163 $('#render_preview').removeClass('hidden');
164 164 }
165 165 $('#render_preview').on('click', function(e){
166 166 if($(this).hasClass('preview')){
167 167 $(this).removeClass('preview');
168 168 $(this).html("${_('Edit')}");
169 169 $('#editor_preview').show();
170 170 $(myCodeMirror.getWrapperElement()).hide();
171 171
172 172 var possible_renderer = {
173 173 'rst':'rst',
174 174 'markdown':'markdown',
175 175 'gfm': 'markdown'}[myCodeMirror.getMode().name];
176 176 var _text = myCodeMirror.getValue();
177 177 var _renderer = possible_renderer || DEFAULT_RENDERER;
178 178 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
179 179 $('#editor_preview').html(_gettext('Loading ...'));
180 180 var url = pyroutes.url('repo_commit_comment_preview',
181 181 {'repo_name': '${c.repo_name}',
182 182 'commit_id': '${c.commit.raw_id}'});
183 183 ajaxPOST(url, post_data, function(o){
184 184 $('#editor_preview').html(o);
185 185 })
186 186 }
187 187 else{
188 188 $(this).addClass('preview');
189 189 $(this).html("${_('Preview')}");
190 190 $('#editor_preview').hide();
191 191 $(myCodeMirror.getWrapperElement()).show();
192 192 }
193 193 });
194 194
195 195 })
196 196 </script>
197 197 </%def>
@@ -1,526 +1,526 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 </div>
24 24
25 ${h.secure_form(h.url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
25 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name), id='pull_request_form', method='POST', request=request)}
26 26
27 27 ${self.breadcrumbs()}
28 28
29 29 <div class="box pr-summary">
30 30
31 31 <div class="summary-details block-left">
32 32
33 33
34 34 <div class="pr-details-title">
35 35 ${_('Pull request summary')}
36 36 </div>
37 37
38 38 <div class="form" style="padding-top: 10px">
39 39 <!-- fields -->
40 40
41 41 <div class="fields" >
42 42
43 43 <div class="field">
44 44 <div class="label">
45 45 <label for="pullrequest_title">${_('Title')}:</label>
46 46 </div>
47 47 <div class="input">
48 48 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
49 49 </div>
50 50 </div>
51 51
52 52 <div class="field">
53 53 <div class="label label-textarea">
54 54 <label for="pullrequest_desc">${_('Description')}:</label>
55 55 </div>
56 56 <div class="textarea text-area editor">
57 57 ${h.textarea('pullrequest_desc',size=30, )}
58 58 <span class="help-block">${_('Write a short description on this pull request')}</span>
59 59 </div>
60 60 </div>
61 61
62 62 <div class="field">
63 63 <div class="label label-textarea">
64 64 <label for="pullrequest_desc">${_('Commit flow')}:</label>
65 65 </div>
66 66
67 67 ## TODO: johbo: Abusing the "content" class here to get the
68 68 ## desired effect. Should be replaced by a proper solution.
69 69
70 70 ##ORG
71 71 <div class="content">
72 72 <strong>${_('Source repository')}:</strong>
73 73 ${c.rhodecode_db_repo.description}
74 74 </div>
75 75 <div class="content">
76 76 ${h.hidden('source_repo')}
77 77 ${h.hidden('source_ref')}
78 78 </div>
79 79
80 80 ##OTHER, most Probably the PARENT OF THIS FORK
81 81 <div class="content">
82 82 ## filled with JS
83 83 <div id="target_repo_desc"></div>
84 84 </div>
85 85
86 86 <div class="content">
87 87 ${h.hidden('target_repo')}
88 88 ${h.hidden('target_ref')}
89 89 <span id="target_ref_loading" style="display: none">
90 90 ${_('Loading refs...')}
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 <div class="field">
96 96 <div class="label label-textarea">
97 97 <label for="pullrequest_submit"></label>
98 98 </div>
99 99 <div class="input">
100 100 <div class="pr-submit-button">
101 101 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
102 102 </div>
103 103 <div id="pr_open_message"></div>
104 104 </div>
105 105 </div>
106 106
107 107 <div class="pr-spacing-container"></div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 <div>
112 112 ## AUTHOR
113 113 <div class="reviewers-title block-right">
114 114 <div class="pr-details-title">
115 115 ${_('Author of this pull request')}
116 116 </div>
117 117 </div>
118 118 <div class="block-right pr-details-content reviewers">
119 119 <ul class="group_members">
120 120 <li>
121 121 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
122 122 </li>
123 123 </ul>
124 124 </div>
125 125
126 126 ## REVIEW RULES
127 127 <div id="review_rules" style="display: none" class="reviewers-title block-right">
128 128 <div class="pr-details-title">
129 129 ${_('Reviewer rules')}
130 130 </div>
131 131 <div class="pr-reviewer-rules">
132 132 ## review rules will be appended here, by default reviewers logic
133 133 </div>
134 134 </div>
135 135
136 136 ## REVIEWERS
137 137 <div class="reviewers-title block-right">
138 138 <div class="pr-details-title">
139 139 ${_('Pull request reviewers')}
140 140 <span class="calculate-reviewers"> - ${_('loading...')}</span>
141 141 </div>
142 142 </div>
143 143 <div id="reviewers" class="block-right pr-details-content reviewers">
144 144 ## members goes here, filled via JS based on initial selection !
145 145 <input type="hidden" name="__start__" value="review_members:sequence">
146 146 <ul id="review_members" class="group_members"></ul>
147 147 <input type="hidden" name="__end__" value="review_members:sequence">
148 148 <div id="add_reviewer_input" class='ac'>
149 149 <div class="reviewer_ac">
150 150 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
151 151 <div id="reviewers_container"></div>
152 152 </div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="box">
158 158 <div>
159 159 ## overview pulled by ajax
160 160 <div id="pull_request_overview"></div>
161 161 </div>
162 162 </div>
163 163 ${h.end_form()}
164 164 </div>
165 165
166 166 <script type="text/javascript">
167 167 $(function(){
168 168 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
169 169 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
170 170 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
171 171 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
172 172
173 173 var $pullRequestForm = $('#pull_request_form');
174 174 var $sourceRepo = $('#source_repo', $pullRequestForm);
175 175 var $targetRepo = $('#target_repo', $pullRequestForm);
176 176 var $sourceRef = $('#source_ref', $pullRequestForm);
177 177 var $targetRef = $('#target_ref', $pullRequestForm);
178 178
179 179 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
180 180 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
181 181
182 182 var targetRepo = function() { return $targetRepo.eq(0).val() };
183 183 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
184 184
185 185 var calculateContainerWidth = function() {
186 186 var maxWidth = 0;
187 187 var repoSelect2Containers = ['#source_repo', '#target_repo'];
188 188 $.each(repoSelect2Containers, function(idx, value) {
189 189 $(value).select2('container').width('auto');
190 190 var curWidth = $(value).select2('container').width();
191 191 if (maxWidth <= curWidth) {
192 192 maxWidth = curWidth;
193 193 }
194 194 $.each(repoSelect2Containers, function(idx, value) {
195 195 $(value).select2('container').width(maxWidth + 10);
196 196 });
197 197 });
198 198 };
199 199
200 200 var initRefSelection = function(selectedRef) {
201 201 return function(element, callback) {
202 202 // translate our select2 id into a text, it's a mapping to show
203 203 // simple label when selecting by internal ID.
204 204 var id, refData;
205 205 if (selectedRef === undefined) {
206 206 id = element.val();
207 207 refData = element.val().split(':');
208 208 } else {
209 209 id = selectedRef;
210 210 refData = selectedRef.split(':');
211 211 }
212 212
213 213 var text = refData[1];
214 214 if (refData[0] === 'rev') {
215 215 text = text.substring(0, 12);
216 216 }
217 217
218 218 var data = {id: id, text: text};
219 219
220 220 callback(data);
221 221 };
222 222 };
223 223
224 224 var formatRefSelection = function(item) {
225 225 var prefix = '';
226 226 var refData = item.id.split(':');
227 227 if (refData[0] === 'branch') {
228 228 prefix = '<i class="icon-branch"></i>';
229 229 }
230 230 else if (refData[0] === 'book') {
231 231 prefix = '<i class="icon-bookmark"></i>';
232 232 }
233 233 else if (refData[0] === 'tag') {
234 234 prefix = '<i class="icon-tag"></i>';
235 235 }
236 236
237 237 var originalOption = item.element;
238 238 return prefix + item.text;
239 239 };
240 240
241 241 // custom code mirror
242 242 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
243 243
244 244 reviewersController = new ReviewersController();
245 245
246 246 var queryTargetRepo = function(self, query) {
247 247 // cache ALL results if query is empty
248 248 var cacheKey = query.term || '__';
249 249 var cachedData = self.cachedDataSource[cacheKey];
250 250
251 251 if (cachedData) {
252 252 query.callback({results: cachedData.results});
253 253 } else {
254 254 $.ajax({
255 255 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
256 256 data: {query: query.term},
257 257 dataType: 'json',
258 258 type: 'GET',
259 259 success: function(data) {
260 260 self.cachedDataSource[cacheKey] = data;
261 261 query.callback({results: data.results});
262 262 },
263 263 error: function(data, textStatus, errorThrown) {
264 264 alert(
265 265 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
266 266 }
267 267 });
268 268 }
269 269 };
270 270
271 271 var queryTargetRefs = function(initialData, query) {
272 272 var data = {results: []};
273 273 // filter initialData
274 274 $.each(initialData, function() {
275 275 var section = this.text;
276 276 var children = [];
277 277 $.each(this.children, function() {
278 278 if (query.term.length === 0 ||
279 279 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
280 280 children.push({'id': this.id, 'text': this.text})
281 281 }
282 282 });
283 283 data.results.push({'text': section, 'children': children})
284 284 });
285 285 query.callback({results: data.results});
286 286 };
287 287
288 288 var loadRepoRefDiffPreview = function() {
289 289
290 290 var url_data = {
291 291 'repo_name': targetRepo(),
292 292 'target_repo': sourceRepo(),
293 293 'source_ref': targetRef()[2],
294 294 'source_ref_type': 'rev',
295 295 'target_ref': sourceRef()[2],
296 296 'target_ref_type': 'rev',
297 297 'merge': true,
298 298 '_': Date.now() // bypass browser caching
299 299 }; // gather the source/target ref and repo here
300 300
301 301 if (sourceRef().length !== 3 || targetRef().length !== 3) {
302 302 prButtonLock(true, "${_('Please select source and target')}");
303 303 return;
304 304 }
305 305 var url = pyroutes.url('repo_compare', url_data);
306 306
307 307 // lock PR button, so we cannot send PR before it's calculated
308 308 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
309 309
310 310 if (loadRepoRefDiffPreview._currentRequest) {
311 311 loadRepoRefDiffPreview._currentRequest.abort();
312 312 }
313 313
314 314 loadRepoRefDiffPreview._currentRequest = $.get(url)
315 315 .error(function(data, textStatus, errorThrown) {
316 316 alert(
317 317 "Error while processing request.\nError code {0} ({1}).".format(
318 318 data.status, data.statusText));
319 319 })
320 320 .done(function(data) {
321 321 loadRepoRefDiffPreview._currentRequest = null;
322 322 $('#pull_request_overview').html(data);
323 323
324 324 var commitElements = $(data).find('tr[commit_id]');
325 325
326 326 var prTitleAndDesc = getTitleAndDescription(
327 327 sourceRef()[1], commitElements, 5);
328 328
329 329 var title = prTitleAndDesc[0];
330 330 var proposedDescription = prTitleAndDesc[1];
331 331
332 332 var useGeneratedTitle = (
333 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
334 334 $('#pullrequest_title').val() === "");
335 335
336 336 if (title && useGeneratedTitle) {
337 337 // use generated title if we haven't specified our own
338 338 $('#pullrequest_title').val(title);
339 339 $('#pullrequest_title').addClass('autogenerated-title');
340 340
341 341 }
342 342
343 343 var useGeneratedDescription = (
344 344 !codeMirrorInstance._userDefinedDesc ||
345 345 codeMirrorInstance.getValue() === "");
346 346
347 347 if (proposedDescription && useGeneratedDescription) {
348 348 // set proposed content, if we haven't defined our own,
349 349 // or we don't have description written
350 350 codeMirrorInstance._userDefinedDesc = false; // reset state
351 351 codeMirrorInstance.setValue(proposedDescription);
352 352 }
353 353
354 354 var msg = '';
355 355 if (commitElements.length === 1) {
356 356 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
357 357 } else {
358 358 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
359 359 }
360 360
361 361 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
362 362
363 363 if (commitElements.length) {
364 364 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
365 365 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
366 366 }
367 367 else {
368 368 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
369 369 }
370 370
371 371
372 372 });
373 373 };
374 374
375 375 var Select2Box = function(element, overrides) {
376 376 var globalDefaults = {
377 377 dropdownAutoWidth: true,
378 378 containerCssClass: "drop-menu",
379 379 dropdownCssClass: "drop-menu-dropdown"
380 380 };
381 381
382 382 var initSelect2 = function(defaultOptions) {
383 383 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
384 384 element.select2(options);
385 385 };
386 386
387 387 return {
388 388 initRef: function() {
389 389 var defaultOptions = {
390 390 minimumResultsForSearch: 5,
391 391 formatSelection: formatRefSelection
392 392 };
393 393
394 394 initSelect2(defaultOptions);
395 395 },
396 396
397 397 initRepo: function(defaultValue, readOnly) {
398 398 var defaultOptions = {
399 399 initSelection : function (element, callback) {
400 400 var data = {id: defaultValue, text: defaultValue};
401 401 callback(data);
402 402 }
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406
407 407 element.select2('val', defaultSourceRepo);
408 408 if (readOnly === true) {
409 409 element.select2('readonly', true);
410 410 }
411 411 }
412 412 };
413 413 };
414 414
415 415 var initTargetRefs = function(refsData, selectedRef){
416 416 Select2Box($targetRef, {
417 417 query: function(query) {
418 418 queryTargetRefs(refsData, query);
419 419 },
420 420 initSelection : initRefSelection(selectedRef)
421 421 }).initRef();
422 422
423 423 if (!(selectedRef === undefined)) {
424 424 $targetRef.select2('val', selectedRef);
425 425 }
426 426 };
427 427
428 428 var targetRepoChanged = function(repoData) {
429 429 // generate new DESC of target repo displayed next to select
430 430 $('#target_repo_desc').html(
431 431 "<strong>${_('Target repository')}</strong>: {0}".format(repoData['description'])
432 432 );
433 433
434 434 // generate dynamic select2 for refs.
435 435 initTargetRefs(repoData['refs']['select2_refs'],
436 436 repoData['refs']['selected_ref']);
437 437
438 438 };
439 439
440 440 var sourceRefSelect2 = Select2Box($sourceRef, {
441 441 placeholder: "${_('Select commit reference')}",
442 442 query: function(query) {
443 443 var initialData = defaultSourceRepoData['refs']['select2_refs'];
444 444 queryTargetRefs(initialData, query)
445 445 },
446 446 initSelection: initRefSelection()
447 447 }
448 448 );
449 449
450 450 var sourceRepoSelect2 = Select2Box($sourceRepo, {
451 451 query: function(query) {}
452 452 });
453 453
454 454 var targetRepoSelect2 = Select2Box($targetRepo, {
455 455 cachedDataSource: {},
456 456 query: $.debounce(250, function(query) {
457 457 queryTargetRepo(this, query);
458 458 }),
459 459 formatResult: formatResult
460 460 });
461 461
462 462 sourceRefSelect2.initRef();
463 463
464 464 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
465 465
466 466 targetRepoSelect2.initRepo(defaultTargetRepo, false);
467 467
468 468 $sourceRef.on('change', function(e){
469 469 loadRepoRefDiffPreview();
470 470 reviewersController.loadDefaultReviewers(
471 471 sourceRepo(), sourceRef(), targetRepo(), targetRef());
472 472 });
473 473
474 474 $targetRef.on('change', function(e){
475 475 loadRepoRefDiffPreview();
476 476 reviewersController.loadDefaultReviewers(
477 477 sourceRepo(), sourceRef(), targetRepo(), targetRef());
478 478 });
479 479
480 480 $targetRepo.on('change', function(e){
481 481 var repoName = $(this).val();
482 482 calculateContainerWidth();
483 483 $targetRef.select2('destroy');
484 484 $('#target_ref_loading').show();
485 485
486 486 $.ajax({
487 487 url: pyroutes.url('pullrequest_repo_refs',
488 488 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
489 489 data: {},
490 490 dataType: 'json',
491 491 type: 'GET',
492 492 success: function(data) {
493 493 $('#target_ref_loading').hide();
494 494 targetRepoChanged(data);
495 495 loadRepoRefDiffPreview();
496 496 },
497 497 error: function(data, textStatus, errorThrown) {
498 498 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
499 499 }
500 500 })
501 501
502 502 });
503 503
504 504 prButtonLock(true, "${_('Please select source and target')}", 'all');
505 505
506 506 // auto-load on init, the target refs select2
507 507 calculateContainerWidth();
508 508 targetRepoChanged(defaultTargetRepoData);
509 509
510 510 $('#pullrequest_title').on('keyup', function(e){
511 511 $(this).removeClass('autogenerated-title');
512 512 });
513 513
514 514 % if c.default_source_ref:
515 515 // in case we have a pre-selected value, use it now
516 516 $sourceRef.select2('val', '${c.default_source_ref}');
517 517 loadRepoRefDiffPreview();
518 518 reviewersController.loadDefaultReviewers(
519 519 sourceRepo(), sourceRef(), targetRepo(), targetRef());
520 520 % endif
521 521
522 522 ReviewerAutoComplete('#user');
523 523 });
524 524 </script>
525 525
526 526 </%def>
@@ -1,63 +1,63 b''
1 1
2 2 <div class="pull-request-wrap">
3 3
4 4 % if c.pr_merge_possible:
5 5 <h2 class="merge-status">
6 6 <span class="merge-icon success"><i class="icon-ok"></i></span>
7 7 ${_('This pull request can be merged automatically.')}
8 8 </h2>
9 9 % else:
10 10 <h2 class="merge-status">
11 11 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 12 ${_('Merge is not currently possible because of below failed checks.')}
13 13 </h2>
14 14 % endif
15 15
16 16 <ul>
17 17 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
18 18 <% pr_check_type = pr_check_details['error_type'] %>
19 19 <li>
20 20 <span class="merge-message ${pr_check_type}" data-role="merge-message">
21 21 - ${pr_check_details['message']}
22 22 % if pr_check_key == 'todo':
23 23 % for co in pr_check_details['details']:
24 24 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
25 25 % endfor
26 26 % endif
27 27 </span>
28 28 </li>
29 29 % endfor
30 30 </ul>
31 31
32 32 <div class="pull-request-merge-actions">
33 33 % if c.allowed_to_merge:
34 34 <div class="pull-right">
35 ${h.secure_form(h.url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
35 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', method='POST', request=request)}
36 36 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
37 37 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
38 38 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
39 39 ${h.end_form()}
40 40 </div>
41 41 % elif c.rhodecode_user.username != h.DEFAULT_USER:
42 42 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
43 43 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
44 44 % else:
45 45 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
46 46 % endif
47 47 </div>
48 48
49 49 % if c.allowed_to_close:
50 50 ## close PR action, injected later next to COMMENT button
51 51 <div id="close-pull-request-action" style="display: none">
52 52 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
53 53 <a class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
54 54 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
55 55 </a>
56 56 % else:
57 57 <a class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
58 58 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
59 59 </a>
60 60 % endif
61 61 </div>
62 62 % endif
63 63 </div>
@@ -1,860 +1,860 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 AJAX_COMMENT_DELETE_URL = "${h.url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 ${h.secure_form(h.url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), method='POST', request=request)}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Source')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 78 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 <div class="pr-pullinfo">
93 93 %if h.is_hg(c.pull_request.source_repo):
94 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 95 %elif h.is_git(c.pull_request.source_repo):
96 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 97 %endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label-summary">
103 103 <label>${_('Target')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 <div class="pr-targetinfo">
107 107 ## branch link is only valid if it is a branch
108 108 <span class="tag">
109 109 %if c.pull_request.target_ref_parts.type == 'branch':
110 110 <a href="${h.route_path('repo_changelog', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 111 %else:
112 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 113 %endif
114 114 </span>
115 115 <span class="clone-url">
116 116 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 117 </span>
118 118 </div>
119 119 </div>
120 120 </div>
121 121
122 122 ## Link to the shadow repository.
123 123 <div class="field">
124 124 <div class="label-summary">
125 125 <label>${_('Merge')}:</label>
126 126 </div>
127 127 <div class="input">
128 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 129 <div class="pr-mergeinfo">
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 134 %endif
135 135 </div>
136 136 % else:
137 137 <div class="">
138 138 ${_('Shadow repository data not available')}.
139 139 </div>
140 140 % endif
141 141 </div>
142 142 </div>
143 143
144 144 <div class="field">
145 145 <div class="label-summary">
146 146 <label>${_('Review')}:</label>
147 147 </div>
148 148 <div class="input">
149 149 %if c.pull_request_review_status:
150 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 151 <span class="changeset-status-lbl tooltip">
152 152 %if c.pull_request.is_closed():
153 153 ${_('Closed')},
154 154 %endif
155 155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 156 </span>
157 157 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 158 %endif
159 159 </div>
160 160 </div>
161 161 <div class="field">
162 162 <div class="pr-description-label label-summary">
163 163 <label>${_('Description')}:</label>
164 164 </div>
165 165 <div id="pr-desc" class="input">
166 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 167 </div>
168 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 170 </div>
171 171 </div>
172 172
173 173 <div class="field">
174 174 <div class="label-summary">
175 175 <label>${_('Versions')}:</label>
176 176 </div>
177 177
178 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180 180
181 181 <div class="pr-versions">
182 182 % if c.show_version_changes:
183 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 186 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 188 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 189 </a>
190 190 <table>
191 191 ## SHOW ALL VERSIONS OF PR
192 192 <% ver_pr = None %>
193 193
194 194 % for data in reversed(list(enumerate(c.versions, 1))):
195 195 <% ver_pos = data[0] %>
196 196 <% ver = data[1] %>
197 197 <% ver_pr = ver.pull_request_version_id %>
198 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199 199
200 200 <tr class="version-pr" style="display: ${display_row}">
201 201 <td>
202 202 <code>
203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
203 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
204 204 </code>
205 205 </td>
206 206 <td>
207 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 209 </td>
210 210 <td>
211 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 213 </div>
214 214 </td>
215 215 <td>
216 216 % if c.at_version_num != ver_pr:
217 217 <i class="icon-comment"></i>
218 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 220 </code>
221 221 % endif
222 222 </td>
223 223 <td>
224 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 225 </td>
226 226 <td>
227 227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 228 </td>
229 229 </tr>
230 230 % endfor
231 231
232 232 <tr>
233 233 <td colspan="6">
234 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 235 data-label-text-locked="${_('select versions to show changes')}"
236 236 data-label-text-diff="${_('show changes between versions')}"
237 237 data-label-text-show="${_('show pull request for this version')}"
238 238 >
239 239 ${_('select versions to show changes')}
240 240 </button>
241 241 </td>
242 242 </tr>
243 243
244 244 ## show comment/inline comments summary
245 245 <%def name="comments_summary()">
246 246 <tr>
247 247 <td colspan="6" class="comments-summary-td">
248 248
249 249 % if c.at_version:
250 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 252 ${_('Comments at this version')}:
253 253 % else:
254 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 256 ${_('Comments for this pull request')}:
257 257 % endif
258 258
259 259
260 260 %if general_comm_count_ver:
261 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 262 %else:
263 263 ${_("%d General ") % general_comm_count_ver}
264 264 %endif
265 265
266 266 %if inline_comm_count_ver:
267 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 268 %else:
269 269 , ${_("%d Inline") % inline_comm_count_ver}
270 270 %endif
271 271
272 272 %if outdated_comm_count_ver:
273 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 276 %else:
277 277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 278 %endif
279 279 </td>
280 280 </tr>
281 281 </%def>
282 282 ${comments_summary()}
283 283 </table>
284 284 % else:
285 285 <div class="input">
286 286 ${_('Pull request versions not available')}.
287 287 </div>
288 288 <div>
289 289 <table>
290 290 ${comments_summary()}
291 291 </table>
292 292 </div>
293 293 % endif
294 294 </div>
295 295 </div>
296 296
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 300 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
304 304 </div>
305 305 <div>
306 306 ## AUTHOR
307 307 <div class="reviewers-title block-right">
308 308 <div class="pr-details-title">
309 309 ${_('Author of this pull request')}
310 310 </div>
311 311 </div>
312 312 <div class="block-right pr-details-content reviewers">
313 313 <ul class="group_members">
314 314 <li>
315 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 316 </li>
317 317 </ul>
318 318 </div>
319 319
320 320 ## REVIEW RULES
321 321 <div id="review_rules" style="display: none" class="reviewers-title block-right">
322 322 <div class="pr-details-title">
323 323 ${_('Reviewer rules')}
324 324 %if c.allowed_to_update:
325 325 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
326 326 %endif
327 327 </div>
328 328 <div class="pr-reviewer-rules">
329 329 ## review rules will be appended here, by default reviewers logic
330 330 </div>
331 331 <input id="review_data" type="hidden" name="review_data" value="">
332 332 </div>
333 333
334 334 ## REVIEWERS
335 335 <div class="reviewers-title block-right">
336 336 <div class="pr-details-title">
337 337 ${_('Pull request reviewers')}
338 338 %if c.allowed_to_update:
339 339 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
340 340 %endif
341 341 </div>
342 342 </div>
343 343 <div id="reviewers" class="block-right pr-details-content reviewers">
344 344 ## members goes here !
345 345 <input type="hidden" name="__start__" value="review_members:sequence">
346 346 <ul id="review_members" class="group_members">
347 347 %for member,reasons,mandatory,status in c.pull_request_reviewers:
348 348 <li id="reviewer_${member.user_id}" class="reviewer_entry">
349 349 <div class="reviewers_member">
350 350 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
351 351 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
352 352 </div>
353 353 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
354 354 ${self.gravatar_with_user(member.email, 16)}
355 355 </div>
356 356 <input type="hidden" name="__start__" value="reviewer:mapping">
357 357 <input type="hidden" name="__start__" value="reasons:sequence">
358 358 %for reason in reasons:
359 359 <div class="reviewer_reason">- ${reason}</div>
360 360 <input type="hidden" name="reason" value="${reason}">
361 361
362 362 %endfor
363 363 <input type="hidden" name="__end__" value="reasons:sequence">
364 364 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
365 365 <input type="hidden" name="mandatory" value="${mandatory}"/>
366 366 <input type="hidden" name="__end__" value="reviewer:mapping">
367 367 % if mandatory:
368 368 <div class="reviewer_member_mandatory_remove">
369 369 <i class="icon-remove-sign"></i>
370 370 </div>
371 371 <div class="reviewer_member_mandatory">
372 372 <i class="icon-lock" title="${h.tooltip(_('Mandatory reviewer'))}"></i>
373 373 </div>
374 374 % else:
375 375 %if c.allowed_to_update:
376 376 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
377 377 <i class="icon-remove-sign" ></i>
378 378 </div>
379 379 %endif
380 380 % endif
381 381 </div>
382 382 </li>
383 383 %endfor
384 384 </ul>
385 385 <input type="hidden" name="__end__" value="review_members:sequence">
386 386
387 387 %if not c.pull_request.is_closed():
388 388 <div id="add_reviewer" class="ac" style="display: none;">
389 389 %if c.allowed_to_update:
390 390 % if not c.forbid_adding_reviewers:
391 391 <div id="add_reviewer_input" class="reviewer_ac">
392 392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
393 393 <div id="reviewers_container"></div>
394 394 </div>
395 395 % endif
396 396 <div class="pull-right">
397 397 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
398 398 </div>
399 399 %endif
400 400 </div>
401 401 %endif
402 402 </div>
403 403 </div>
404 404 </div>
405 405 <div class="box">
406 406 ##DIFF
407 407 <div class="table" >
408 408 <div id="changeset_compare_view_content">
409 409 ##CS
410 410 % if c.missing_requirements:
411 411 <div class="box">
412 412 <div class="alert alert-warning">
413 413 <div>
414 414 <strong>${_('Missing requirements:')}</strong>
415 415 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
416 416 </div>
417 417 </div>
418 418 </div>
419 419 % elif c.missing_commits:
420 420 <div class="box">
421 421 <div class="alert alert-warning">
422 422 <div>
423 423 <strong>${_('Missing commits')}:</strong>
424 424 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
425 425 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
426 426 </div>
427 427 </div>
428 428 </div>
429 429 % endif
430 430
431 431 <div class="compare_view_commits_title">
432 432 % if not c.compare_mode:
433 433
434 434 % if c.at_version_pos:
435 435 <h4>
436 436 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
437 437 </h4>
438 438 % endif
439 439
440 440 <div class="pull-left">
441 441 <div class="btn-group">
442 442 <a
443 443 class="btn"
444 444 href="#"
445 445 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
446 446 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
447 447 </a>
448 448 <a
449 449 class="btn"
450 450 href="#"
451 451 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
452 452 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
453 453 </a>
454 454 </div>
455 455 </div>
456 456
457 457 <div class="pull-right">
458 458 % if c.allowed_to_update and not c.pull_request.is_closed():
459 459 <a id="update_commits" class="btn btn-primary no-margin pull-right">${_('Update commits')}</a>
460 460 % else:
461 461 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
462 462 % endif
463 463
464 464 </div>
465 465 % endif
466 466 </div>
467 467
468 468 % if not c.missing_commits:
469 469 % if c.compare_mode:
470 470 % if c.at_version:
471 471 <h4>
472 472 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
473 473 </h4>
474 474
475 475 <div class="subtitle-compare">
476 476 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
477 477 </div>
478 478
479 479 <div class="container">
480 480 <table class="rctable compare_view_commits">
481 481 <tr>
482 482 <th></th>
483 483 <th>${_('Time')}</th>
484 484 <th>${_('Author')}</th>
485 485 <th>${_('Commit')}</th>
486 486 <th></th>
487 487 <th>${_('Description')}</th>
488 488 </tr>
489 489
490 490 % for c_type, commit in c.commit_changes:
491 491 % if c_type in ['a', 'r']:
492 492 <%
493 493 if c_type == 'a':
494 494 cc_title = _('Commit added in displayed changes')
495 495 elif c_type == 'r':
496 496 cc_title = _('Commit removed in displayed changes')
497 497 else:
498 498 cc_title = ''
499 499 %>
500 500 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
501 501 <td>
502 502 <div class="commit-change-indicator color-${c_type}-border">
503 503 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
504 504 ${c_type.upper()}
505 505 </div>
506 506 </div>
507 507 </td>
508 508 <td class="td-time">
509 509 ${h.age_component(commit.date)}
510 510 </td>
511 511 <td class="td-user">
512 512 ${base.gravatar_with_user(commit.author, 16)}
513 513 </td>
514 514 <td class="td-hash">
515 515 <code>
516 516 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
517 517 r${commit.revision}:${h.short_id(commit.raw_id)}
518 518 </a>
519 519 ${h.hidden('revisions', commit.raw_id)}
520 520 </code>
521 521 </td>
522 522 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
523 523 <div class="show_more_col">
524 524 <i class="show_more"></i>
525 525 </div>
526 526 </td>
527 527 <td class="mid td-description">
528 528 <div class="log-container truncate-wrap">
529 529 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
530 530 ${h.urlify_commit_message(commit.message, c.repo_name)}
531 531 </div>
532 532 </div>
533 533 </td>
534 534 </tr>
535 535 % endif
536 536 % endfor
537 537 </table>
538 538 </div>
539 539
540 540 <script>
541 541 $('.expand_commit').on('click',function(e){
542 542 var target_expand = $(this);
543 543 var cid = target_expand.data('commitId');
544 544
545 545 if (target_expand.hasClass('open')){
546 546 $('#c-'+cid).css({
547 547 'height': '1.5em',
548 548 'white-space': 'nowrap',
549 549 'text-overflow': 'ellipsis',
550 550 'overflow':'hidden'
551 551 });
552 552 target_expand.removeClass('open');
553 553 }
554 554 else {
555 555 $('#c-'+cid).css({
556 556 'height': 'auto',
557 557 'white-space': 'pre-line',
558 558 'text-overflow': 'initial',
559 559 'overflow':'visible'
560 560 });
561 561 target_expand.addClass('open');
562 562 }
563 563 });
564 564 </script>
565 565
566 566 % endif
567 567
568 568 % else:
569 569 <%include file="/compare/compare_commits.mako" />
570 570 % endif
571 571
572 572 <div class="cs_files">
573 573 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
574 574 ${cbdiffs.render_diffset_menu()}
575 575 ${cbdiffs.render_diffset(
576 576 c.diffset, use_comments=True,
577 577 collapse_when_files_over=30,
578 578 disable_new_comments=not c.allowed_to_comment,
579 579 deleted_files_comments=c.deleted_files_comments)}
580 580 </div>
581 581 % else:
582 582 ## skipping commits we need to clear the view for missing commits
583 583 <div style="clear:both;"></div>
584 584 % endif
585 585
586 586 </div>
587 587 </div>
588 588
589 589 ## template for inline comment form
590 590 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
591 591
592 592 ## render general comments
593 593
594 594 <div id="comment-tr-show">
595 595 <div class="comment">
596 596 % if general_outdated_comm_count_ver:
597 597 <div class="meta">
598 598 % if general_outdated_comm_count_ver == 1:
599 599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
600 600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
601 601 % else:
602 602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
603 603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
604 604 % endif
605 605 </div>
606 606 % endif
607 607 </div>
608 608 </div>
609 609
610 610 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
611 611
612 612 % if not c.pull_request.is_closed():
613 613 ## merge status, and merge action
614 614 <div class="pull-request-merge">
615 615 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
616 616 </div>
617 617
618 618 ## main comment form and it status
619 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
619 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
620 620 pull_request_id=c.pull_request.pull_request_id),
621 621 c.pull_request_review_status,
622 622 is_pull_request=True, change_status=c.allowed_to_change_status)}
623 623 %endif
624 624
625 625 <script type="text/javascript">
626 626 if (location.hash) {
627 627 var result = splitDelimitedHash(location.hash);
628 628 var line = $('html').find(result.loc);
629 629 // show hidden comments if we use location.hash
630 630 if (line.hasClass('comment-general')) {
631 631 $(line).show();
632 632 } else if (line.hasClass('comment-inline')) {
633 633 $(line).show();
634 634 var $cb = $(line).closest('.cb');
635 635 $cb.removeClass('cb-collapsed')
636 636 }
637 637 if (line.length > 0){
638 638 offsetScroll(line, 70);
639 639 }
640 640 }
641 641
642 642 versionController = new VersionController();
643 643 versionController.init();
644 644
645 645 reviewersController = new ReviewersController();
646 646
647 647 $(function(){
648 648
649 649 // custom code mirror
650 650 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
651 651
652 652 var PRDetails = {
653 653 editButton: $('#open_edit_pullrequest'),
654 654 closeButton: $('#close_edit_pullrequest'),
655 655 deleteButton: $('#delete_pullrequest'),
656 656 viewFields: $('#pr-desc, #pr-title'),
657 657 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
658 658
659 659 init: function() {
660 660 var that = this;
661 661 this.editButton.on('click', function(e) { that.edit(); });
662 662 this.closeButton.on('click', function(e) { that.view(); });
663 663 },
664 664
665 665 edit: function(event) {
666 666 this.viewFields.hide();
667 667 this.editButton.hide();
668 668 this.deleteButton.hide();
669 669 this.closeButton.show();
670 670 this.editFields.show();
671 671 codeMirrorInstance.refresh();
672 672 },
673 673
674 674 view: function(event) {
675 675 this.editButton.show();
676 676 this.deleteButton.show();
677 677 this.editFields.hide();
678 678 this.closeButton.hide();
679 679 this.viewFields.show();
680 680 }
681 681 };
682 682
683 683 var ReviewersPanel = {
684 684 editButton: $('#open_edit_reviewers'),
685 685 closeButton: $('#close_edit_reviewers'),
686 686 addButton: $('#add_reviewer'),
687 687 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'),
688 688
689 689 init: function() {
690 690 var self = this;
691 691 this.editButton.on('click', function(e) { self.edit(); });
692 692 this.closeButton.on('click', function(e) { self.close(); });
693 693 },
694 694
695 695 edit: function(event) {
696 696 this.editButton.hide();
697 697 this.closeButton.show();
698 698 this.addButton.show();
699 699 this.removeButtons.css('visibility', 'visible');
700 700 // review rules
701 701 reviewersController.loadReviewRules(
702 702 ${c.pull_request.reviewer_data_json | n});
703 703 },
704 704
705 705 close: function(event) {
706 706 this.editButton.show();
707 707 this.closeButton.hide();
708 708 this.addButton.hide();
709 709 this.removeButtons.css('visibility', 'hidden');
710 710 // hide review rules
711 711 reviewersController.hideReviewRules()
712 712 }
713 713 };
714 714
715 715 PRDetails.init();
716 716 ReviewersPanel.init();
717 717
718 718 showOutdated = function(self){
719 719 $('.comment-inline.comment-outdated').show();
720 720 $('.filediff-outdated').show();
721 721 $('.showOutdatedComments').hide();
722 722 $('.hideOutdatedComments').show();
723 723 };
724 724
725 725 hideOutdated = function(self){
726 726 $('.comment-inline.comment-outdated').hide();
727 727 $('.filediff-outdated').hide();
728 728 $('.hideOutdatedComments').hide();
729 729 $('.showOutdatedComments').show();
730 730 };
731 731
732 732 refreshMergeChecks = function(){
733 var loadUrl = "${h.url.current(merge_checks=1)}";
733 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
734 734 $('.pull-request-merge').css('opacity', 0.3);
735 735 $('.action-buttons-extra').css('opacity', 0.3);
736 736
737 737 $('.pull-request-merge').load(
738 738 loadUrl, function() {
739 739 $('.pull-request-merge').css('opacity', 1);
740 740
741 741 $('.action-buttons-extra').css('opacity', 1);
742 742 injectCloseAction();
743 743 }
744 744 );
745 745 };
746 746
747 747 injectCloseAction = function() {
748 748 var closeAction = $('#close-pull-request-action').html();
749 749 var $actionButtons = $('.action-buttons-extra');
750 750 // clear the action before
751 751 $actionButtons.html("");
752 752 $actionButtons.html(closeAction);
753 753 };
754 754
755 755 closePullRequest = function (status) {
756 756 // inject closing flag
757 757 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
758 758 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
759 759 $(generalCommentForm.submitForm).submit();
760 760 };
761 761
762 762 $('#show-outdated-comments').on('click', function(e){
763 763 var button = $(this);
764 764 var outdated = $('.comment-outdated');
765 765
766 766 if (button.html() === "(Show)") {
767 767 button.html("(Hide)");
768 768 outdated.show();
769 769 } else {
770 770 button.html("(Show)");
771 771 outdated.hide();
772 772 }
773 773 });
774 774
775 775 $('.show-inline-comments').on('change', function(e){
776 776 var show = 'none';
777 777 var target = e.currentTarget;
778 778 if(target.checked){
779 779 show = ''
780 780 }
781 781 var boxid = $(target).attr('id_for');
782 782 var comments = $('#{0} .inline-comments'.format(boxid));
783 783 var fn_display = function(idx){
784 784 $(this).css('display', show);
785 785 };
786 786 $(comments).each(fn_display);
787 787 var btns = $('#{0} .inline-comments-button'.format(boxid));
788 788 $(btns).each(fn_display);
789 789 });
790 790
791 791 $('#merge_pull_request_form').submit(function() {
792 792 if (!$('#merge_pull_request').attr('disabled')) {
793 793 $('#merge_pull_request').attr('disabled', 'disabled');
794 794 }
795 795 return true;
796 796 });
797 797
798 798 $('#edit_pull_request').on('click', function(e){
799 799 var title = $('#pr-title-input').val();
800 800 var description = codeMirrorInstance.getValue();
801 801 editPullRequest(
802 802 "${c.repo_name}", "${c.pull_request.pull_request_id}",
803 803 title, description);
804 804 });
805 805
806 806 $('#update_pull_request').on('click', function(e){
807 807 $(this).attr('disabled', 'disabled');
808 808 $(this).addClass('disabled');
809 809 $(this).html(_gettext('Saving...'));
810 810 reviewersController.updateReviewers(
811 811 "${c.repo_name}", "${c.pull_request.pull_request_id}");
812 812 });
813 813
814 814 $('#update_commits').on('click', function(e){
815 815 var isDisabled = !$(e.currentTarget).attr('disabled');
816 816 $(e.currentTarget).attr('disabled', 'disabled');
817 817 $(e.currentTarget).addClass('disabled');
818 818 $(e.currentTarget).removeClass('btn-primary');
819 819 $(e.currentTarget).text(_gettext('Updating...'));
820 820 if(isDisabled){
821 821 updateCommits(
822 822 "${c.repo_name}", "${c.pull_request.pull_request_id}");
823 823 }
824 824 });
825 825 // fixing issue with caches on firefox
826 826 $('#update_commits').removeAttr("disabled");
827 827
828 828 $('.show-inline-comments').on('click', function(e){
829 829 var boxid = $(this).attr('data-comment-id');
830 830 var button = $(this);
831 831
832 832 if(button.hasClass("comments-visible")) {
833 833 $('#{0} .inline-comments'.format(boxid)).each(function(index){
834 834 $(this).hide();
835 835 });
836 836 button.removeClass("comments-visible");
837 837 } else {
838 838 $('#{0} .inline-comments'.format(boxid)).each(function(index){
839 839 $(this).show();
840 840 });
841 841 button.addClass("comments-visible");
842 842 }
843 843 });
844 844
845 845 // register submit callback on commentForm form to track TODOs
846 846 window.commentFormGlobalSubmitSuccessCallback = function(){
847 847 refreshMergeChecks();
848 848 };
849 849 // initial injection
850 850 injectCloseAction();
851 851
852 852 ReviewerAutoComplete('#user');
853 853
854 854 })
855 855 </script>
856 856
857 857 </div>
858 858 </div>
859 859
860 860 </%def>
@@ -1,146 +1,146 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Requests') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11
12 12 </%def>
13 13
14 14 <%def name="menu_bar_nav()">
15 15 ${self.menu_items(active='repositories')}
16 16 </%def>
17 17
18 18
19 19 <%def name="menu_bar_subnav()">
20 20 ${self.repo_menu(active='showpullrequest')}
21 21 </%def>
22 22
23 23
24 24 <%def name="main()">
25 25 <div class="box">
26 26 <div class="title">
27 27 ${self.repo_page_title(c.rhodecode_db_repo)}
28 28
29 29 <ul class="links">
30 30 <li>
31 31 %if c.rhodecode_user.username != h.DEFAULT_USER:
32 32 <span>
33 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
33 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
34 34 ${_('Open new Pull Request')}
35 35 </a>
36 36 </span>
37 37 %endif
38 38 </li>
39 39 </ul>
40 40
41 41 ${self.breadcrumbs()}
42 42 </div>
43 43
44 44 <div class="sidebar-col-wrapper">
45 45 ##main
46 46 <div class="sidebar">
47 47 <ul class="nav nav-pills nav-stacked">
48 48 <li class="${'active' if c.active=='open' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
49 49 <li class="${'active' if c.active=='my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
50 50 <li class="${'active' if c.active=='awaiting' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
51 51 <li class="${'active' if c.active=='awaiting_my' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
52 52 <li class="${'active' if c.active=='closed' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
53 53 <li class="${'active' if c.active=='source' else ''}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
54 54 </ul>
55 55 </div>
56 56
57 57 <div class="main-content-full-width">
58 58 <div class="panel panel-default">
59 59 <div class="panel-heading">
60 60 <h3 class="panel-title">
61 61 %if c.source:
62 62 ${_('Pull Requests from %(repo_name)s repository') % {'repo_name': c.repo_name}}
63 63 %elif c.closed:
64 64 ${_('Closed Pull Requests to repository %(repo_name)s') % {'repo_name': c.repo_name}}
65 65 %elif c.my:
66 66 ${_('Pull Requests to %(repo_name)s repository opened by me') % {'repo_name': c.repo_name}}
67 67 %elif c.awaiting_review:
68 68 ${_('Pull Requests to %(repo_name)s repository awaiting review') % {'repo_name': c.repo_name}}
69 69 %elif c.awaiting_my_review:
70 70 ${_('Pull Requests to %(repo_name)s repository awaiting my review') % {'repo_name': c.repo_name}}
71 71 %else:
72 72 ${_('Pull Requests to %(repo_name)s repository') % {'repo_name': c.repo_name}}
73 73 %endif
74 74 </h3>
75 75 </div>
76 76 <div class="panel-body panel-body-min-height">
77 77 <table id="pull_request_list_table" class="display"></table>
78 78 </div>
79 79 </div>
80 80 </div>
81 81 </div>
82 82 </div>
83 83
84 84 <script type="text/javascript">
85 85 $(document).ready(function() {
86 86
87 87 var $pullRequestListTable = $('#pull_request_list_table');
88 88
89 89 // object list
90 90 $pullRequestListTable.DataTable({
91 91 processing: true,
92 92 serverSide: true,
93 93 ajax: {
94 94 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
95 95 "data": function (d) {
96 96 d.source = "${c.source}";
97 97 d.closed = "${c.closed}";
98 98 d.my = "${c.my}";
99 99 d.awaiting_review = "${c.awaiting_review}";
100 100 d.awaiting_my_review = "${c.awaiting_my_review}";
101 101 }
102 102 },
103 103 dom: 'rtp',
104 104 pageLength: ${c.visual.dashboard_items},
105 105 order: [[ 1, "desc" ]],
106 106 columns: [
107 107 { data: {"_": "status",
108 108 "sort": "status"}, title: "", className: "td-status", orderable: false},
109 109 { data: {"_": "name",
110 110 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
111 111 { data: {"_": "author",
112 112 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
113 113 { data: {"_": "title",
114 114 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
115 115 { data: {"_": "comments",
116 116 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
117 117 { data: {"_": "updated_on",
118 118 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
119 119 ],
120 120 language: {
121 121 paginate: DEFAULT_GRID_PAGINATION,
122 122 sProcessing: _gettext('loading...'),
123 123 emptyTable: _gettext("No pull requests available yet.")
124 124 },
125 125 "drawCallback": function( settings, json ) {
126 126 timeagoActivate();
127 127 },
128 128 "createdRow": function ( row, data, index ) {
129 129 if (data['closed']) {
130 130 $(row).addClass('closed');
131 131 }
132 132 }
133 133 });
134 134
135 135 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
136 136 $pullRequestListTable.css('opacity', 1);
137 137 });
138 138
139 139 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
140 140 $pullRequestListTable.css('opacity', 0.3);
141 141 });
142 142
143 143 });
144 144
145 145 </script>
146 146 </%def>
@@ -1,268 +1,269 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import (
24 24 TestController, url, assert_session_flash, link_to, TEST_USER_ADMIN_LOGIN)
25 25 from rhodecode.model.db import User, UserGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 TEST_USER_GROUP = 'admins_test'
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 class TestAdminUsersGroupsController(TestController):
35 35
36 36 def test_index(self):
37 37 self.log_user()
38 38 response = self.app.get(url('users_groups'))
39 39 assert response.status_int == 200
40 40
41 41 def test_create(self):
42 42 self.log_user()
43 43 users_group_name = TEST_USER_GROUP
44 44 response = self.app.post(url('users_groups'), {
45 45 'users_group_name': users_group_name,
46 46 'user_group_description': 'DESC',
47 47 'active': True,
48 48 'csrf_token': self.csrf_token})
49 49
50 50 user_group_link = link_to(
51 51 users_group_name,
52 52 url('edit_users_group',
53 53 user_group_id=UserGroup.get_by_group_name(
54 54 users_group_name).users_group_id))
55 55 assert_session_flash(
56 56 response,
57 57 'Created user group %s' % user_group_link)
58 58
59 59 def test_set_synchronization(self):
60 60 self.log_user()
61 61 users_group_name = TEST_USER_GROUP + 'sync'
62 62 response = self.app.post(url('users_groups'), {
63 63 'users_group_name': users_group_name,
64 64 'user_group_description': 'DESC',
65 65 'active': True,
66 66 'csrf_token': self.csrf_token})
67 67
68 68 group = Session().query(UserGroup).filter(
69 69 UserGroup.users_group_name == users_group_name).one()
70 70
71 71 assert group.group_data.get('extern_type') is None
72 72
73 73 # enable
74 74 self.app.post(
75 75 url('edit_user_group_advanced_sync', user_group_id=group.users_group_id),
76 76 params={'csrf_token': self.csrf_token}, status=302)
77 77
78 78 group = Session().query(UserGroup).filter(
79 79 UserGroup.users_group_name == users_group_name).one()
80 80 assert group.group_data.get('extern_type') == 'manual'
81 81 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
82 82
83 83 # disable
84 84 self.app.post(
85 85 url('edit_user_group_advanced_sync',
86 86 user_group_id=group.users_group_id),
87 87 params={'csrf_token': self.csrf_token}, status=302)
88 88
89 89 group = Session().query(UserGroup).filter(
90 90 UserGroup.users_group_name == users_group_name).one()
91 91 assert group.group_data.get('extern_type') is None
92 92 assert group.group_data.get('extern_type_set_by') == TEST_USER_ADMIN_LOGIN
93 93
94 94 def test_delete(self):
95 95 self.log_user()
96 96 users_group_name = TEST_USER_GROUP + 'another'
97 97 response = self.app.post(url('users_groups'), {
98 98 'users_group_name': users_group_name,
99 99 'user_group_description': 'DESC',
100 100 'active': True,
101 101 'csrf_token': self.csrf_token})
102 102
103 103 user_group_link = link_to(
104 104 users_group_name,
105 105 url('edit_users_group',
106 106 user_group_id=UserGroup.get_by_group_name(
107 107 users_group_name).users_group_id))
108 108 assert_session_flash(
109 109 response,
110 110 'Created user group %s' % user_group_link)
111 111
112 112 group = Session().query(UserGroup).filter(
113 113 UserGroup.users_group_name == users_group_name).one()
114 114
115 115 self.app.post(
116 116 url('delete_users_group', user_group_id=group.users_group_id),
117 117 params={'_method': 'delete', 'csrf_token': self.csrf_token})
118 118
119 119 group = Session().query(UserGroup).filter(
120 120 UserGroup.users_group_name == users_group_name).scalar()
121 121
122 122 assert group is None
123 123
124 124 @pytest.mark.parametrize('repo_create, repo_create_write, user_group_create, repo_group_create, fork_create, inherit_default_permissions, expect_error, expect_form_error', [
125 125 ('hg.create.none', 'hg.create.write_on_repogroup.false', 'hg.usergroup.create.false', 'hg.repogroup.create.false', 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
126 126 ('hg.create.repository', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, False),
127 127 ('hg.create.XXX', 'hg.create.write_on_repogroup.true', 'hg.usergroup.create.true', 'hg.repogroup.create.true', 'hg.fork.repository', 'hg.inherit_default_perms.false', False, True),
128 128 ('', '', '', '', '', '', True, False),
129 129 ])
130 130 def test_global_perms_on_group(
131 131 self, repo_create, repo_create_write, user_group_create,
132 132 repo_group_create, fork_create, expect_error, expect_form_error,
133 133 inherit_default_permissions):
134 134 self.log_user()
135 135 users_group_name = TEST_USER_GROUP + 'another2'
136 136 response = self.app.post(url('users_groups'),
137 137 {'users_group_name': users_group_name,
138 138 'user_group_description': 'DESC',
139 139 'active': True,
140 140 'csrf_token': self.csrf_token})
141 141
142 142 ug = UserGroup.get_by_group_name(users_group_name)
143 143 user_group_link = link_to(
144 144 users_group_name,
145 145 url('edit_users_group', user_group_id=ug.users_group_id))
146 146 assert_session_flash(
147 147 response,
148 148 'Created user group %s' % user_group_link)
149 149 response.follow()
150 150
151 151 # ENABLE REPO CREATE ON A GROUP
152 152 perm_params = {
153 153 'inherit_default_permissions': False,
154 154 'default_repo_create': repo_create,
155 155 'default_repo_create_on_write': repo_create_write,
156 156 'default_user_group_create': user_group_create,
157 157 'default_repo_group_create': repo_group_create,
158 158 'default_fork_create': fork_create,
159 159 'default_inherit_default_permissions': inherit_default_permissions,
160 160
161 161 '_method': 'put',
162 162 'csrf_token': self.csrf_token,
163 163 }
164 164 response = self.app.post(
165 165 url('edit_user_group_global_perms',
166 166 user_group_id=ug.users_group_id),
167 167 params=perm_params)
168 168
169 169 if expect_form_error:
170 170 assert response.status_int == 200
171 171 response.mustcontain('Value must be one of')
172 172 else:
173 173 if expect_error:
174 174 msg = 'An error occurred during permissions saving'
175 175 else:
176 176 msg = 'User Group global permissions updated successfully'
177 177 ug = UserGroup.get_by_group_name(users_group_name)
178 178 del perm_params['_method']
179 179 del perm_params['csrf_token']
180 180 del perm_params['inherit_default_permissions']
181 181 assert perm_params == ug.get_default_perms()
182 182 assert_session_flash(response, msg)
183 183
184 184 fixture.destroy_user_group(users_group_name)
185 185
186 186 def test_edit_autocomplete(self):
187 187 self.log_user()
188 188 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
189 189 response = self.app.get(
190 190 url('edit_users_group', user_group_id=ug.users_group_id))
191 191 fixture.destroy_user_group(TEST_USER_GROUP)
192 192
193 193 def test_edit_user_group_autocomplete_members(self, xhr_header):
194 194 self.log_user()
195 195 ug = fixture.create_user_group(TEST_USER_GROUP, skip_if_exists=True)
196 196 response = self.app.get(
197 197 url('edit_user_group_members', user_group_id=ug.users_group_id),
198 198 extra_environ=xhr_header)
199 199
200 200 assert response.body == '{"members": []}'
201 201 fixture.destroy_user_group(TEST_USER_GROUP)
202 202
203 def test_usergroup_escape(self):
204 user = User.get_by_username('test_admin')
205 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
206 user.lastname = (
207 '<img src="/image2" onload="alert(\'Hello, World!\');">')
208 Session().add(user)
209 Session().commit()
203 def test_usergroup_escape(self, user_util):
204 user = user_util.create_user(
205 username='escape_user',
206 firstname='<img src="/image2" onload="alert(\'Hello, World!\');">',
207 lastname='<img src="/image2" onload="alert(\'Hello, World!\');">'
208 )
209
210 user_util.create_user_group(owner=user.username)
210 211
211 212 self.log_user()
212 213 users_group_name = 'samplegroup'
213 214 data = {
214 215 'users_group_name': users_group_name,
215 216 'user_group_description': (
216 217 '<strong onload="alert();">DESC</strong>'),
217 218 'active': True,
218 219 'csrf_token': self.csrf_token
219 220 }
220 221
221 222 self.app.post(url('users_groups'), data)
222 223 response = self.app.get(url('users_groups'))
223 224
224 225 response.mustcontain(
225 226 '&lt;strong onload=&#34;alert();&#34;&gt;'
226 227 'DESC&lt;/strong&gt;')
227 228 response.mustcontain(
228 229 '&lt;img src=&#34;/image2&#34; onload=&#34;'
229 230 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
230 231
231 232 def test_update_members_from_user_ids(self, user_regular):
232 233 uid = user_regular.user_id
233 234 username = user_regular.username
234 235 self.log_user()
235 236
236 237 user_group = fixture.create_user_group('test_gr_ids')
237 238 assert user_group.members == []
238 239 assert user_group.user != user_regular
239 240 expected_active_state = not user_group.users_group_active
240 241
241 242 form_data = [
242 243 ('csrf_token', self.csrf_token),
243 244 ('_method', 'put'),
244 245 ('user', username),
245 246 ('users_group_name', 'changed_name'),
246 247 ('users_group_active', expected_active_state),
247 248 ('user_group_description', 'changed_description'),
248 249
249 250 ('__start__', 'user_group_members:sequence'),
250 251 ('__start__', 'member:mapping'),
251 252 ('member_user_id', uid),
252 253 ('type', 'existing'),
253 254 ('__end__', 'member:mapping'),
254 255 ('__end__', 'user_group_members:sequence'),
255 256 ]
256 257 ugid = user_group.users_group_id
257 258 self.app.post(url('update_users_group', user_group_id=ugid), form_data)
258 259
259 260 user_group = UserGroup.get(ugid)
260 261 assert user_group
261 262
262 263 assert user_group.members[0].user_id == uid
263 264 assert user_group.user_id == uid
264 265 assert 'changed_name' in user_group.users_group_name
265 266 assert 'changed_description' in user_group.user_group_description
266 267 assert user_group.users_group_active == expected_active_state
267 268
268 269 fixture.destroy_user_group(user_group)
1 NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (1018 lines changed) Show them Hide them
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now