##// END OF EJS Templates
pull-requests: allow having repo targets all forks and parent forks of target....
marcink -
r3330:7e8ec062 default
parent child Browse files
Show More
@@ -1,483 +1,483 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 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 # repo creating checks, special cases that aren't repo routes
26 26 config.add_route(
27 27 name='repo_creating',
28 28 pattern='/{repo_name:.*?[^/]}/repo_creating')
29 29
30 30 config.add_route(
31 31 name='repo_creating_check',
32 32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
33 33
34 34 # Summary
35 35 # NOTE(marcink): one additional route is defined in very bottom, catch
36 36 # all pattern
37 37 config.add_route(
38 38 name='repo_summary_explicit',
39 39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
40 40 config.add_route(
41 41 name='repo_summary_commits',
42 42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
43 43
44 44 # Commits
45 45 config.add_route(
46 46 name='repo_commit',
47 47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
48 48
49 49 config.add_route(
50 50 name='repo_commit_children',
51 51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
52 52
53 53 config.add_route(
54 54 name='repo_commit_parents',
55 55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
56 56
57 57 config.add_route(
58 58 name='repo_commit_raw',
59 59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
60 60
61 61 config.add_route(
62 62 name='repo_commit_patch',
63 63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
64 64
65 65 config.add_route(
66 66 name='repo_commit_download',
67 67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
68 68
69 69 config.add_route(
70 70 name='repo_commit_data',
71 71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
72 72
73 73 config.add_route(
74 74 name='repo_commit_comment_create',
75 75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
76 76
77 77 config.add_route(
78 78 name='repo_commit_comment_preview',
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 81 config.add_route(
82 82 name='repo_commit_comment_delete',
83 83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
84 84
85 85 # still working url for backward compat.
86 86 config.add_route(
87 87 name='repo_commit_raw_deprecated',
88 88 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
89 89
90 90 # Files
91 91 config.add_route(
92 92 name='repo_archivefile',
93 93 pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
94 94
95 95 config.add_route(
96 96 name='repo_files_diff',
97 97 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
98 98 config.add_route( # legacy route to make old links work
99 99 name='repo_files_diff_2way_redirect',
100 100 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
101 101
102 102 config.add_route(
103 103 name='repo_files',
104 104 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
105 105 config.add_route(
106 106 name='repo_files:default_path',
107 107 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
108 108 config.add_route(
109 109 name='repo_files:default_commit',
110 110 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
111 111
112 112 config.add_route(
113 113 name='repo_files:rendered',
114 114 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
115 115
116 116 config.add_route(
117 117 name='repo_files:annotated',
118 118 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
119 119 config.add_route(
120 120 name='repo_files:annotated_previous',
121 121 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
122 122
123 123 config.add_route(
124 124 name='repo_nodetree_full',
125 125 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
126 126 config.add_route(
127 127 name='repo_nodetree_full:default_path',
128 128 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
129 129
130 130 config.add_route(
131 131 name='repo_files_nodelist',
132 132 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
133 133
134 134 config.add_route(
135 135 name='repo_file_raw',
136 136 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
137 137
138 138 config.add_route(
139 139 name='repo_file_download',
140 140 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
141 141 config.add_route( # backward compat to keep old links working
142 142 name='repo_file_download:legacy',
143 143 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
144 144 repo_route=True)
145 145
146 146 config.add_route(
147 147 name='repo_file_history',
148 148 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
149 149
150 150 config.add_route(
151 151 name='repo_file_authors',
152 152 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
153 153
154 154 config.add_route(
155 155 name='repo_files_remove_file',
156 156 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
157 157 repo_route=True)
158 158 config.add_route(
159 159 name='repo_files_delete_file',
160 160 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
161 161 repo_route=True)
162 162 config.add_route(
163 163 name='repo_files_edit_file',
164 164 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
165 165 repo_route=True)
166 166 config.add_route(
167 167 name='repo_files_update_file',
168 168 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
169 169 repo_route=True)
170 170 config.add_route(
171 171 name='repo_files_add_file',
172 172 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
173 173 repo_route=True)
174 174 config.add_route(
175 175 name='repo_files_create_file',
176 176 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
177 177 repo_route=True)
178 178
179 179 # Refs data
180 180 config.add_route(
181 181 name='repo_refs_data',
182 182 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
183 183
184 184 config.add_route(
185 185 name='repo_refs_changelog_data',
186 186 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
187 187
188 188 config.add_route(
189 189 name='repo_stats',
190 190 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
191 191
192 192 # Changelog
193 193 config.add_route(
194 194 name='repo_changelog',
195 195 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
196 196 config.add_route(
197 197 name='repo_changelog_file',
198 198 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
199 199 config.add_route(
200 200 name='repo_changelog_elements',
201 201 pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
202 202 config.add_route(
203 203 name='repo_changelog_elements_file',
204 204 pattern='/{repo_name:.*?[^/]}/changelog_elements/{commit_id}/{f_path:.*}', repo_route=True)
205 205
206 206 # Compare
207 207 config.add_route(
208 208 name='repo_compare_select',
209 209 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
210 210
211 211 config.add_route(
212 212 name='repo_compare',
213 213 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
214 214
215 215 # Tags
216 216 config.add_route(
217 217 name='tags_home',
218 218 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
219 219
220 220 # Branches
221 221 config.add_route(
222 222 name='branches_home',
223 223 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
224 224
225 225 # Bookmarks
226 226 config.add_route(
227 227 name='bookmarks_home',
228 228 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
229 229
230 230 # Forks
231 231 config.add_route(
232 232 name='repo_fork_new',
233 233 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
234 234 repo_forbid_when_archived=True,
235 235 repo_accepted_types=['hg', 'git'])
236 236
237 237 config.add_route(
238 238 name='repo_fork_create',
239 239 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
240 240 repo_forbid_when_archived=True,
241 241 repo_accepted_types=['hg', 'git'])
242 242
243 243 config.add_route(
244 244 name='repo_forks_show_all',
245 245 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
246 246 repo_accepted_types=['hg', 'git'])
247 247 config.add_route(
248 248 name='repo_forks_data',
249 249 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
250 250 repo_accepted_types=['hg', 'git'])
251 251
252 252 # Pull Requests
253 253 config.add_route(
254 254 name='pullrequest_show',
255 255 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
256 256 repo_route=True)
257 257
258 258 config.add_route(
259 259 name='pullrequest_show_all',
260 260 pattern='/{repo_name:.*?[^/]}/pull-request',
261 261 repo_route=True, repo_accepted_types=['hg', 'git'])
262 262
263 263 config.add_route(
264 264 name='pullrequest_show_all_data',
265 265 pattern='/{repo_name:.*?[^/]}/pull-request-data',
266 266 repo_route=True, repo_accepted_types=['hg', 'git'])
267 267
268 268 config.add_route(
269 269 name='pullrequest_repo_refs',
270 270 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
271 271 repo_route=True)
272 272
273 273 config.add_route(
274 name='pullrequest_repo_destinations',
275 pattern='/{repo_name:.*?[^/]}/pull-request/repo-destinations',
274 name='pullrequest_repo_targets',
275 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
276 276 repo_route=True)
277 277
278 278 config.add_route(
279 279 name='pullrequest_new',
280 280 pattern='/{repo_name:.*?[^/]}/pull-request/new',
281 281 repo_route=True, repo_accepted_types=['hg', 'git'],
282 282 repo_forbid_when_archived=True)
283 283
284 284 config.add_route(
285 285 name='pullrequest_create',
286 286 pattern='/{repo_name:.*?[^/]}/pull-request/create',
287 287 repo_route=True, repo_accepted_types=['hg', 'git'],
288 288 repo_forbid_when_archived=True)
289 289
290 290 config.add_route(
291 291 name='pullrequest_update',
292 292 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
293 293 repo_route=True, repo_forbid_when_archived=True)
294 294
295 295 config.add_route(
296 296 name='pullrequest_merge',
297 297 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
298 298 repo_route=True, repo_forbid_when_archived=True)
299 299
300 300 config.add_route(
301 301 name='pullrequest_delete',
302 302 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
303 303 repo_route=True, repo_forbid_when_archived=True)
304 304
305 305 config.add_route(
306 306 name='pullrequest_comment_create',
307 307 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
308 308 repo_route=True)
309 309
310 310 config.add_route(
311 311 name='pullrequest_comment_delete',
312 312 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
313 313 repo_route=True, repo_accepted_types=['hg', 'git'])
314 314
315 315 # Settings
316 316 config.add_route(
317 317 name='edit_repo',
318 318 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
319 319 # update is POST on edit_repo
320 320
321 321 # Settings advanced
322 322 config.add_route(
323 323 name='edit_repo_advanced',
324 324 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
325 325 config.add_route(
326 326 name='edit_repo_advanced_archive',
327 327 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
328 328 config.add_route(
329 329 name='edit_repo_advanced_delete',
330 330 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
331 331 config.add_route(
332 332 name='edit_repo_advanced_locking',
333 333 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
334 334 config.add_route(
335 335 name='edit_repo_advanced_journal',
336 336 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
337 337 config.add_route(
338 338 name='edit_repo_advanced_fork',
339 339 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
340 340
341 341 config.add_route(
342 342 name='edit_repo_advanced_hooks',
343 343 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
344 344
345 345 # Caches
346 346 config.add_route(
347 347 name='edit_repo_caches',
348 348 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
349 349
350 350 # Permissions
351 351 config.add_route(
352 352 name='edit_repo_perms',
353 353 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
354 354
355 355 # Permissions Branch (EE feature)
356 356 config.add_route(
357 357 name='edit_repo_perms_branch',
358 358 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
359 359 config.add_route(
360 360 name='edit_repo_perms_branch_delete',
361 361 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
362 362 repo_route=True)
363 363
364 364 # Maintenance
365 365 config.add_route(
366 366 name='edit_repo_maintenance',
367 367 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
368 368
369 369 config.add_route(
370 370 name='edit_repo_maintenance_execute',
371 371 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
372 372
373 373 # Fields
374 374 config.add_route(
375 375 name='edit_repo_fields',
376 376 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
377 377 config.add_route(
378 378 name='edit_repo_fields_create',
379 379 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
380 380 config.add_route(
381 381 name='edit_repo_fields_delete',
382 382 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
383 383
384 384 # Locking
385 385 config.add_route(
386 386 name='repo_edit_toggle_locking',
387 387 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
388 388
389 389 # Remote
390 390 config.add_route(
391 391 name='edit_repo_remote',
392 392 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
393 393 config.add_route(
394 394 name='edit_repo_remote_pull',
395 395 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
396 396 config.add_route(
397 397 name='edit_repo_remote_push',
398 398 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
399 399
400 400 # Statistics
401 401 config.add_route(
402 402 name='edit_repo_statistics',
403 403 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
404 404 config.add_route(
405 405 name='edit_repo_statistics_reset',
406 406 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
407 407
408 408 # Issue trackers
409 409 config.add_route(
410 410 name='edit_repo_issuetracker',
411 411 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
412 412 config.add_route(
413 413 name='edit_repo_issuetracker_test',
414 414 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
415 415 config.add_route(
416 416 name='edit_repo_issuetracker_delete',
417 417 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
418 418 config.add_route(
419 419 name='edit_repo_issuetracker_update',
420 420 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
421 421
422 422 # VCS Settings
423 423 config.add_route(
424 424 name='edit_repo_vcs',
425 425 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
426 426 config.add_route(
427 427 name='edit_repo_vcs_update',
428 428 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
429 429
430 430 # svn pattern
431 431 config.add_route(
432 432 name='edit_repo_vcs_svn_pattern_delete',
433 433 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
434 434
435 435 # Repo Review Rules (EE feature)
436 436 config.add_route(
437 437 name='repo_reviewers',
438 438 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
439 439
440 440 config.add_route(
441 441 name='repo_default_reviewers_data',
442 442 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
443 443
444 444 # Repo Automation (EE feature)
445 445 config.add_route(
446 446 name='repo_automation',
447 447 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
448 448
449 449 # Strip
450 450 config.add_route(
451 451 name='edit_repo_strip',
452 452 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
453 453
454 454 config.add_route(
455 455 name='strip_check',
456 456 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
457 457
458 458 config.add_route(
459 459 name='strip_execute',
460 460 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
461 461
462 462 # Audit logs
463 463 config.add_route(
464 464 name='edit_repo_audit_logs',
465 465 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
466 466
467 467 # ATOM/RSS Feed
468 468 config.add_route(
469 469 name='rss_feed_home',
470 470 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
471 471
472 472 config.add_route(
473 473 name='atom_feed_home',
474 474 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
475 475
476 476 # NOTE(marcink): needs to be at the end for catch-all
477 477 add_route_with_slash(
478 478 config,
479 479 name='repo_summary',
480 480 pattern='/{repo_name:.*?[^/]}', repo_route=True)
481 481
482 482 # Scan module for configuration decorators.
483 483 config.scan('.views', ignore='.tests')
@@ -1,1233 +1,1233 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 35 from rhodecode.tests.utils import AssertResponse
36 36
37 37
38 38 def route_path(name, params=None, **kwargs):
39 39 import urllib
40 40
41 41 base_url = {
42 42 'repo_changelog': '/{repo_name}/changelog',
43 43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 44 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
45 45 'pullrequest_show_all': '/{repo_name}/pull-request',
46 46 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
47 47 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
48 'pullrequest_repo_destinations': '/{repo_name}/pull-request/repo-destinations',
48 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
49 49 'pullrequest_new': '/{repo_name}/pull-request/new',
50 50 'pullrequest_create': '/{repo_name}/pull-request/create',
51 51 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
52 52 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
53 53 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
54 54 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
55 55 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
56 56 }[name].format(**kwargs)
57 57
58 58 if params:
59 59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 60 return base_url
61 61
62 62
63 63 @pytest.mark.usefixtures('app', 'autologin_user')
64 64 @pytest.mark.backends("git", "hg")
65 65 class TestPullrequestsView(object):
66 66
67 67 def test_index(self, backend):
68 68 self.app.get(route_path(
69 69 'pullrequest_new',
70 70 repo_name=backend.repo_name))
71 71
72 72 def test_option_menu_create_pull_request_exists(self, backend):
73 73 repo_name = backend.repo_name
74 74 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
75 75
76 76 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
77 77 'pullrequest_new', repo_name=repo_name)
78 78 response.mustcontain(create_pr_link)
79 79
80 80 def test_create_pr_form_with_raw_commit_id(self, backend):
81 81 repo = backend.repo
82 82
83 83 self.app.get(
84 84 route_path('pullrequest_new', repo_name=repo.repo_name,
85 85 commit=repo.get_commit().raw_id),
86 86 status=200)
87 87
88 88 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
89 89 @pytest.mark.parametrize('range_diff', ["0", "1"])
90 90 def test_show(self, pr_util, pr_merge_enabled, range_diff):
91 91 pull_request = pr_util.create_pull_request(
92 92 mergeable=pr_merge_enabled, enable_notifications=False)
93 93
94 94 response = self.app.get(route_path(
95 95 'pullrequest_show',
96 96 repo_name=pull_request.target_repo.scm_instance().name,
97 97 pull_request_id=pull_request.pull_request_id,
98 98 params={'range-diff': range_diff}))
99 99
100 100 for commit_id in pull_request.revisions:
101 101 response.mustcontain(commit_id)
102 102
103 103 assert pull_request.target_ref_parts.type in response
104 104 assert pull_request.target_ref_parts.name in response
105 105 target_clone_url = pull_request.target_repo.clone_url()
106 106 assert target_clone_url in response
107 107
108 108 assert 'class="pull-request-merge"' in response
109 109 if pr_merge_enabled:
110 110 response.mustcontain('Pull request reviewer approval is pending')
111 111 else:
112 112 response.mustcontain('Server-side pull request merging is disabled.')
113 113
114 114 if range_diff == "1":
115 115 response.mustcontain('Turn off: Show the diff as commit range')
116 116
117 117 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
118 118 # Logout
119 119 response = self.app.post(
120 120 h.route_path('logout'),
121 121 params={'csrf_token': csrf_token})
122 122 # Login as regular user
123 123 response = self.app.post(h.route_path('login'),
124 124 {'username': TEST_USER_REGULAR_LOGIN,
125 125 'password': 'test12'})
126 126
127 127 pull_request = pr_util.create_pull_request(
128 128 author=TEST_USER_REGULAR_LOGIN)
129 129
130 130 response = self.app.get(route_path(
131 131 'pullrequest_show',
132 132 repo_name=pull_request.target_repo.scm_instance().name,
133 133 pull_request_id=pull_request.pull_request_id))
134 134
135 135 response.mustcontain('Server-side pull request merging is disabled.')
136 136
137 137 assert_response = response.assert_response()
138 138 # for regular user without a merge permissions, we don't see it
139 139 assert_response.no_element_exists('#close-pull-request-action')
140 140
141 141 user_util.grant_user_permission_to_repo(
142 142 pull_request.target_repo,
143 143 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
144 144 'repository.write')
145 145 response = self.app.get(route_path(
146 146 'pullrequest_show',
147 147 repo_name=pull_request.target_repo.scm_instance().name,
148 148 pull_request_id=pull_request.pull_request_id))
149 149
150 150 response.mustcontain('Server-side pull request merging is disabled.')
151 151
152 152 assert_response = response.assert_response()
153 153 # now regular user has a merge permissions, we have CLOSE button
154 154 assert_response.one_element_exists('#close-pull-request-action')
155 155
156 156 def test_show_invalid_commit_id(self, pr_util):
157 157 # Simulating invalid revisions which will cause a lookup error
158 158 pull_request = pr_util.create_pull_request()
159 159 pull_request.revisions = ['invalid']
160 160 Session().add(pull_request)
161 161 Session().commit()
162 162
163 163 response = self.app.get(route_path(
164 164 'pullrequest_show',
165 165 repo_name=pull_request.target_repo.scm_instance().name,
166 166 pull_request_id=pull_request.pull_request_id))
167 167
168 168 for commit_id in pull_request.revisions:
169 169 response.mustcontain(commit_id)
170 170
171 171 def test_show_invalid_source_reference(self, pr_util):
172 172 pull_request = pr_util.create_pull_request()
173 173 pull_request.source_ref = 'branch:b:invalid'
174 174 Session().add(pull_request)
175 175 Session().commit()
176 176
177 177 self.app.get(route_path(
178 178 'pullrequest_show',
179 179 repo_name=pull_request.target_repo.scm_instance().name,
180 180 pull_request_id=pull_request.pull_request_id))
181 181
182 182 def test_edit_title_description(self, pr_util, csrf_token):
183 183 pull_request = pr_util.create_pull_request()
184 184 pull_request_id = pull_request.pull_request_id
185 185
186 186 response = self.app.post(
187 187 route_path('pullrequest_update',
188 188 repo_name=pull_request.target_repo.repo_name,
189 189 pull_request_id=pull_request_id),
190 190 params={
191 191 'edit_pull_request': 'true',
192 192 'title': 'New title',
193 193 'description': 'New description',
194 194 'csrf_token': csrf_token})
195 195
196 196 assert_session_flash(
197 197 response, u'Pull request title & description updated.',
198 198 category='success')
199 199
200 200 pull_request = PullRequest.get(pull_request_id)
201 201 assert pull_request.title == 'New title'
202 202 assert pull_request.description == 'New description'
203 203
204 204 def test_edit_title_description_closed(self, pr_util, csrf_token):
205 205 pull_request = pr_util.create_pull_request()
206 206 pull_request_id = pull_request.pull_request_id
207 207 repo_name = pull_request.target_repo.repo_name
208 208 pr_util.close()
209 209
210 210 response = self.app.post(
211 211 route_path('pullrequest_update',
212 212 repo_name=repo_name, pull_request_id=pull_request_id),
213 213 params={
214 214 'edit_pull_request': 'true',
215 215 'title': 'New title',
216 216 'description': 'New description',
217 217 'csrf_token': csrf_token}, status=200)
218 218 assert_session_flash(
219 219 response, u'Cannot update closed pull requests.',
220 220 category='error')
221 221
222 222 def test_update_invalid_source_reference(self, pr_util, csrf_token):
223 223 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
224 224
225 225 pull_request = pr_util.create_pull_request()
226 226 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
227 227 Session().add(pull_request)
228 228 Session().commit()
229 229
230 230 pull_request_id = pull_request.pull_request_id
231 231
232 232 response = self.app.post(
233 233 route_path('pullrequest_update',
234 234 repo_name=pull_request.target_repo.repo_name,
235 235 pull_request_id=pull_request_id),
236 236 params={'update_commits': 'true',
237 237 'csrf_token': csrf_token})
238 238
239 239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 240 UpdateFailureReason.MISSING_SOURCE_REF])
241 241 assert_session_flash(response, expected_msg, category='error')
242 242
243 243 def test_missing_target_reference(self, pr_util, csrf_token):
244 244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 245 pull_request = pr_util.create_pull_request(
246 246 approved=True, mergeable=True)
247 247 pull_request.target_ref = 'branch:invalid-branch:invalid-commit-id'
248 248 Session().add(pull_request)
249 249 Session().commit()
250 250
251 251 pull_request_id = pull_request.pull_request_id
252 252 pull_request_url = route_path(
253 253 'pullrequest_show',
254 254 repo_name=pull_request.target_repo.repo_name,
255 255 pull_request_id=pull_request_id)
256 256
257 257 response = self.app.get(pull_request_url)
258 258
259 259 assertr = AssertResponse(response)
260 260 expected_msg = PullRequestModel.MERGE_STATUS_MESSAGES[
261 261 MergeFailureReason.MISSING_TARGET_REF]
262 262 assertr.element_contains(
263 263 'span[data-role="merge-message"]', str(expected_msg))
264 264
265 265 def test_comment_and_close_pull_request_custom_message_approved(
266 266 self, pr_util, csrf_token, xhr_header):
267 267
268 268 pull_request = pr_util.create_pull_request(approved=True)
269 269 pull_request_id = pull_request.pull_request_id
270 270 author = pull_request.user_id
271 271 repo = pull_request.target_repo.repo_id
272 272
273 273 self.app.post(
274 274 route_path('pullrequest_comment_create',
275 275 repo_name=pull_request.target_repo.scm_instance().name,
276 276 pull_request_id=pull_request_id),
277 277 params={
278 278 'close_pull_request': '1',
279 279 'text': 'Closing a PR',
280 280 'csrf_token': csrf_token},
281 281 extra_environ=xhr_header,)
282 282
283 283 journal = UserLog.query()\
284 284 .filter(UserLog.user_id == author)\
285 285 .filter(UserLog.repository_id == repo) \
286 286 .order_by('user_log_id') \
287 287 .all()
288 288 assert journal[-1].action == 'repo.pull_request.close'
289 289
290 290 pull_request = PullRequest.get(pull_request_id)
291 291 assert pull_request.is_closed()
292 292
293 293 status = ChangesetStatusModel().get_status(
294 294 pull_request.source_repo, pull_request=pull_request)
295 295 assert status == ChangesetStatus.STATUS_APPROVED
296 296 comments = ChangesetComment().query() \
297 297 .filter(ChangesetComment.pull_request == pull_request) \
298 298 .order_by(ChangesetComment.comment_id.asc())\
299 299 .all()
300 300 assert comments[-1].text == 'Closing a PR'
301 301
302 302 def test_comment_force_close_pull_request_rejected(
303 303 self, pr_util, csrf_token, xhr_header):
304 304 pull_request = pr_util.create_pull_request()
305 305 pull_request_id = pull_request.pull_request_id
306 306 PullRequestModel().update_reviewers(
307 307 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
308 308 pull_request.author)
309 309 author = pull_request.user_id
310 310 repo = pull_request.target_repo.repo_id
311 311
312 312 self.app.post(
313 313 route_path('pullrequest_comment_create',
314 314 repo_name=pull_request.target_repo.scm_instance().name,
315 315 pull_request_id=pull_request_id),
316 316 params={
317 317 'close_pull_request': '1',
318 318 'csrf_token': csrf_token},
319 319 extra_environ=xhr_header)
320 320
321 321 pull_request = PullRequest.get(pull_request_id)
322 322
323 323 journal = UserLog.query()\
324 324 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
325 325 .order_by('user_log_id') \
326 326 .all()
327 327 assert journal[-1].action == 'repo.pull_request.close'
328 328
329 329 # check only the latest status, not the review status
330 330 status = ChangesetStatusModel().get_status(
331 331 pull_request.source_repo, pull_request=pull_request)
332 332 assert status == ChangesetStatus.STATUS_REJECTED
333 333
334 334 def test_comment_and_close_pull_request(
335 335 self, pr_util, csrf_token, xhr_header):
336 336 pull_request = pr_util.create_pull_request()
337 337 pull_request_id = pull_request.pull_request_id
338 338
339 339 response = self.app.post(
340 340 route_path('pullrequest_comment_create',
341 341 repo_name=pull_request.target_repo.scm_instance().name,
342 342 pull_request_id=pull_request.pull_request_id),
343 343 params={
344 344 'close_pull_request': 'true',
345 345 'csrf_token': csrf_token},
346 346 extra_environ=xhr_header)
347 347
348 348 assert response.json
349 349
350 350 pull_request = PullRequest.get(pull_request_id)
351 351 assert pull_request.is_closed()
352 352
353 353 # check only the latest status, not the review status
354 354 status = ChangesetStatusModel().get_status(
355 355 pull_request.source_repo, pull_request=pull_request)
356 356 assert status == ChangesetStatus.STATUS_REJECTED
357 357
358 358 def test_create_pull_request(self, backend, csrf_token):
359 359 commits = [
360 360 {'message': 'ancestor'},
361 361 {'message': 'change'},
362 362 {'message': 'change2'},
363 363 ]
364 364 commit_ids = backend.create_master_repo(commits)
365 365 target = backend.create_repo(heads=['ancestor'])
366 366 source = backend.create_repo(heads=['change2'])
367 367
368 368 response = self.app.post(
369 369 route_path('pullrequest_create', repo_name=source.repo_name),
370 370 [
371 371 ('source_repo', source.repo_name),
372 372 ('source_ref', 'branch:default:' + commit_ids['change2']),
373 373 ('target_repo', target.repo_name),
374 374 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
375 375 ('common_ancestor', commit_ids['ancestor']),
376 376 ('pullrequest_title', 'Title'),
377 377 ('pullrequest_desc', 'Description'),
378 378 ('description_renderer', 'markdown'),
379 379 ('__start__', 'review_members:sequence'),
380 380 ('__start__', 'reviewer:mapping'),
381 381 ('user_id', '1'),
382 382 ('__start__', 'reasons:sequence'),
383 383 ('reason', 'Some reason'),
384 384 ('__end__', 'reasons:sequence'),
385 385 ('__start__', 'rules:sequence'),
386 386 ('__end__', 'rules:sequence'),
387 387 ('mandatory', 'False'),
388 388 ('__end__', 'reviewer:mapping'),
389 389 ('__end__', 'review_members:sequence'),
390 390 ('__start__', 'revisions:sequence'),
391 391 ('revisions', commit_ids['change']),
392 392 ('revisions', commit_ids['change2']),
393 393 ('__end__', 'revisions:sequence'),
394 394 ('user', ''),
395 395 ('csrf_token', csrf_token),
396 396 ],
397 397 status=302)
398 398
399 399 location = response.headers['Location']
400 400 pull_request_id = location.rsplit('/', 1)[1]
401 401 assert pull_request_id != 'new'
402 402 pull_request = PullRequest.get(int(pull_request_id))
403 403
404 404 # check that we have now both revisions
405 405 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
406 406 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
407 407 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
408 408 assert pull_request.target_ref == expected_target_ref
409 409
410 410 def test_reviewer_notifications(self, backend, csrf_token):
411 411 # We have to use the app.post for this test so it will create the
412 412 # notifications properly with the new PR
413 413 commits = [
414 414 {'message': 'ancestor',
415 415 'added': [FileNode('file_A', content='content_of_ancestor')]},
416 416 {'message': 'change',
417 417 'added': [FileNode('file_a', content='content_of_change')]},
418 418 {'message': 'change-child'},
419 419 {'message': 'ancestor-child', 'parents': ['ancestor'],
420 420 'added': [
421 421 FileNode('file_B', content='content_of_ancestor_child')]},
422 422 {'message': 'ancestor-child-2'},
423 423 ]
424 424 commit_ids = backend.create_master_repo(commits)
425 425 target = backend.create_repo(heads=['ancestor-child'])
426 426 source = backend.create_repo(heads=['change'])
427 427
428 428 response = self.app.post(
429 429 route_path('pullrequest_create', repo_name=source.repo_name),
430 430 [
431 431 ('source_repo', source.repo_name),
432 432 ('source_ref', 'branch:default:' + commit_ids['change']),
433 433 ('target_repo', target.repo_name),
434 434 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
435 435 ('common_ancestor', commit_ids['ancestor']),
436 436 ('pullrequest_title', 'Title'),
437 437 ('pullrequest_desc', 'Description'),
438 438 ('description_renderer', 'markdown'),
439 439 ('__start__', 'review_members:sequence'),
440 440 ('__start__', 'reviewer:mapping'),
441 441 ('user_id', '2'),
442 442 ('__start__', 'reasons:sequence'),
443 443 ('reason', 'Some reason'),
444 444 ('__end__', 'reasons:sequence'),
445 445 ('__start__', 'rules:sequence'),
446 446 ('__end__', 'rules:sequence'),
447 447 ('mandatory', 'False'),
448 448 ('__end__', 'reviewer:mapping'),
449 449 ('__end__', 'review_members:sequence'),
450 450 ('__start__', 'revisions:sequence'),
451 451 ('revisions', commit_ids['change']),
452 452 ('__end__', 'revisions:sequence'),
453 453 ('user', ''),
454 454 ('csrf_token', csrf_token),
455 455 ],
456 456 status=302)
457 457
458 458 location = response.headers['Location']
459 459
460 460 pull_request_id = location.rsplit('/', 1)[1]
461 461 assert pull_request_id != 'new'
462 462 pull_request = PullRequest.get(int(pull_request_id))
463 463
464 464 # Check that a notification was made
465 465 notifications = Notification.query()\
466 466 .filter(Notification.created_by == pull_request.author.user_id,
467 467 Notification.type_ == Notification.TYPE_PULL_REQUEST,
468 468 Notification.subject.contains(
469 469 "wants you to review pull request #%s" % pull_request_id))
470 470 assert len(notifications.all()) == 1
471 471
472 472 # Change reviewers and check that a notification was made
473 473 PullRequestModel().update_reviewers(
474 474 pull_request.pull_request_id, [(1, [], False, [])],
475 475 pull_request.author)
476 476 assert len(notifications.all()) == 2
477 477
478 478 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
479 479 csrf_token):
480 480 commits = [
481 481 {'message': 'ancestor',
482 482 'added': [FileNode('file_A', content='content_of_ancestor')]},
483 483 {'message': 'change',
484 484 'added': [FileNode('file_a', content='content_of_change')]},
485 485 {'message': 'change-child'},
486 486 {'message': 'ancestor-child', 'parents': ['ancestor'],
487 487 'added': [
488 488 FileNode('file_B', content='content_of_ancestor_child')]},
489 489 {'message': 'ancestor-child-2'},
490 490 ]
491 491 commit_ids = backend.create_master_repo(commits)
492 492 target = backend.create_repo(heads=['ancestor-child'])
493 493 source = backend.create_repo(heads=['change'])
494 494
495 495 response = self.app.post(
496 496 route_path('pullrequest_create', repo_name=source.repo_name),
497 497 [
498 498 ('source_repo', source.repo_name),
499 499 ('source_ref', 'branch:default:' + commit_ids['change']),
500 500 ('target_repo', target.repo_name),
501 501 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
502 502 ('common_ancestor', commit_ids['ancestor']),
503 503 ('pullrequest_title', 'Title'),
504 504 ('pullrequest_desc', 'Description'),
505 505 ('description_renderer', 'markdown'),
506 506 ('__start__', 'review_members:sequence'),
507 507 ('__start__', 'reviewer:mapping'),
508 508 ('user_id', '1'),
509 509 ('__start__', 'reasons:sequence'),
510 510 ('reason', 'Some reason'),
511 511 ('__end__', 'reasons:sequence'),
512 512 ('__start__', 'rules:sequence'),
513 513 ('__end__', 'rules:sequence'),
514 514 ('mandatory', 'False'),
515 515 ('__end__', 'reviewer:mapping'),
516 516 ('__end__', 'review_members:sequence'),
517 517 ('__start__', 'revisions:sequence'),
518 518 ('revisions', commit_ids['change']),
519 519 ('__end__', 'revisions:sequence'),
520 520 ('user', ''),
521 521 ('csrf_token', csrf_token),
522 522 ],
523 523 status=302)
524 524
525 525 location = response.headers['Location']
526 526
527 527 pull_request_id = location.rsplit('/', 1)[1]
528 528 assert pull_request_id != 'new'
529 529 pull_request = PullRequest.get(int(pull_request_id))
530 530
531 531 # target_ref has to point to the ancestor's commit_id in order to
532 532 # show the correct diff
533 533 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
534 534 assert pull_request.target_ref == expected_target_ref
535 535
536 536 # Check generated diff contents
537 537 response = response.follow()
538 538 assert 'content_of_ancestor' not in response.body
539 539 assert 'content_of_ancestor-child' not in response.body
540 540 assert 'content_of_change' in response.body
541 541
542 542 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
543 543 # Clear any previous calls to rcextensions
544 544 rhodecode.EXTENSIONS.calls.clear()
545 545
546 546 pull_request = pr_util.create_pull_request(
547 547 approved=True, mergeable=True)
548 548 pull_request_id = pull_request.pull_request_id
549 549 repo_name = pull_request.target_repo.scm_instance().name,
550 550
551 551 response = self.app.post(
552 552 route_path('pullrequest_merge',
553 553 repo_name=str(repo_name[0]),
554 554 pull_request_id=pull_request_id),
555 555 params={'csrf_token': csrf_token}).follow()
556 556
557 557 pull_request = PullRequest.get(pull_request_id)
558 558
559 559 assert response.status_int == 200
560 560 assert pull_request.is_closed()
561 561 assert_pull_request_status(
562 562 pull_request, ChangesetStatus.STATUS_APPROVED)
563 563
564 564 # Check the relevant log entries were added
565 565 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
566 566 actions = [log.action for log in user_logs]
567 567 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
568 568 expected_actions = [
569 569 u'repo.pull_request.close',
570 570 u'repo.pull_request.merge',
571 571 u'repo.pull_request.comment.create'
572 572 ]
573 573 assert actions == expected_actions
574 574
575 575 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
576 576 actions = [log for log in user_logs]
577 577 assert actions[-1].action == 'user.push'
578 578 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
579 579
580 580 # Check post_push rcextension was really executed
581 581 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
582 582 assert len(push_calls) == 1
583 583 unused_last_call_args, last_call_kwargs = push_calls[0]
584 584 assert last_call_kwargs['action'] == 'push'
585 585 assert last_call_kwargs['commit_ids'] == pr_commit_ids
586 586
587 587 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
588 588 pull_request = pr_util.create_pull_request(mergeable=False)
589 589 pull_request_id = pull_request.pull_request_id
590 590 pull_request = PullRequest.get(pull_request_id)
591 591
592 592 response = self.app.post(
593 593 route_path('pullrequest_merge',
594 594 repo_name=pull_request.target_repo.scm_instance().name,
595 595 pull_request_id=pull_request.pull_request_id),
596 596 params={'csrf_token': csrf_token}).follow()
597 597
598 598 assert response.status_int == 200
599 599 response.mustcontain(
600 600 'Merge is not currently possible because of below failed checks.')
601 601 response.mustcontain('Server-side pull request merging is disabled.')
602 602
603 603 @pytest.mark.skip_backends('svn')
604 604 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
605 605 pull_request = pr_util.create_pull_request(mergeable=True)
606 606 pull_request_id = pull_request.pull_request_id
607 607 repo_name = pull_request.target_repo.scm_instance().name
608 608
609 609 response = self.app.post(
610 610 route_path('pullrequest_merge',
611 611 repo_name=repo_name,
612 612 pull_request_id=pull_request_id),
613 613 params={'csrf_token': csrf_token}).follow()
614 614
615 615 assert response.status_int == 200
616 616
617 617 response.mustcontain(
618 618 'Merge is not currently possible because of below failed checks.')
619 619 response.mustcontain('Pull request reviewer approval is pending.')
620 620
621 621 def test_merge_pull_request_renders_failure_reason(
622 622 self, user_regular, csrf_token, pr_util):
623 623 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
624 624 pull_request_id = pull_request.pull_request_id
625 625 repo_name = pull_request.target_repo.scm_instance().name
626 626
627 627 model_patcher = mock.patch.multiple(
628 628 PullRequestModel,
629 629 merge_repo=mock.Mock(return_value=MergeResponse(
630 630 True, False, 'STUB_COMMIT_ID', MergeFailureReason.PUSH_FAILED)),
631 631 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
632 632
633 633 with model_patcher:
634 634 response = self.app.post(
635 635 route_path('pullrequest_merge',
636 636 repo_name=repo_name,
637 637 pull_request_id=pull_request_id),
638 638 params={'csrf_token': csrf_token}, status=302)
639 639
640 640 assert_session_flash(response, PullRequestModel.MERGE_STATUS_MESSAGES[
641 641 MergeFailureReason.PUSH_FAILED])
642 642
643 643 def test_update_source_revision(self, backend, csrf_token):
644 644 commits = [
645 645 {'message': 'ancestor'},
646 646 {'message': 'change'},
647 647 {'message': 'change-2'},
648 648 ]
649 649 commit_ids = backend.create_master_repo(commits)
650 650 target = backend.create_repo(heads=['ancestor'])
651 651 source = backend.create_repo(heads=['change'])
652 652
653 653 # create pr from a in source to A in target
654 654 pull_request = PullRequest()
655 655 pull_request.source_repo = source
656 656 # TODO: johbo: Make sure that we write the source ref this way!
657 657 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
658 658 branch=backend.default_branch_name, commit_id=commit_ids['change'])
659 659 pull_request.target_repo = target
660 660
661 661 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
662 662 branch=backend.default_branch_name,
663 663 commit_id=commit_ids['ancestor'])
664 664 pull_request.revisions = [commit_ids['change']]
665 665 pull_request.title = u"Test"
666 666 pull_request.description = u"Description"
667 667 pull_request.author = UserModel().get_by_username(
668 668 TEST_USER_ADMIN_LOGIN)
669 669 Session().add(pull_request)
670 670 Session().commit()
671 671 pull_request_id = pull_request.pull_request_id
672 672
673 673 # source has ancestor - change - change-2
674 674 backend.pull_heads(source, heads=['change-2'])
675 675
676 676 # update PR
677 677 self.app.post(
678 678 route_path('pullrequest_update',
679 679 repo_name=target.repo_name,
680 680 pull_request_id=pull_request_id),
681 681 params={'update_commits': 'true',
682 682 'csrf_token': csrf_token})
683 683
684 684 # check that we have now both revisions
685 685 pull_request = PullRequest.get(pull_request_id)
686 686 assert pull_request.revisions == [
687 687 commit_ids['change-2'], commit_ids['change']]
688 688
689 689 # TODO: johbo: this should be a test on its own
690 690 response = self.app.get(route_path(
691 691 'pullrequest_new',
692 692 repo_name=target.repo_name))
693 693 assert response.status_int == 200
694 694 assert 'Pull request updated to' in response.body
695 695 assert 'with 1 added, 0 removed commits.' in response.body
696 696
697 697 def test_update_target_revision(self, backend, csrf_token):
698 698 commits = [
699 699 {'message': 'ancestor'},
700 700 {'message': 'change'},
701 701 {'message': 'ancestor-new', 'parents': ['ancestor']},
702 702 {'message': 'change-rebased'},
703 703 ]
704 704 commit_ids = backend.create_master_repo(commits)
705 705 target = backend.create_repo(heads=['ancestor'])
706 706 source = backend.create_repo(heads=['change'])
707 707
708 708 # create pr from a in source to A in target
709 709 pull_request = PullRequest()
710 710 pull_request.source_repo = source
711 711 # TODO: johbo: Make sure that we write the source ref this way!
712 712 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
713 713 branch=backend.default_branch_name, commit_id=commit_ids['change'])
714 714 pull_request.target_repo = target
715 715 # TODO: johbo: Target ref should be branch based, since tip can jump
716 716 # from branch to branch
717 717 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
718 718 branch=backend.default_branch_name,
719 719 commit_id=commit_ids['ancestor'])
720 720 pull_request.revisions = [commit_ids['change']]
721 721 pull_request.title = u"Test"
722 722 pull_request.description = u"Description"
723 723 pull_request.author = UserModel().get_by_username(
724 724 TEST_USER_ADMIN_LOGIN)
725 725 Session().add(pull_request)
726 726 Session().commit()
727 727 pull_request_id = pull_request.pull_request_id
728 728
729 729 # target has ancestor - ancestor-new
730 730 # source has ancestor - ancestor-new - change-rebased
731 731 backend.pull_heads(target, heads=['ancestor-new'])
732 732 backend.pull_heads(source, heads=['change-rebased'])
733 733
734 734 # update PR
735 735 self.app.post(
736 736 route_path('pullrequest_update',
737 737 repo_name=target.repo_name,
738 738 pull_request_id=pull_request_id),
739 739 params={'update_commits': 'true',
740 740 'csrf_token': csrf_token},
741 741 status=200)
742 742
743 743 # check that we have now both revisions
744 744 pull_request = PullRequest.get(pull_request_id)
745 745 assert pull_request.revisions == [commit_ids['change-rebased']]
746 746 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
747 747 branch=backend.default_branch_name,
748 748 commit_id=commit_ids['ancestor-new'])
749 749
750 750 # TODO: johbo: This should be a test on its own
751 751 response = self.app.get(route_path(
752 752 'pullrequest_new',
753 753 repo_name=target.repo_name))
754 754 assert response.status_int == 200
755 755 assert 'Pull request updated to' in response.body
756 756 assert 'with 1 added, 1 removed commits.' in response.body
757 757
758 758 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
759 759 backend = backend_git
760 760 commits = [
761 761 {'message': 'master-commit-1'},
762 762 {'message': 'master-commit-2-change-1'},
763 763 {'message': 'master-commit-3-change-2'},
764 764
765 765 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
766 766 {'message': 'feat-commit-2'},
767 767 ]
768 768 commit_ids = backend.create_master_repo(commits)
769 769 target = backend.create_repo(heads=['master-commit-3-change-2'])
770 770 source = backend.create_repo(heads=['feat-commit-2'])
771 771
772 772 # create pr from a in source to A in target
773 773 pull_request = PullRequest()
774 774 pull_request.source_repo = source
775 775 # TODO: johbo: Make sure that we write the source ref this way!
776 776 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
777 777 branch=backend.default_branch_name,
778 778 commit_id=commit_ids['master-commit-3-change-2'])
779 779
780 780 pull_request.target_repo = target
781 781 # TODO: johbo: Target ref should be branch based, since tip can jump
782 782 # from branch to branch
783 783 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
784 784 branch=backend.default_branch_name,
785 785 commit_id=commit_ids['feat-commit-2'])
786 786
787 787 pull_request.revisions = [
788 788 commit_ids['feat-commit-1'],
789 789 commit_ids['feat-commit-2']
790 790 ]
791 791 pull_request.title = u"Test"
792 792 pull_request.description = u"Description"
793 793 pull_request.author = UserModel().get_by_username(
794 794 TEST_USER_ADMIN_LOGIN)
795 795 Session().add(pull_request)
796 796 Session().commit()
797 797 pull_request_id = pull_request.pull_request_id
798 798
799 799 # PR is created, now we simulate a force-push into target,
800 800 # that drops a 2 last commits
801 801 vcsrepo = target.scm_instance()
802 802 vcsrepo.config.clear_section('hooks')
803 803 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
804 804
805 805 # update PR
806 806 self.app.post(
807 807 route_path('pullrequest_update',
808 808 repo_name=target.repo_name,
809 809 pull_request_id=pull_request_id),
810 810 params={'update_commits': 'true',
811 811 'csrf_token': csrf_token},
812 812 status=200)
813 813
814 814 response = self.app.get(route_path(
815 815 'pullrequest_new',
816 816 repo_name=target.repo_name))
817 817 assert response.status_int == 200
818 818 response.mustcontain('Pull request updated to')
819 819 response.mustcontain('with 0 added, 0 removed commits.')
820 820
821 821 def test_update_of_ancestor_reference(self, backend, csrf_token):
822 822 commits = [
823 823 {'message': 'ancestor'},
824 824 {'message': 'change'},
825 825 {'message': 'change-2'},
826 826 {'message': 'ancestor-new', 'parents': ['ancestor']},
827 827 {'message': 'change-rebased'},
828 828 ]
829 829 commit_ids = backend.create_master_repo(commits)
830 830 target = backend.create_repo(heads=['ancestor'])
831 831 source = backend.create_repo(heads=['change'])
832 832
833 833 # create pr from a in source to A in target
834 834 pull_request = PullRequest()
835 835 pull_request.source_repo = source
836 836 # TODO: johbo: Make sure that we write the source ref this way!
837 837 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
838 838 branch=backend.default_branch_name,
839 839 commit_id=commit_ids['change'])
840 840 pull_request.target_repo = target
841 841 # TODO: johbo: Target ref should be branch based, since tip can jump
842 842 # from branch to branch
843 843 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
844 844 branch=backend.default_branch_name,
845 845 commit_id=commit_ids['ancestor'])
846 846 pull_request.revisions = [commit_ids['change']]
847 847 pull_request.title = u"Test"
848 848 pull_request.description = u"Description"
849 849 pull_request.author = UserModel().get_by_username(
850 850 TEST_USER_ADMIN_LOGIN)
851 851 Session().add(pull_request)
852 852 Session().commit()
853 853 pull_request_id = pull_request.pull_request_id
854 854
855 855 # target has ancestor - ancestor-new
856 856 # source has ancestor - ancestor-new - change-rebased
857 857 backend.pull_heads(target, heads=['ancestor-new'])
858 858 backend.pull_heads(source, heads=['change-rebased'])
859 859
860 860 # update PR
861 861 self.app.post(
862 862 route_path('pullrequest_update',
863 863 repo_name=target.repo_name,
864 864 pull_request_id=pull_request_id),
865 865 params={'update_commits': 'true',
866 866 'csrf_token': csrf_token},
867 867 status=200)
868 868
869 869 # Expect the target reference to be updated correctly
870 870 pull_request = PullRequest.get(pull_request_id)
871 871 assert pull_request.revisions == [commit_ids['change-rebased']]
872 872 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
873 873 branch=backend.default_branch_name,
874 874 commit_id=commit_ids['ancestor-new'])
875 875 assert pull_request.target_ref == expected_target_ref
876 876
877 877 def test_remove_pull_request_branch(self, backend_git, csrf_token):
878 878 branch_name = 'development'
879 879 commits = [
880 880 {'message': 'initial-commit'},
881 881 {'message': 'old-feature'},
882 882 {'message': 'new-feature', 'branch': branch_name},
883 883 ]
884 884 repo = backend_git.create_repo(commits)
885 885 commit_ids = backend_git.commit_ids
886 886
887 887 pull_request = PullRequest()
888 888 pull_request.source_repo = repo
889 889 pull_request.target_repo = repo
890 890 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
891 891 branch=branch_name, commit_id=commit_ids['new-feature'])
892 892 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
893 893 branch=backend_git.default_branch_name,
894 894 commit_id=commit_ids['old-feature'])
895 895 pull_request.revisions = [commit_ids['new-feature']]
896 896 pull_request.title = u"Test"
897 897 pull_request.description = u"Description"
898 898 pull_request.author = UserModel().get_by_username(
899 899 TEST_USER_ADMIN_LOGIN)
900 900 Session().add(pull_request)
901 901 Session().commit()
902 902
903 903 vcs = repo.scm_instance()
904 904 vcs.remove_ref('refs/heads/{}'.format(branch_name))
905 905
906 906 response = self.app.get(route_path(
907 907 'pullrequest_show',
908 908 repo_name=repo.repo_name,
909 909 pull_request_id=pull_request.pull_request_id))
910 910
911 911 assert response.status_int == 200
912 912 assert_response = AssertResponse(response)
913 913 assert_response.element_contains(
914 914 '#changeset_compare_view_content .alert strong',
915 915 'Missing commits')
916 916 assert_response.element_contains(
917 917 '#changeset_compare_view_content .alert',
918 918 'This pull request cannot be displayed, because one or more'
919 919 ' commits no longer exist in the source repository.')
920 920
921 921 def test_strip_commits_from_pull_request(
922 922 self, backend, pr_util, csrf_token):
923 923 commits = [
924 924 {'message': 'initial-commit'},
925 925 {'message': 'old-feature'},
926 926 {'message': 'new-feature', 'parents': ['initial-commit']},
927 927 ]
928 928 pull_request = pr_util.create_pull_request(
929 929 commits, target_head='initial-commit', source_head='new-feature',
930 930 revisions=['new-feature'])
931 931
932 932 vcs = pr_util.source_repository.scm_instance()
933 933 if backend.alias == 'git':
934 934 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
935 935 else:
936 936 vcs.strip(pr_util.commit_ids['new-feature'])
937 937
938 938 response = self.app.get(route_path(
939 939 'pullrequest_show',
940 940 repo_name=pr_util.target_repository.repo_name,
941 941 pull_request_id=pull_request.pull_request_id))
942 942
943 943 assert response.status_int == 200
944 944 assert_response = AssertResponse(response)
945 945 assert_response.element_contains(
946 946 '#changeset_compare_view_content .alert strong',
947 947 'Missing commits')
948 948 assert_response.element_contains(
949 949 '#changeset_compare_view_content .alert',
950 950 'This pull request cannot be displayed, because one or more'
951 951 ' commits no longer exist in the source repository.')
952 952 assert_response.element_contains(
953 953 '#update_commits',
954 954 'Update commits')
955 955
956 956 def test_strip_commits_and_update(
957 957 self, backend, pr_util, csrf_token):
958 958 commits = [
959 959 {'message': 'initial-commit'},
960 960 {'message': 'old-feature'},
961 961 {'message': 'new-feature', 'parents': ['old-feature']},
962 962 ]
963 963 pull_request = pr_util.create_pull_request(
964 964 commits, target_head='old-feature', source_head='new-feature',
965 965 revisions=['new-feature'], mergeable=True)
966 966
967 967 vcs = pr_util.source_repository.scm_instance()
968 968 if backend.alias == 'git':
969 969 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
970 970 else:
971 971 vcs.strip(pr_util.commit_ids['new-feature'])
972 972
973 973 response = self.app.post(
974 974 route_path('pullrequest_update',
975 975 repo_name=pull_request.target_repo.repo_name,
976 976 pull_request_id=pull_request.pull_request_id),
977 977 params={'update_commits': 'true',
978 978 'csrf_token': csrf_token})
979 979
980 980 assert response.status_int == 200
981 981 assert response.body == 'true'
982 982
983 983 # Make sure that after update, it won't raise 500 errors
984 984 response = self.app.get(route_path(
985 985 'pullrequest_show',
986 986 repo_name=pr_util.target_repository.repo_name,
987 987 pull_request_id=pull_request.pull_request_id))
988 988
989 989 assert response.status_int == 200
990 990 assert_response = AssertResponse(response)
991 991 assert_response.element_contains(
992 992 '#changeset_compare_view_content .alert strong',
993 993 'Missing commits')
994 994
995 995 def test_branch_is_a_link(self, pr_util):
996 996 pull_request = pr_util.create_pull_request()
997 997 pull_request.source_ref = 'branch:origin:1234567890abcdef'
998 998 pull_request.target_ref = 'branch:target:abcdef1234567890'
999 999 Session().add(pull_request)
1000 1000 Session().commit()
1001 1001
1002 1002 response = self.app.get(route_path(
1003 1003 'pullrequest_show',
1004 1004 repo_name=pull_request.target_repo.scm_instance().name,
1005 1005 pull_request_id=pull_request.pull_request_id))
1006 1006 assert response.status_int == 200
1007 1007 assert_response = AssertResponse(response)
1008 1008
1009 1009 origin = assert_response.get_element('.pr-origininfo .tag')
1010 1010 origin_children = origin.getchildren()
1011 1011 assert len(origin_children) == 1
1012 1012 target = assert_response.get_element('.pr-targetinfo .tag')
1013 1013 target_children = target.getchildren()
1014 1014 assert len(target_children) == 1
1015 1015
1016 1016 expected_origin_link = route_path(
1017 1017 'repo_changelog',
1018 1018 repo_name=pull_request.source_repo.scm_instance().name,
1019 1019 params=dict(branch='origin'))
1020 1020 expected_target_link = route_path(
1021 1021 'repo_changelog',
1022 1022 repo_name=pull_request.target_repo.scm_instance().name,
1023 1023 params=dict(branch='target'))
1024 1024 assert origin_children[0].attrib['href'] == expected_origin_link
1025 1025 assert origin_children[0].text == 'branch: origin'
1026 1026 assert target_children[0].attrib['href'] == expected_target_link
1027 1027 assert target_children[0].text == 'branch: target'
1028 1028
1029 1029 def test_bookmark_is_not_a_link(self, pr_util):
1030 1030 pull_request = pr_util.create_pull_request()
1031 1031 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1032 1032 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1033 1033 Session().add(pull_request)
1034 1034 Session().commit()
1035 1035
1036 1036 response = self.app.get(route_path(
1037 1037 'pullrequest_show',
1038 1038 repo_name=pull_request.target_repo.scm_instance().name,
1039 1039 pull_request_id=pull_request.pull_request_id))
1040 1040 assert response.status_int == 200
1041 1041 assert_response = AssertResponse(response)
1042 1042
1043 1043 origin = assert_response.get_element('.pr-origininfo .tag')
1044 1044 assert origin.text.strip() == 'bookmark: origin'
1045 1045 assert origin.getchildren() == []
1046 1046
1047 1047 target = assert_response.get_element('.pr-targetinfo .tag')
1048 1048 assert target.text.strip() == 'bookmark: target'
1049 1049 assert target.getchildren() == []
1050 1050
1051 1051 def test_tag_is_not_a_link(self, pr_util):
1052 1052 pull_request = pr_util.create_pull_request()
1053 1053 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1054 1054 pull_request.target_ref = 'tag:target:abcdef1234567890'
1055 1055 Session().add(pull_request)
1056 1056 Session().commit()
1057 1057
1058 1058 response = self.app.get(route_path(
1059 1059 'pullrequest_show',
1060 1060 repo_name=pull_request.target_repo.scm_instance().name,
1061 1061 pull_request_id=pull_request.pull_request_id))
1062 1062 assert response.status_int == 200
1063 1063 assert_response = AssertResponse(response)
1064 1064
1065 1065 origin = assert_response.get_element('.pr-origininfo .tag')
1066 1066 assert origin.text.strip() == 'tag: origin'
1067 1067 assert origin.getchildren() == []
1068 1068
1069 1069 target = assert_response.get_element('.pr-targetinfo .tag')
1070 1070 assert target.text.strip() == 'tag: target'
1071 1071 assert target.getchildren() == []
1072 1072
1073 1073 @pytest.mark.parametrize('mergeable', [True, False])
1074 1074 def test_shadow_repository_link(
1075 1075 self, mergeable, pr_util, http_host_only_stub):
1076 1076 """
1077 1077 Check that the pull request summary page displays a link to the shadow
1078 1078 repository if the pull request is mergeable. If it is not mergeable
1079 1079 the link should not be displayed.
1080 1080 """
1081 1081 pull_request = pr_util.create_pull_request(
1082 1082 mergeable=mergeable, enable_notifications=False)
1083 1083 target_repo = pull_request.target_repo.scm_instance()
1084 1084 pr_id = pull_request.pull_request_id
1085 1085 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1086 1086 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1087 1087
1088 1088 response = self.app.get(route_path(
1089 1089 'pullrequest_show',
1090 1090 repo_name=target_repo.name,
1091 1091 pull_request_id=pr_id))
1092 1092
1093 1093 assertr = AssertResponse(response)
1094 1094 if mergeable:
1095 1095 assertr.element_value_contains('input.pr-mergeinfo', shadow_url)
1096 1096 assertr.element_value_contains('input.pr-mergeinfo ', 'pr-merge')
1097 1097 else:
1098 1098 assertr.no_element_exists('.pr-mergeinfo')
1099 1099
1100 1100
1101 1101 @pytest.mark.usefixtures('app')
1102 1102 @pytest.mark.backends("git", "hg")
1103 1103 class TestPullrequestsControllerDelete(object):
1104 1104 def test_pull_request_delete_button_permissions_admin(
1105 1105 self, autologin_user, user_admin, pr_util):
1106 1106 pull_request = pr_util.create_pull_request(
1107 1107 author=user_admin.username, enable_notifications=False)
1108 1108
1109 1109 response = self.app.get(route_path(
1110 1110 'pullrequest_show',
1111 1111 repo_name=pull_request.target_repo.scm_instance().name,
1112 1112 pull_request_id=pull_request.pull_request_id))
1113 1113
1114 1114 response.mustcontain('id="delete_pullrequest"')
1115 1115 response.mustcontain('Confirm to delete this pull request')
1116 1116
1117 1117 def test_pull_request_delete_button_permissions_owner(
1118 1118 self, autologin_regular_user, user_regular, pr_util):
1119 1119 pull_request = pr_util.create_pull_request(
1120 1120 author=user_regular.username, enable_notifications=False)
1121 1121
1122 1122 response = self.app.get(route_path(
1123 1123 'pullrequest_show',
1124 1124 repo_name=pull_request.target_repo.scm_instance().name,
1125 1125 pull_request_id=pull_request.pull_request_id))
1126 1126
1127 1127 response.mustcontain('id="delete_pullrequest"')
1128 1128 response.mustcontain('Confirm to delete this pull request')
1129 1129
1130 1130 def test_pull_request_delete_button_permissions_forbidden(
1131 1131 self, autologin_regular_user, user_regular, user_admin, pr_util):
1132 1132 pull_request = pr_util.create_pull_request(
1133 1133 author=user_admin.username, enable_notifications=False)
1134 1134
1135 1135 response = self.app.get(route_path(
1136 1136 'pullrequest_show',
1137 1137 repo_name=pull_request.target_repo.scm_instance().name,
1138 1138 pull_request_id=pull_request.pull_request_id))
1139 1139 response.mustcontain(no=['id="delete_pullrequest"'])
1140 1140 response.mustcontain(no=['Confirm to delete this pull request'])
1141 1141
1142 1142 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1143 1143 self, autologin_regular_user, user_regular, user_admin, pr_util,
1144 1144 user_util):
1145 1145
1146 1146 pull_request = pr_util.create_pull_request(
1147 1147 author=user_admin.username, enable_notifications=False)
1148 1148
1149 1149 user_util.grant_user_permission_to_repo(
1150 1150 pull_request.target_repo, user_regular,
1151 1151 'repository.write')
1152 1152
1153 1153 response = self.app.get(route_path(
1154 1154 'pullrequest_show',
1155 1155 repo_name=pull_request.target_repo.scm_instance().name,
1156 1156 pull_request_id=pull_request.pull_request_id))
1157 1157
1158 1158 response.mustcontain('id="open_edit_pullrequest"')
1159 1159 response.mustcontain('id="delete_pullrequest"')
1160 1160 response.mustcontain(no=['Confirm to delete this pull request'])
1161 1161
1162 1162 def test_delete_comment_returns_404_if_comment_does_not_exist(
1163 1163 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1164 1164
1165 1165 pull_request = pr_util.create_pull_request(
1166 1166 author=user_admin.username, enable_notifications=False)
1167 1167
1168 1168 self.app.post(
1169 1169 route_path(
1170 1170 'pullrequest_comment_delete',
1171 1171 repo_name=pull_request.target_repo.scm_instance().name,
1172 1172 pull_request_id=pull_request.pull_request_id,
1173 1173 comment_id=1024404),
1174 1174 extra_environ=xhr_header,
1175 1175 params={'csrf_token': csrf_token},
1176 1176 status=404
1177 1177 )
1178 1178
1179 1179 def test_delete_comment(
1180 1180 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1181 1181
1182 1182 pull_request = pr_util.create_pull_request(
1183 1183 author=user_admin.username, enable_notifications=False)
1184 1184 comment = pr_util.create_comment()
1185 1185 comment_id = comment.comment_id
1186 1186
1187 1187 response = self.app.post(
1188 1188 route_path(
1189 1189 'pullrequest_comment_delete',
1190 1190 repo_name=pull_request.target_repo.scm_instance().name,
1191 1191 pull_request_id=pull_request.pull_request_id,
1192 1192 comment_id=comment_id),
1193 1193 extra_environ=xhr_header,
1194 1194 params={'csrf_token': csrf_token},
1195 1195 status=200
1196 1196 )
1197 1197 assert response.body == 'true'
1198 1198
1199 1199 @pytest.mark.parametrize('url_type', [
1200 1200 'pullrequest_new',
1201 1201 'pullrequest_create',
1202 1202 'pullrequest_update',
1203 1203 'pullrequest_merge',
1204 1204 ])
1205 1205 def test_pull_request_is_forbidden_on_archived_repo(
1206 1206 self, autologin_user, backend, xhr_header, user_util, url_type):
1207 1207
1208 1208 # create a temporary repo
1209 1209 source = user_util.create_repo(repo_type=backend.alias)
1210 1210 repo_name = source.repo_name
1211 1211 repo = Repository.get_by_repo_name(repo_name)
1212 1212 repo.archived = True
1213 1213 Session().commit()
1214 1214
1215 1215 response = self.app.get(
1216 1216 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1217 1217
1218 1218 msg = 'Action not supported for archived repository.'
1219 1219 assert_session_flash(response, msg)
1220 1220
1221 1221
1222 1222 def assert_pull_request_status(pull_request, expected_status):
1223 1223 status = ChangesetStatusModel().calculated_review_status(
1224 1224 pull_request=pull_request)
1225 1225 assert status == expected_status
1226 1226
1227 1227
1228 1228 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1229 1229 @pytest.mark.usefixtures("autologin_user")
1230 1230 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1231 1231 response = app.get(
1232 1232 route_path(route, repo_name=backend_svn.repo_name), status=404)
1233 1233
@@ -1,1401 +1,1414 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name, source=source, opened_by=opened_by,
81 81 statuses=statuses, offset=start, length=limit,
82 82 order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name, source=source, statuses=statuses,
85 85 opened_by=opened_by)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, source=source, opened_by=opened_by,
89 89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by,
91 91 order_dir=order_dir)
92 92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 93 repo_name, source=source, user_id=self._rhodecode_user.user_id,
94 94 statuses=statuses, opened_by=opened_by)
95 95 else:
96 96 pull_requests = PullRequestModel().get_all(
97 97 repo_name, source=source, opened_by=opened_by,
98 98 statuses=statuses, offset=start, length=limit,
99 99 order_by=order_by, order_dir=order_dir)
100 100 pull_requests_total_count = PullRequestModel().count_all(
101 101 repo_name, source=source, statuses=statuses,
102 102 opened_by=opened_by)
103 103
104 104 data = []
105 105 comments_model = CommentsModel()
106 106 for pr in pull_requests:
107 107 comments = comments_model.get_all_comments(
108 108 self.db_repo.repo_id, pull_request=pr)
109 109
110 110 data.append({
111 111 'name': _render('pullrequest_name',
112 112 pr.pull_request_id, pr.target_repo.repo_name),
113 113 'name_raw': pr.pull_request_id,
114 114 'status': _render('pullrequest_status',
115 115 pr.calculated_review_status()),
116 116 'title': _render(
117 117 'pullrequest_title', pr.title, pr.description),
118 118 'description': h.escape(pr.description),
119 119 'updated_on': _render('pullrequest_updated_on',
120 120 h.datetime_to_time(pr.updated_on)),
121 121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 122 'created_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.created_on)),
124 124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 125 'author': _render('pullrequest_author',
126 126 pr.author.full_contact, ),
127 127 'author_raw': pr.author.full_name,
128 128 'comments': _render('pullrequest_comments', len(comments)),
129 129 'comments_raw': len(comments),
130 130 'closed': pr.is_closed(),
131 131 })
132 132
133 133 data = ({
134 134 'draw': draw,
135 135 'data': data,
136 136 'recordsTotal': pull_requests_total_count,
137 137 'recordsFiltered': pull_requests_total_count,
138 138 })
139 139 return data
140 140
141 141 def get_recache_flag(self):
142 142 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
143 143 flag_val = self.request.GET.get(flag_name)
144 144 if str2bool(flag_val):
145 145 return True
146 146 return False
147 147
148 148 @LoginRequired()
149 149 @HasRepoPermissionAnyDecorator(
150 150 'repository.read', 'repository.write', 'repository.admin')
151 151 @view_config(
152 152 route_name='pullrequest_show_all', request_method='GET',
153 153 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
154 154 def pull_request_list(self):
155 155 c = self.load_default_context()
156 156
157 157 req_get = self.request.GET
158 158 c.source = str2bool(req_get.get('source'))
159 159 c.closed = str2bool(req_get.get('closed'))
160 160 c.my = str2bool(req_get.get('my'))
161 161 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
162 162 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
163 163
164 164 c.active = 'open'
165 165 if c.my:
166 166 c.active = 'my'
167 167 if c.closed:
168 168 c.active = 'closed'
169 169 if c.awaiting_review and not c.source:
170 170 c.active = 'awaiting'
171 171 if c.source and not c.awaiting_review:
172 172 c.active = 'source'
173 173 if c.awaiting_my_review:
174 174 c.active = 'awaiting_my'
175 175
176 176 return self._get_template_context(c)
177 177
178 178 @LoginRequired()
179 179 @HasRepoPermissionAnyDecorator(
180 180 'repository.read', 'repository.write', 'repository.admin')
181 181 @view_config(
182 182 route_name='pullrequest_show_all_data', request_method='GET',
183 183 renderer='json_ext', xhr=True)
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 source_ref_id, target_ref_id,
221 221 target_commit, source_commit, diff_limit, file_limit,
222 222 fulldiff, hide_whitespace_changes, diff_context):
223 223
224 224 vcs_diff = PullRequestModel().get_diff(
225 225 source_repo, source_ref_id, target_ref_id,
226 226 hide_whitespace_changes, diff_context)
227 227
228 228 diff_processor = diffs.DiffProcessor(
229 229 vcs_diff, format='newdiff', diff_limit=diff_limit,
230 230 file_limit=file_limit, show_full_diff=fulldiff)
231 231
232 232 _parsed = diff_processor.prepare()
233 233
234 234 diffset = codeblocks.DiffSet(
235 235 repo_name=self.db_repo_name,
236 236 source_repo_name=source_repo_name,
237 237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
238 238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
239 239 )
240 240 diffset = self.path_filter.render_patchset_filtered(
241 241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
242 242
243 243 return diffset
244 244
245 245 def _get_range_diffset(self, source_scm, source_repo,
246 246 commit1, commit2, diff_limit, file_limit,
247 247 fulldiff, hide_whitespace_changes, diff_context):
248 248 vcs_diff = source_scm.get_diff(
249 249 commit1, commit2,
250 250 ignore_whitespace=hide_whitespace_changes,
251 251 context=diff_context)
252 252
253 253 diff_processor = diffs.DiffProcessor(
254 254 vcs_diff, format='newdiff', diff_limit=diff_limit,
255 255 file_limit=file_limit, show_full_diff=fulldiff)
256 256
257 257 _parsed = diff_processor.prepare()
258 258
259 259 diffset = codeblocks.DiffSet(
260 260 repo_name=source_repo.repo_name,
261 261 source_node_getter=codeblocks.diffset_node_getter(commit1),
262 262 target_node_getter=codeblocks.diffset_node_getter(commit2))
263 263
264 264 diffset = self.path_filter.render_patchset_filtered(
265 265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
266 266
267 267 return diffset
268 268
269 269 @LoginRequired()
270 270 @HasRepoPermissionAnyDecorator(
271 271 'repository.read', 'repository.write', 'repository.admin')
272 272 @view_config(
273 273 route_name='pullrequest_show', request_method='GET',
274 274 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
275 275 def pull_request_show(self):
276 276 pull_request_id = self.request.matchdict['pull_request_id']
277 277
278 278 c = self.load_default_context()
279 279
280 280 version = self.request.GET.get('version')
281 281 from_version = self.request.GET.get('from_version') or version
282 282 merge_checks = self.request.GET.get('merge_checks')
283 283 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
284 284
285 285 # fetch global flags of ignore ws or context lines
286 286 diff_context = diffs.get_diff_context(self.request)
287 287 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
288 288
289 289 force_refresh = str2bool(self.request.GET.get('force_refresh'))
290 290
291 291 (pull_request_latest,
292 292 pull_request_at_ver,
293 293 pull_request_display_obj,
294 294 at_version) = PullRequestModel().get_pr_version(
295 295 pull_request_id, version=version)
296 296 pr_closed = pull_request_latest.is_closed()
297 297
298 298 if pr_closed and (version or from_version):
299 299 # not allow to browse versions
300 300 raise HTTPFound(h.route_path(
301 301 'pullrequest_show', repo_name=self.db_repo_name,
302 302 pull_request_id=pull_request_id))
303 303
304 304 versions = pull_request_display_obj.versions()
305 305 # used to store per-commit range diffs
306 306 c.changes = collections.OrderedDict()
307 307 c.range_diff_on = self.request.GET.get('range-diff') == "1"
308 308
309 309 c.at_version = at_version
310 310 c.at_version_num = (at_version
311 311 if at_version and at_version != 'latest'
312 312 else None)
313 313 c.at_version_pos = ChangesetComment.get_index_from_version(
314 314 c.at_version_num, versions)
315 315
316 316 (prev_pull_request_latest,
317 317 prev_pull_request_at_ver,
318 318 prev_pull_request_display_obj,
319 319 prev_at_version) = PullRequestModel().get_pr_version(
320 320 pull_request_id, version=from_version)
321 321
322 322 c.from_version = prev_at_version
323 323 c.from_version_num = (prev_at_version
324 324 if prev_at_version and prev_at_version != 'latest'
325 325 else None)
326 326 c.from_version_pos = ChangesetComment.get_index_from_version(
327 327 c.from_version_num, versions)
328 328
329 329 # define if we're in COMPARE mode or VIEW at version mode
330 330 compare = at_version != prev_at_version
331 331
332 332 # pull_requests repo_name we opened it against
333 333 # ie. target_repo must match
334 334 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
335 335 raise HTTPNotFound()
336 336
337 337 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
338 338 pull_request_at_ver)
339 339
340 340 c.pull_request = pull_request_display_obj
341 341 c.renderer = pull_request_at_ver.description_renderer or c.renderer
342 342 c.pull_request_latest = pull_request_latest
343 343
344 344 if compare or (at_version and not at_version == 'latest'):
345 345 c.allowed_to_change_status = False
346 346 c.allowed_to_update = False
347 347 c.allowed_to_merge = False
348 348 c.allowed_to_delete = False
349 349 c.allowed_to_comment = False
350 350 c.allowed_to_close = False
351 351 else:
352 352 can_change_status = PullRequestModel().check_user_change_status(
353 353 pull_request_at_ver, self._rhodecode_user)
354 354 c.allowed_to_change_status = can_change_status and not pr_closed
355 355
356 356 c.allowed_to_update = PullRequestModel().check_user_update(
357 357 pull_request_latest, self._rhodecode_user) and not pr_closed
358 358 c.allowed_to_merge = PullRequestModel().check_user_merge(
359 359 pull_request_latest, self._rhodecode_user) and not pr_closed
360 360 c.allowed_to_delete = PullRequestModel().check_user_delete(
361 361 pull_request_latest, self._rhodecode_user) and not pr_closed
362 362 c.allowed_to_comment = not pr_closed
363 363 c.allowed_to_close = c.allowed_to_merge and not pr_closed
364 364
365 365 c.forbid_adding_reviewers = False
366 366 c.forbid_author_to_review = False
367 367 c.forbid_commit_author_to_review = False
368 368
369 369 if pull_request_latest.reviewer_data and \
370 370 'rules' in pull_request_latest.reviewer_data:
371 371 rules = pull_request_latest.reviewer_data['rules'] or {}
372 372 try:
373 373 c.forbid_adding_reviewers = rules.get(
374 374 'forbid_adding_reviewers')
375 375 c.forbid_author_to_review = rules.get(
376 376 'forbid_author_to_review')
377 377 c.forbid_commit_author_to_review = rules.get(
378 378 'forbid_commit_author_to_review')
379 379 except Exception:
380 380 pass
381 381
382 382 # check merge capabilities
383 383 _merge_check = MergeCheck.validate(
384 384 pull_request_latest, auth_user=self._rhodecode_user,
385 385 translator=self.request.translate,
386 386 force_shadow_repo_refresh=force_refresh)
387 387 c.pr_merge_errors = _merge_check.error_details
388 388 c.pr_merge_possible = not _merge_check.failed
389 389 c.pr_merge_message = _merge_check.merge_msg
390 390
391 391 c.pr_merge_info = MergeCheck.get_merge_conditions(
392 392 pull_request_latest, translator=self.request.translate)
393 393
394 394 c.pull_request_review_status = _merge_check.review_status
395 395 if merge_checks:
396 396 self.request.override_renderer = \
397 397 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
398 398 return self._get_template_context(c)
399 399
400 400 comments_model = CommentsModel()
401 401
402 402 # reviewers and statuses
403 403 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
404 404 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
405 405
406 406 # GENERAL COMMENTS with versions #
407 407 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
408 408 q = q.order_by(ChangesetComment.comment_id.asc())
409 409 general_comments = q
410 410
411 411 # pick comments we want to render at current version
412 412 c.comment_versions = comments_model.aggregate_comments(
413 413 general_comments, versions, c.at_version_num)
414 414 c.comments = c.comment_versions[c.at_version_num]['until']
415 415
416 416 # INLINE COMMENTS with versions #
417 417 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
418 418 q = q.order_by(ChangesetComment.comment_id.asc())
419 419 inline_comments = q
420 420
421 421 c.inline_versions = comments_model.aggregate_comments(
422 422 inline_comments, versions, c.at_version_num, inline=True)
423 423
424 424 # inject latest version
425 425 latest_ver = PullRequest.get_pr_display_object(
426 426 pull_request_latest, pull_request_latest)
427 427
428 428 c.versions = versions + [latest_ver]
429 429
430 430 # if we use version, then do not show later comments
431 431 # than current version
432 432 display_inline_comments = collections.defaultdict(
433 433 lambda: collections.defaultdict(list))
434 434 for co in inline_comments:
435 435 if c.at_version_num:
436 436 # pick comments that are at least UPTO given version, so we
437 437 # don't render comments for higher version
438 438 should_render = co.pull_request_version_id and \
439 439 co.pull_request_version_id <= c.at_version_num
440 440 else:
441 441 # showing all, for 'latest'
442 442 should_render = True
443 443
444 444 if should_render:
445 445 display_inline_comments[co.f_path][co.line_no].append(co)
446 446
447 447 # load diff data into template context, if we use compare mode then
448 448 # diff is calculated based on changes between versions of PR
449 449
450 450 source_repo = pull_request_at_ver.source_repo
451 451 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
452 452
453 453 target_repo = pull_request_at_ver.target_repo
454 454 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
455 455
456 456 if compare:
457 457 # in compare switch the diff base to latest commit from prev version
458 458 target_ref_id = prev_pull_request_display_obj.revisions[0]
459 459
460 460 # despite opening commits for bookmarks/branches/tags, we always
461 461 # convert this to rev to prevent changes after bookmark or branch change
462 462 c.source_ref_type = 'rev'
463 463 c.source_ref = source_ref_id
464 464
465 465 c.target_ref_type = 'rev'
466 466 c.target_ref = target_ref_id
467 467
468 468 c.source_repo = source_repo
469 469 c.target_repo = target_repo
470 470
471 471 c.commit_ranges = []
472 472 source_commit = EmptyCommit()
473 473 target_commit = EmptyCommit()
474 474 c.missing_requirements = False
475 475
476 476 source_scm = source_repo.scm_instance()
477 477 target_scm = target_repo.scm_instance()
478 478
479 479 shadow_scm = None
480 480 try:
481 481 shadow_scm = pull_request_latest.get_shadow_repo()
482 482 except Exception:
483 483 log.debug('Failed to get shadow repo', exc_info=True)
484 484 # try first the existing source_repo, and then shadow
485 485 # repo if we can obtain one
486 486 commits_source_repo = source_scm or shadow_scm
487 487
488 488 c.commits_source_repo = commits_source_repo
489 489 c.ancestor = None # set it to None, to hide it from PR view
490 490
491 491 # empty version means latest, so we keep this to prevent
492 492 # double caching
493 493 version_normalized = version or 'latest'
494 494 from_version_normalized = from_version or 'latest'
495 495
496 496 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
497 497 cache_file_path = diff_cache_exist(
498 498 cache_path, 'pull_request', pull_request_id, version_normalized,
499 499 from_version_normalized, source_ref_id, target_ref_id,
500 500 hide_whitespace_changes, diff_context, c.fulldiff)
501 501
502 502 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
503 503 force_recache = self.get_recache_flag()
504 504
505 505 cached_diff = None
506 506 if caching_enabled:
507 507 cached_diff = load_cached_diff(cache_file_path)
508 508
509 509 has_proper_commit_cache = (
510 510 cached_diff and cached_diff.get('commits')
511 511 and len(cached_diff.get('commits', [])) == 5
512 512 and cached_diff.get('commits')[0]
513 513 and cached_diff.get('commits')[3])
514 514
515 515 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
516 516 diff_commit_cache = \
517 517 (ancestor_commit, commit_cache, missing_requirements,
518 518 source_commit, target_commit) = cached_diff['commits']
519 519 else:
520 520 diff_commit_cache = \
521 521 (ancestor_commit, commit_cache, missing_requirements,
522 522 source_commit, target_commit) = self.get_commits(
523 523 commits_source_repo,
524 524 pull_request_at_ver,
525 525 source_commit,
526 526 source_ref_id,
527 527 source_scm,
528 528 target_commit,
529 529 target_ref_id,
530 530 target_scm)
531 531
532 532 # register our commit range
533 533 for comm in commit_cache.values():
534 534 c.commit_ranges.append(comm)
535 535
536 536 c.missing_requirements = missing_requirements
537 537 c.ancestor_commit = ancestor_commit
538 538 c.statuses = source_repo.statuses(
539 539 [x.raw_id for x in c.commit_ranges])
540 540
541 541 # auto collapse if we have more than limit
542 542 collapse_limit = diffs.DiffProcessor._collapse_commits_over
543 543 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
544 544 c.compare_mode = compare
545 545
546 546 # diff_limit is the old behavior, will cut off the whole diff
547 547 # if the limit is applied otherwise will just hide the
548 548 # big files from the front-end
549 549 diff_limit = c.visual.cut_off_limit_diff
550 550 file_limit = c.visual.cut_off_limit_file
551 551
552 552 c.missing_commits = False
553 553 if (c.missing_requirements
554 554 or isinstance(source_commit, EmptyCommit)
555 555 or source_commit == target_commit):
556 556
557 557 c.missing_commits = True
558 558 else:
559 559 c.inline_comments = display_inline_comments
560 560
561 561 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
562 562 if not force_recache and has_proper_diff_cache:
563 563 c.diffset = cached_diff['diff']
564 564 (ancestor_commit, commit_cache, missing_requirements,
565 565 source_commit, target_commit) = cached_diff['commits']
566 566 else:
567 567 c.diffset = self._get_diffset(
568 568 c.source_repo.repo_name, commits_source_repo,
569 569 source_ref_id, target_ref_id,
570 570 target_commit, source_commit,
571 571 diff_limit, file_limit, c.fulldiff,
572 572 hide_whitespace_changes, diff_context)
573 573
574 574 # save cached diff
575 575 if caching_enabled:
576 576 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
577 577
578 578 c.limited_diff = c.diffset.limited_diff
579 579
580 580 # calculate removed files that are bound to comments
581 581 comment_deleted_files = [
582 582 fname for fname in display_inline_comments
583 583 if fname not in c.diffset.file_stats]
584 584
585 585 c.deleted_files_comments = collections.defaultdict(dict)
586 586 for fname, per_line_comments in display_inline_comments.items():
587 587 if fname in comment_deleted_files:
588 588 c.deleted_files_comments[fname]['stats'] = 0
589 589 c.deleted_files_comments[fname]['comments'] = list()
590 590 for lno, comments in per_line_comments.items():
591 591 c.deleted_files_comments[fname]['comments'].extend(comments)
592 592
593 593 # maybe calculate the range diff
594 594 if c.range_diff_on:
595 595 # TODO(marcink): set whitespace/context
596 596 context_lcl = 3
597 597 ign_whitespace_lcl = False
598 598
599 599 for commit in c.commit_ranges:
600 600 commit2 = commit
601 601 commit1 = commit.first_parent
602 602
603 603 range_diff_cache_file_path = diff_cache_exist(
604 604 cache_path, 'diff', commit.raw_id,
605 605 ign_whitespace_lcl, context_lcl, c.fulldiff)
606 606
607 607 cached_diff = None
608 608 if caching_enabled:
609 609 cached_diff = load_cached_diff(range_diff_cache_file_path)
610 610
611 611 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
612 612 if not force_recache and has_proper_diff_cache:
613 613 diffset = cached_diff['diff']
614 614 else:
615 615 diffset = self._get_range_diffset(
616 616 source_scm, source_repo,
617 617 commit1, commit2, diff_limit, file_limit,
618 618 c.fulldiff, ign_whitespace_lcl, context_lcl
619 619 )
620 620
621 621 # save cached diff
622 622 if caching_enabled:
623 623 cache_diff(range_diff_cache_file_path, diffset, None)
624 624
625 625 c.changes[commit.raw_id] = diffset
626 626
627 627 # this is a hack to properly display links, when creating PR, the
628 628 # compare view and others uses different notation, and
629 629 # compare_commits.mako renders links based on the target_repo.
630 630 # We need to swap that here to generate it properly on the html side
631 631 c.target_repo = c.source_repo
632 632
633 633 c.commit_statuses = ChangesetStatus.STATUSES
634 634
635 635 c.show_version_changes = not pr_closed
636 636 if c.show_version_changes:
637 637 cur_obj = pull_request_at_ver
638 638 prev_obj = prev_pull_request_at_ver
639 639
640 640 old_commit_ids = prev_obj.revisions
641 641 new_commit_ids = cur_obj.revisions
642 642 commit_changes = PullRequestModel()._calculate_commit_id_changes(
643 643 old_commit_ids, new_commit_ids)
644 644 c.commit_changes_summary = commit_changes
645 645
646 646 # calculate the diff for commits between versions
647 647 c.commit_changes = []
648 648 mark = lambda cs, fw: list(
649 649 h.itertools.izip_longest([], cs, fillvalue=fw))
650 650 for c_type, raw_id in mark(commit_changes.added, 'a') \
651 651 + mark(commit_changes.removed, 'r') \
652 652 + mark(commit_changes.common, 'c'):
653 653
654 654 if raw_id in commit_cache:
655 655 commit = commit_cache[raw_id]
656 656 else:
657 657 try:
658 658 commit = commits_source_repo.get_commit(raw_id)
659 659 except CommitDoesNotExistError:
660 660 # in case we fail extracting still use "dummy" commit
661 661 # for display in commit diff
662 662 commit = h.AttributeDict(
663 663 {'raw_id': raw_id,
664 664 'message': 'EMPTY or MISSING COMMIT'})
665 665 c.commit_changes.append([c_type, commit])
666 666
667 667 # current user review statuses for each version
668 668 c.review_versions = {}
669 669 if self._rhodecode_user.user_id in allowed_reviewers:
670 670 for co in general_comments:
671 671 if co.author.user_id == self._rhodecode_user.user_id:
672 672 status = co.status_change
673 673 if status:
674 674 _ver_pr = status[0].comment.pull_request_version_id
675 675 c.review_versions[_ver_pr] = status[0]
676 676
677 677 return self._get_template_context(c)
678 678
679 679 def get_commits(
680 680 self, commits_source_repo, pull_request_at_ver, source_commit,
681 681 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
682 682 commit_cache = collections.OrderedDict()
683 683 missing_requirements = False
684 684 try:
685 685 pre_load = ["author", "branch", "date", "message", "parents"]
686 686 show_revs = pull_request_at_ver.revisions
687 687 for rev in show_revs:
688 688 comm = commits_source_repo.get_commit(
689 689 commit_id=rev, pre_load=pre_load)
690 690 commit_cache[comm.raw_id] = comm
691 691
692 692 # Order here matters, we first need to get target, and then
693 693 # the source
694 694 target_commit = commits_source_repo.get_commit(
695 695 commit_id=safe_str(target_ref_id))
696 696
697 697 source_commit = commits_source_repo.get_commit(
698 698 commit_id=safe_str(source_ref_id))
699 699 except CommitDoesNotExistError:
700 700 log.warning(
701 701 'Failed to get commit from `{}` repo'.format(
702 702 commits_source_repo), exc_info=True)
703 703 except RepositoryRequirementError:
704 704 log.warning(
705 705 'Failed to get all required data from repo', exc_info=True)
706 706 missing_requirements = True
707 707 ancestor_commit = None
708 708 try:
709 709 ancestor_id = source_scm.get_common_ancestor(
710 710 source_commit.raw_id, target_commit.raw_id, target_scm)
711 711 ancestor_commit = source_scm.get_commit(ancestor_id)
712 712 except Exception:
713 713 ancestor_commit = None
714 714 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
715 715
716 716 def assure_not_empty_repo(self):
717 717 _ = self.request.translate
718 718
719 719 try:
720 720 self.db_repo.scm_instance().get_commit()
721 721 except EmptyRepositoryError:
722 722 h.flash(h.literal(_('There are no commits yet')),
723 723 category='warning')
724 724 raise HTTPFound(
725 725 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
726 726
727 727 @LoginRequired()
728 728 @NotAnonymous()
729 729 @HasRepoPermissionAnyDecorator(
730 730 'repository.read', 'repository.write', 'repository.admin')
731 731 @view_config(
732 732 route_name='pullrequest_new', request_method='GET',
733 733 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
734 734 def pull_request_new(self):
735 735 _ = self.request.translate
736 736 c = self.load_default_context()
737 737
738 738 self.assure_not_empty_repo()
739 739 source_repo = self.db_repo
740 740
741 741 commit_id = self.request.GET.get('commit')
742 742 branch_ref = self.request.GET.get('branch')
743 743 bookmark_ref = self.request.GET.get('bookmark')
744 744
745 745 try:
746 746 source_repo_data = PullRequestModel().generate_repo_data(
747 747 source_repo, commit_id=commit_id,
748 748 branch=branch_ref, bookmark=bookmark_ref,
749 749 translator=self.request.translate)
750 750 except CommitDoesNotExistError as e:
751 751 log.exception(e)
752 752 h.flash(_('Commit does not exist'), 'error')
753 753 raise HTTPFound(
754 754 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
755 755
756 756 default_target_repo = source_repo
757 757
758 758 if source_repo.parent:
759 759 parent_vcs_obj = source_repo.parent.scm_instance()
760 760 if parent_vcs_obj and not parent_vcs_obj.is_empty():
761 761 # change default if we have a parent repo
762 762 default_target_repo = source_repo.parent
763 763
764 764 target_repo_data = PullRequestModel().generate_repo_data(
765 765 default_target_repo, translator=self.request.translate)
766 766
767 767 selected_source_ref = source_repo_data['refs']['selected_ref']
768 768 title_source_ref = ''
769 769 if selected_source_ref:
770 770 title_source_ref = selected_source_ref.split(':', 2)[1]
771 771 c.default_title = PullRequestModel().generate_pullrequest_title(
772 772 source=source_repo.repo_name,
773 773 source_ref=title_source_ref,
774 774 target=default_target_repo.repo_name
775 775 )
776 776
777 777 c.default_repo_data = {
778 778 'source_repo_name': source_repo.repo_name,
779 779 'source_refs_json': json.dumps(source_repo_data),
780 780 'target_repo_name': default_target_repo.repo_name,
781 781 'target_refs_json': json.dumps(target_repo_data),
782 782 }
783 783 c.default_source_ref = selected_source_ref
784 784
785 785 return self._get_template_context(c)
786 786
787 787 @LoginRequired()
788 788 @NotAnonymous()
789 789 @HasRepoPermissionAnyDecorator(
790 790 'repository.read', 'repository.write', 'repository.admin')
791 791 @view_config(
792 792 route_name='pullrequest_repo_refs', request_method='GET',
793 793 renderer='json_ext', xhr=True)
794 794 def pull_request_repo_refs(self):
795 795 self.load_default_context()
796 796 target_repo_name = self.request.matchdict['target_repo_name']
797 797 repo = Repository.get_by_repo_name(target_repo_name)
798 798 if not repo:
799 799 raise HTTPNotFound()
800 800
801 801 target_perm = HasRepoPermissionAny(
802 802 'repository.read', 'repository.write', 'repository.admin')(
803 803 target_repo_name)
804 804 if not target_perm:
805 805 raise HTTPNotFound()
806 806
807 807 return PullRequestModel().generate_repo_data(
808 808 repo, translator=self.request.translate)
809 809
810 810 @LoginRequired()
811 811 @NotAnonymous()
812 812 @HasRepoPermissionAnyDecorator(
813 813 'repository.read', 'repository.write', 'repository.admin')
814 814 @view_config(
815 route_name='pullrequest_repo_destinations', request_method='GET',
815 route_name='pullrequest_repo_targets', request_method='GET',
816 816 renderer='json_ext', xhr=True)
817 def pull_request_repo_destinations(self):
817 def pullrequest_repo_targets(self):
818 818 _ = self.request.translate
819 819 filter_query = self.request.GET.get('query')
820 820
821 # get the parents
822 parent_target_repos = []
823 if self.db_repo.parent:
824 parents_query = Repository.query() \
825 .order_by(func.length(Repository.repo_name)) \
826 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
827
828 if filter_query:
829 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
830 parents_query = parents_query.filter(
831 Repository.repo_name.ilike(ilike_expression))
832 parents = parents_query.limit(20).all()
833
834 for parent in parents:
835 parent_vcs_obj = parent.scm_instance()
836 if parent_vcs_obj and not parent_vcs_obj.is_empty():
837 parent_target_repos.append(parent)
838
839 # get other forks, and repo itself
821 840 query = Repository.query() \
822 841 .order_by(func.length(Repository.repo_name)) \
823 842 .filter(
824 or_(Repository.repo_name == self.db_repo.repo_name,
825 Repository.fork_id == self.db_repo.repo_id))
843 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
844 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
845 ) \
846 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
826 847
827 848 if filter_query:
828 849 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
829 query = query.filter(
830 Repository.repo_name.ilike(ilike_expression))
850 query = query.filter(Repository.repo_name.ilike(ilike_expression))
831 851
832 add_parent = False
833 if self.db_repo.parent:
834 if filter_query in self.db_repo.parent.repo_name:
835 parent_vcs_obj = self.db_repo.parent.scm_instance()
836 if parent_vcs_obj and not parent_vcs_obj.is_empty():
837 add_parent = True
852 limit = max(20 - len(parent_target_repos), 5) # not less then 5
853 target_repos = query.limit(limit).all()
838 854
839 limit = 20 - 1 if add_parent else 20
840 all_repos = query.limit(limit).all()
841 if add_parent:
842 all_repos += [self.db_repo.parent]
855 all_target_repos = target_repos + parent_target_repos
843 856
844 857 repos = []
845 for obj in ScmModel().get_repos(all_repos):
858 for obj in ScmModel().get_repos(all_target_repos):
846 859 repos.append({
847 860 'id': obj['name'],
848 861 'text': obj['name'],
849 862 'type': 'repo',
850 863 'repo_id': obj['dbrepo']['repo_id'],
851 864 'repo_type': obj['dbrepo']['repo_type'],
852 865 'private': obj['dbrepo']['private'],
853 866
854 867 })
855 868
856 869 data = {
857 870 'more': False,
858 871 'results': [{
859 872 'text': _('Repositories'),
860 873 'children': repos
861 874 }] if repos else []
862 875 }
863 876 return data
864 877
865 878 @LoginRequired()
866 879 @NotAnonymous()
867 880 @HasRepoPermissionAnyDecorator(
868 881 'repository.read', 'repository.write', 'repository.admin')
869 882 @CSRFRequired()
870 883 @view_config(
871 884 route_name='pullrequest_create', request_method='POST',
872 885 renderer=None)
873 886 def pull_request_create(self):
874 887 _ = self.request.translate
875 888 self.assure_not_empty_repo()
876 889 self.load_default_context()
877 890
878 891 controls = peppercorn.parse(self.request.POST.items())
879 892
880 893 try:
881 894 form = PullRequestForm(
882 895 self.request.translate, self.db_repo.repo_id)()
883 896 _form = form.to_python(controls)
884 897 except formencode.Invalid as errors:
885 898 if errors.error_dict.get('revisions'):
886 899 msg = 'Revisions: %s' % errors.error_dict['revisions']
887 900 elif errors.error_dict.get('pullrequest_title'):
888 901 msg = errors.error_dict.get('pullrequest_title')
889 902 else:
890 903 msg = _('Error creating pull request: {}').format(errors)
891 904 log.exception(msg)
892 905 h.flash(msg, 'error')
893 906
894 907 # would rather just go back to form ...
895 908 raise HTTPFound(
896 909 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
897 910
898 911 source_repo = _form['source_repo']
899 912 source_ref = _form['source_ref']
900 913 target_repo = _form['target_repo']
901 914 target_ref = _form['target_ref']
902 915 commit_ids = _form['revisions'][::-1]
903 916
904 917 # find the ancestor for this pr
905 918 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
906 919 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
907 920
908 921 # re-check permissions again here
909 922 # source_repo we must have read permissions
910 923
911 924 source_perm = HasRepoPermissionAny(
912 925 'repository.read',
913 926 'repository.write', 'repository.admin')(source_db_repo.repo_name)
914 927 if not source_perm:
915 928 msg = _('Not Enough permissions to source repo `{}`.'.format(
916 929 source_db_repo.repo_name))
917 930 h.flash(msg, category='error')
918 931 # copy the args back to redirect
919 932 org_query = self.request.GET.mixed()
920 933 raise HTTPFound(
921 934 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
922 935 _query=org_query))
923 936
924 937 # target repo we must have read permissions, and also later on
925 938 # we want to check branch permissions here
926 939 target_perm = HasRepoPermissionAny(
927 940 'repository.read',
928 941 'repository.write', 'repository.admin')(target_db_repo.repo_name)
929 942 if not target_perm:
930 943 msg = _('Not Enough permissions to target repo `{}`.'.format(
931 944 target_db_repo.repo_name))
932 945 h.flash(msg, category='error')
933 946 # copy the args back to redirect
934 947 org_query = self.request.GET.mixed()
935 948 raise HTTPFound(
936 949 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
937 950 _query=org_query))
938 951
939 952 source_scm = source_db_repo.scm_instance()
940 953 target_scm = target_db_repo.scm_instance()
941 954
942 955 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
943 956 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
944 957
945 958 ancestor = source_scm.get_common_ancestor(
946 959 source_commit.raw_id, target_commit.raw_id, target_scm)
947 960
948 961 # recalculate target ref based on ancestor
949 962 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
950 963 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
951 964
952 965 get_default_reviewers_data, validate_default_reviewers = \
953 966 PullRequestModel().get_reviewer_functions()
954 967
955 968 # recalculate reviewers logic, to make sure we can validate this
956 969 reviewer_rules = get_default_reviewers_data(
957 970 self._rhodecode_db_user, source_db_repo,
958 971 source_commit, target_db_repo, target_commit)
959 972
960 973 given_reviewers = _form['review_members']
961 974 reviewers = validate_default_reviewers(
962 975 given_reviewers, reviewer_rules)
963 976
964 977 pullrequest_title = _form['pullrequest_title']
965 978 title_source_ref = source_ref.split(':', 2)[1]
966 979 if not pullrequest_title:
967 980 pullrequest_title = PullRequestModel().generate_pullrequest_title(
968 981 source=source_repo,
969 982 source_ref=title_source_ref,
970 983 target=target_repo
971 984 )
972 985
973 986 description = _form['pullrequest_desc']
974 987 description_renderer = _form['description_renderer']
975 988
976 989 try:
977 990 pull_request = PullRequestModel().create(
978 991 created_by=self._rhodecode_user.user_id,
979 992 source_repo=source_repo,
980 993 source_ref=source_ref,
981 994 target_repo=target_repo,
982 995 target_ref=target_ref,
983 996 revisions=commit_ids,
984 997 reviewers=reviewers,
985 998 title=pullrequest_title,
986 999 description=description,
987 1000 description_renderer=description_renderer,
988 1001 reviewer_data=reviewer_rules,
989 1002 auth_user=self._rhodecode_user
990 1003 )
991 1004 Session().commit()
992 1005
993 1006 h.flash(_('Successfully opened new pull request'),
994 1007 category='success')
995 1008 except Exception:
996 1009 msg = _('Error occurred during creation of this pull request.')
997 1010 log.exception(msg)
998 1011 h.flash(msg, category='error')
999 1012
1000 1013 # copy the args back to redirect
1001 1014 org_query = self.request.GET.mixed()
1002 1015 raise HTTPFound(
1003 1016 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1004 1017 _query=org_query))
1005 1018
1006 1019 raise HTTPFound(
1007 1020 h.route_path('pullrequest_show', repo_name=target_repo,
1008 1021 pull_request_id=pull_request.pull_request_id))
1009 1022
1010 1023 @LoginRequired()
1011 1024 @NotAnonymous()
1012 1025 @HasRepoPermissionAnyDecorator(
1013 1026 'repository.read', 'repository.write', 'repository.admin')
1014 1027 @CSRFRequired()
1015 1028 @view_config(
1016 1029 route_name='pullrequest_update', request_method='POST',
1017 1030 renderer='json_ext')
1018 1031 def pull_request_update(self):
1019 1032 pull_request = PullRequest.get_or_404(
1020 1033 self.request.matchdict['pull_request_id'])
1021 1034 _ = self.request.translate
1022 1035
1023 1036 self.load_default_context()
1024 1037
1025 1038 if pull_request.is_closed():
1026 1039 log.debug('update: forbidden because pull request is closed')
1027 1040 msg = _(u'Cannot update closed pull requests.')
1028 1041 h.flash(msg, category='error')
1029 1042 return True
1030 1043
1031 1044 # only owner or admin can update it
1032 1045 allowed_to_update = PullRequestModel().check_user_update(
1033 1046 pull_request, self._rhodecode_user)
1034 1047 if allowed_to_update:
1035 1048 controls = peppercorn.parse(self.request.POST.items())
1036 1049
1037 1050 if 'review_members' in controls:
1038 1051 self._update_reviewers(
1039 1052 pull_request, controls['review_members'],
1040 1053 pull_request.reviewer_data)
1041 1054 elif str2bool(self.request.POST.get('update_commits', 'false')):
1042 1055 self._update_commits(pull_request)
1043 1056 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1044 1057 self._edit_pull_request(pull_request)
1045 1058 else:
1046 1059 raise HTTPBadRequest()
1047 1060 return True
1048 1061 raise HTTPForbidden()
1049 1062
1050 1063 def _edit_pull_request(self, pull_request):
1051 1064 _ = self.request.translate
1052 1065
1053 1066 try:
1054 1067 PullRequestModel().edit(
1055 1068 pull_request,
1056 1069 self.request.POST.get('title'),
1057 1070 self.request.POST.get('description'),
1058 1071 self.request.POST.get('description_renderer'),
1059 1072 self._rhodecode_user)
1060 1073 except ValueError:
1061 1074 msg = _(u'Cannot update closed pull requests.')
1062 1075 h.flash(msg, category='error')
1063 1076 return
1064 1077 else:
1065 1078 Session().commit()
1066 1079
1067 1080 msg = _(u'Pull request title & description updated.')
1068 1081 h.flash(msg, category='success')
1069 1082 return
1070 1083
1071 1084 def _update_commits(self, pull_request):
1072 1085 _ = self.request.translate
1073 1086 resp = PullRequestModel().update_commits(pull_request)
1074 1087
1075 1088 if resp.executed:
1076 1089
1077 1090 if resp.target_changed and resp.source_changed:
1078 1091 changed = 'target and source repositories'
1079 1092 elif resp.target_changed and not resp.source_changed:
1080 1093 changed = 'target repository'
1081 1094 elif not resp.target_changed and resp.source_changed:
1082 1095 changed = 'source repository'
1083 1096 else:
1084 1097 changed = 'nothing'
1085 1098
1086 1099 msg = _(
1087 1100 u'Pull request updated to "{source_commit_id}" with '
1088 1101 u'{count_added} added, {count_removed} removed commits. '
1089 1102 u'Source of changes: {change_source}')
1090 1103 msg = msg.format(
1091 1104 source_commit_id=pull_request.source_ref_parts.commit_id,
1092 1105 count_added=len(resp.changes.added),
1093 1106 count_removed=len(resp.changes.removed),
1094 1107 change_source=changed)
1095 1108 h.flash(msg, category='success')
1096 1109
1097 1110 channel = '/repo${}$/pr/{}'.format(
1098 1111 pull_request.target_repo.repo_name,
1099 1112 pull_request.pull_request_id)
1100 1113 message = msg + (
1101 1114 ' - <a onclick="window.location.reload()">'
1102 1115 '<strong>{}</strong></a>'.format(_('Reload page')))
1103 1116 channelstream.post_message(
1104 1117 channel, message, self._rhodecode_user.username,
1105 1118 registry=self.request.registry)
1106 1119 else:
1107 1120 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1108 1121 warning_reasons = [
1109 1122 UpdateFailureReason.NO_CHANGE,
1110 1123 UpdateFailureReason.WRONG_REF_TYPE,
1111 1124 ]
1112 1125 category = 'warning' if resp.reason in warning_reasons else 'error'
1113 1126 h.flash(msg, category=category)
1114 1127
1115 1128 @LoginRequired()
1116 1129 @NotAnonymous()
1117 1130 @HasRepoPermissionAnyDecorator(
1118 1131 'repository.read', 'repository.write', 'repository.admin')
1119 1132 @CSRFRequired()
1120 1133 @view_config(
1121 1134 route_name='pullrequest_merge', request_method='POST',
1122 1135 renderer='json_ext')
1123 1136 def pull_request_merge(self):
1124 1137 """
1125 1138 Merge will perform a server-side merge of the specified
1126 1139 pull request, if the pull request is approved and mergeable.
1127 1140 After successful merging, the pull request is automatically
1128 1141 closed, with a relevant comment.
1129 1142 """
1130 1143 pull_request = PullRequest.get_or_404(
1131 1144 self.request.matchdict['pull_request_id'])
1132 1145
1133 1146 self.load_default_context()
1134 1147 check = MergeCheck.validate(
1135 1148 pull_request, auth_user=self._rhodecode_user,
1136 1149 translator=self.request.translate)
1137 1150 merge_possible = not check.failed
1138 1151
1139 1152 for err_type, error_msg in check.errors:
1140 1153 h.flash(error_msg, category=err_type)
1141 1154
1142 1155 if merge_possible:
1143 1156 log.debug("Pre-conditions checked, trying to merge.")
1144 1157 extras = vcs_operation_context(
1145 1158 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1146 1159 username=self._rhodecode_db_user.username, action='push',
1147 1160 scm=pull_request.target_repo.repo_type)
1148 1161 self._merge_pull_request(
1149 1162 pull_request, self._rhodecode_db_user, extras)
1150 1163 else:
1151 1164 log.debug("Pre-conditions failed, NOT merging.")
1152 1165
1153 1166 raise HTTPFound(
1154 1167 h.route_path('pullrequest_show',
1155 1168 repo_name=pull_request.target_repo.repo_name,
1156 1169 pull_request_id=pull_request.pull_request_id))
1157 1170
1158 1171 def _merge_pull_request(self, pull_request, user, extras):
1159 1172 _ = self.request.translate
1160 1173 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1161 1174
1162 1175 if merge_resp.executed:
1163 1176 log.debug("The merge was successful, closing the pull request.")
1164 1177 PullRequestModel().close_pull_request(
1165 1178 pull_request.pull_request_id, user)
1166 1179 Session().commit()
1167 1180 msg = _('Pull request was successfully merged and closed.')
1168 1181 h.flash(msg, category='success')
1169 1182 else:
1170 1183 log.debug(
1171 1184 "The merge was not successful. Merge response: %s",
1172 1185 merge_resp)
1173 1186 msg = PullRequestModel().merge_status_message(
1174 1187 merge_resp.failure_reason)
1175 1188 h.flash(msg, category='error')
1176 1189
1177 1190 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1178 1191 _ = self.request.translate
1179 1192 get_default_reviewers_data, validate_default_reviewers = \
1180 1193 PullRequestModel().get_reviewer_functions()
1181 1194
1182 1195 try:
1183 1196 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1184 1197 except ValueError as e:
1185 1198 log.error('Reviewers Validation: {}'.format(e))
1186 1199 h.flash(e, category='error')
1187 1200 return
1188 1201
1189 1202 PullRequestModel().update_reviewers(
1190 1203 pull_request, reviewers, self._rhodecode_user)
1191 1204 h.flash(_('Pull request reviewers updated.'), category='success')
1192 1205 Session().commit()
1193 1206
1194 1207 @LoginRequired()
1195 1208 @NotAnonymous()
1196 1209 @HasRepoPermissionAnyDecorator(
1197 1210 'repository.read', 'repository.write', 'repository.admin')
1198 1211 @CSRFRequired()
1199 1212 @view_config(
1200 1213 route_name='pullrequest_delete', request_method='POST',
1201 1214 renderer='json_ext')
1202 1215 def pull_request_delete(self):
1203 1216 _ = self.request.translate
1204 1217
1205 1218 pull_request = PullRequest.get_or_404(
1206 1219 self.request.matchdict['pull_request_id'])
1207 1220 self.load_default_context()
1208 1221
1209 1222 pr_closed = pull_request.is_closed()
1210 1223 allowed_to_delete = PullRequestModel().check_user_delete(
1211 1224 pull_request, self._rhodecode_user) and not pr_closed
1212 1225
1213 1226 # only owner can delete it !
1214 1227 if allowed_to_delete:
1215 1228 PullRequestModel().delete(pull_request, self._rhodecode_user)
1216 1229 Session().commit()
1217 1230 h.flash(_('Successfully deleted pull request'),
1218 1231 category='success')
1219 1232 raise HTTPFound(h.route_path('pullrequest_show_all',
1220 1233 repo_name=self.db_repo_name))
1221 1234
1222 1235 log.warning('user %s tried to delete pull request without access',
1223 1236 self._rhodecode_user)
1224 1237 raise HTTPNotFound()
1225 1238
1226 1239 @LoginRequired()
1227 1240 @NotAnonymous()
1228 1241 @HasRepoPermissionAnyDecorator(
1229 1242 'repository.read', 'repository.write', 'repository.admin')
1230 1243 @CSRFRequired()
1231 1244 @view_config(
1232 1245 route_name='pullrequest_comment_create', request_method='POST',
1233 1246 renderer='json_ext')
1234 1247 def pull_request_comment_create(self):
1235 1248 _ = self.request.translate
1236 1249
1237 1250 pull_request = PullRequest.get_or_404(
1238 1251 self.request.matchdict['pull_request_id'])
1239 1252 pull_request_id = pull_request.pull_request_id
1240 1253
1241 1254 if pull_request.is_closed():
1242 1255 log.debug('comment: forbidden because pull request is closed')
1243 1256 raise HTTPForbidden()
1244 1257
1245 1258 allowed_to_comment = PullRequestModel().check_user_comment(
1246 1259 pull_request, self._rhodecode_user)
1247 1260 if not allowed_to_comment:
1248 1261 log.debug(
1249 1262 'comment: forbidden because pull request is from forbidden repo')
1250 1263 raise HTTPForbidden()
1251 1264
1252 1265 c = self.load_default_context()
1253 1266
1254 1267 status = self.request.POST.get('changeset_status', None)
1255 1268 text = self.request.POST.get('text')
1256 1269 comment_type = self.request.POST.get('comment_type')
1257 1270 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1258 1271 close_pull_request = self.request.POST.get('close_pull_request')
1259 1272
1260 1273 # the logic here should work like following, if we submit close
1261 1274 # pr comment, use `close_pull_request_with_comment` function
1262 1275 # else handle regular comment logic
1263 1276
1264 1277 if close_pull_request:
1265 1278 # only owner or admin or person with write permissions
1266 1279 allowed_to_close = PullRequestModel().check_user_update(
1267 1280 pull_request, self._rhodecode_user)
1268 1281 if not allowed_to_close:
1269 1282 log.debug('comment: forbidden because not allowed to close '
1270 1283 'pull request %s', pull_request_id)
1271 1284 raise HTTPForbidden()
1272 1285 comment, status = PullRequestModel().close_pull_request_with_comment(
1273 1286 pull_request, self._rhodecode_user, self.db_repo, message=text,
1274 1287 auth_user=self._rhodecode_user)
1275 1288 Session().flush()
1276 1289 events.trigger(
1277 1290 events.PullRequestCommentEvent(pull_request, comment))
1278 1291
1279 1292 else:
1280 1293 # regular comment case, could be inline, or one with status.
1281 1294 # for that one we check also permissions
1282 1295
1283 1296 allowed_to_change_status = PullRequestModel().check_user_change_status(
1284 1297 pull_request, self._rhodecode_user)
1285 1298
1286 1299 if status and allowed_to_change_status:
1287 1300 message = (_('Status change %(transition_icon)s %(status)s')
1288 1301 % {'transition_icon': '>',
1289 1302 'status': ChangesetStatus.get_status_lbl(status)})
1290 1303 text = text or message
1291 1304
1292 1305 comment = CommentsModel().create(
1293 1306 text=text,
1294 1307 repo=self.db_repo.repo_id,
1295 1308 user=self._rhodecode_user.user_id,
1296 1309 pull_request=pull_request,
1297 1310 f_path=self.request.POST.get('f_path'),
1298 1311 line_no=self.request.POST.get('line'),
1299 1312 status_change=(ChangesetStatus.get_status_lbl(status)
1300 1313 if status and allowed_to_change_status else None),
1301 1314 status_change_type=(status
1302 1315 if status and allowed_to_change_status else None),
1303 1316 comment_type=comment_type,
1304 1317 resolves_comment_id=resolves_comment_id,
1305 1318 auth_user=self._rhodecode_user
1306 1319 )
1307 1320
1308 1321 if allowed_to_change_status:
1309 1322 # calculate old status before we change it
1310 1323 old_calculated_status = pull_request.calculated_review_status()
1311 1324
1312 1325 # get status if set !
1313 1326 if status:
1314 1327 ChangesetStatusModel().set_status(
1315 1328 self.db_repo.repo_id,
1316 1329 status,
1317 1330 self._rhodecode_user.user_id,
1318 1331 comment,
1319 1332 pull_request=pull_request
1320 1333 )
1321 1334
1322 1335 Session().flush()
1323 1336 # this is somehow required to get access to some relationship
1324 1337 # loaded on comment
1325 1338 Session().refresh(comment)
1326 1339
1327 1340 events.trigger(
1328 1341 events.PullRequestCommentEvent(pull_request, comment))
1329 1342
1330 1343 # we now calculate the status of pull request, and based on that
1331 1344 # calculation we set the commits status
1332 1345 calculated_status = pull_request.calculated_review_status()
1333 1346 if old_calculated_status != calculated_status:
1334 1347 PullRequestModel()._trigger_pull_request_hook(
1335 1348 pull_request, self._rhodecode_user, 'review_status_change')
1336 1349
1337 1350 Session().commit()
1338 1351
1339 1352 data = {
1340 1353 'target_id': h.safeid(h.safe_unicode(
1341 1354 self.request.POST.get('f_path'))),
1342 1355 }
1343 1356 if comment:
1344 1357 c.co = comment
1345 1358 rendered_comment = render(
1346 1359 'rhodecode:templates/changeset/changeset_comment_block.mako',
1347 1360 self._get_template_context(c), self.request)
1348 1361
1349 1362 data.update(comment.get_dict())
1350 1363 data.update({'rendered_text': rendered_comment})
1351 1364
1352 1365 return data
1353 1366
1354 1367 @LoginRequired()
1355 1368 @NotAnonymous()
1356 1369 @HasRepoPermissionAnyDecorator(
1357 1370 'repository.read', 'repository.write', 'repository.admin')
1358 1371 @CSRFRequired()
1359 1372 @view_config(
1360 1373 route_name='pullrequest_comment_delete', request_method='POST',
1361 1374 renderer='json_ext')
1362 1375 def pull_request_comment_delete(self):
1363 1376 pull_request = PullRequest.get_or_404(
1364 1377 self.request.matchdict['pull_request_id'])
1365 1378
1366 1379 comment = ChangesetComment.get_or_404(
1367 1380 self.request.matchdict['comment_id'])
1368 1381 comment_id = comment.comment_id
1369 1382
1370 1383 if pull_request.is_closed():
1371 1384 log.debug('comment: forbidden because pull request is closed')
1372 1385 raise HTTPForbidden()
1373 1386
1374 1387 if not comment:
1375 1388 log.debug('Comment with id:%s not found, skipping', comment_id)
1376 1389 # comment already deleted in another call probably
1377 1390 return True
1378 1391
1379 1392 if comment.pull_request.is_closed():
1380 1393 # don't allow deleting comments on closed pull request
1381 1394 raise HTTPForbidden()
1382 1395
1383 1396 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1384 1397 super_admin = h.HasPermissionAny('hg.admin')()
1385 1398 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1386 1399 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1387 1400 comment_repo_admin = is_repo_admin and is_repo_comment
1388 1401
1389 1402 if super_admin or comment_owner or comment_repo_admin:
1390 1403 old_calculated_status = comment.pull_request.calculated_review_status()
1391 1404 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1392 1405 Session().commit()
1393 1406 calculated_status = comment.pull_request.calculated_review_status()
1394 1407 if old_calculated_status != calculated_status:
1395 1408 PullRequestModel()._trigger_pull_request_hook(
1396 1409 comment.pull_request, self._rhodecode_user, 'review_status_change')
1397 1410 return True
1398 1411 else:
1399 1412 log.warning('No permissions for user %s to delete comment_id: %s',
1400 1413 self._rhodecode_db_user, comment_id)
1401 1414 raise HTTPNotFound()
@@ -1,336 +1,336 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('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
18 18 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
19 19 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
20 20 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
21 21 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
22 22 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
23 23 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
25 25 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
27 27 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
28 28 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
29 29 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
30 30 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
32 32 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
33 33 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 34 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 35 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
36 36 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
37 37 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
38 38 pyroutes.register('admin_home', '/_admin', []);
39 39 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 40 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
41 41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 48 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
52 52 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
53 53 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
54 54 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
55 55 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
56 56 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
57 57 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
58 58 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
59 59 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
60 60 pyroutes.register('admin_settings', '/_admin/settings', []);
61 61 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
62 62 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
63 63 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
64 64 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
65 65 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
66 66 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
67 67 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
68 68 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
69 69 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
70 70 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
71 71 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
72 72 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
73 73 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
74 74 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
75 75 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
76 76 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
77 77 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
78 78 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
79 79 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
80 80 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
81 81 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
82 82 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
83 83 pyroutes.register('admin_settings_automation', '/_admin/_admin/settings/automation', []);
84 84 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
85 85 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
86 86 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
87 87 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
88 88 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
89 89 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
90 90 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
91 91 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
92 92 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
93 93 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
94 94 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
95 95 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
96 96 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
97 97 pyroutes.register('users', '/_admin/users', []);
98 98 pyroutes.register('users_data', '/_admin/users_data', []);
99 99 pyroutes.register('users_create', '/_admin/users/create', []);
100 100 pyroutes.register('users_new', '/_admin/users/new', []);
101 101 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
102 102 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
103 103 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
104 104 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
105 105 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
106 106 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
107 107 pyroutes.register('user_force_password_reset', '/_admin/users/%(user_id)s/password_reset', ['user_id']);
108 108 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
109 109 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
110 110 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
111 111 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
113 113 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
114 114 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
115 115 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
116 116 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
117 117 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
118 118 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
119 119 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
120 120 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
121 121 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
122 122 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
123 123 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
124 124 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
125 125 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
126 126 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
127 127 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
128 128 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
129 129 pyroutes.register('user_groups', '/_admin/user_groups', []);
130 130 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
131 131 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
132 132 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
133 133 pyroutes.register('repos', '/_admin/repos', []);
134 134 pyroutes.register('repo_new', '/_admin/repos/new', []);
135 135 pyroutes.register('repo_create', '/_admin/repos/create', []);
136 136 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
137 137 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
138 138 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
139 139 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
140 140 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
141 141 pyroutes.register('channelstream_proxy', '/_channelstream', []);
142 142 pyroutes.register('login', '/_admin/login', []);
143 143 pyroutes.register('logout', '/_admin/logout', []);
144 144 pyroutes.register('register', '/_admin/register', []);
145 145 pyroutes.register('reset_password', '/_admin/password_reset', []);
146 146 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
147 147 pyroutes.register('home', '/', []);
148 148 pyroutes.register('user_autocomplete_data', '/_users', []);
149 149 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
150 150 pyroutes.register('repo_list_data', '/_repos', []);
151 151 pyroutes.register('goto_switcher_data', '/_goto_data', []);
152 152 pyroutes.register('markup_preview', '/_markup_preview', []);
153 153 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
154 154 pyroutes.register('journal', '/_admin/journal', []);
155 155 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
156 156 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
157 157 pyroutes.register('journal_public', '/_admin/public_journal', []);
158 158 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
159 159 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
160 160 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
161 161 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
162 162 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
163 163 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
164 164 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
165 165 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
166 166 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
167 167 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
168 168 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
169 169 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
170 170 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
171 171 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
172 172 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
173 173 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
174 174 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
175 175 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
176 176 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
177 177 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
178 178 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
179 179 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
180 180 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
181 181 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
182 182 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
183 183 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
184 184 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
185 185 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
186 186 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
187 187 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 188 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
189 189 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
190 190 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 191 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 192 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 193 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 194 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
195 195 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 196 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 198 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 199 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 200 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 201 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
202 202 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
203 203 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
204 204 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
205 205 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_changelog_elements', '/%(repo_name)s/changelog_elements', ['repo_name']);
207 207 pyroutes.register('repo_changelog_elements_file', '/%(repo_name)s/changelog_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 208 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
209 209 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']);
210 210 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
211 211 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
212 212 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
213 213 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
214 214 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
215 215 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
216 216 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
217 217 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
218 218 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
219 219 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
220 220 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
221 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
221 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
222 222 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
223 223 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
224 224 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
225 225 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
226 226 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
227 227 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
228 228 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']);
229 229 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
230 230 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
231 231 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
232 232 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
233 233 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
234 234 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
235 235 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
236 236 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
237 237 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
238 238 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
239 239 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
240 240 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
241 241 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
242 242 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
243 243 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
244 244 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
245 245 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
246 246 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
247 247 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
248 248 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
249 249 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
250 250 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
251 251 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
252 252 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
253 253 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
254 254 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
255 255 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
256 256 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
257 257 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
258 258 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
259 259 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
260 260 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
261 261 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
262 262 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
263 263 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
264 264 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
265 265 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
266 266 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
267 267 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
268 268 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
269 269 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
270 270 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
271 271 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
272 272 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
273 273 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
274 274 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
275 275 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
276 276 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
277 277 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
278 278 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
279 279 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
280 280 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
281 281 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
282 282 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
283 283 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
284 284 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
285 285 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
286 286 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
287 287 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
288 288 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
289 289 pyroutes.register('search', '/_admin/search', []);
290 290 pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']);
291 291 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
292 292 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
293 293 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
294 294 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
295 295 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
296 296 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
297 297 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
298 298 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
299 299 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
300 300 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
301 301 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
302 302 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
303 303 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
304 304 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
305 305 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
306 306 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
307 307 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
308 308 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
309 309 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
310 310 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
311 311 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
312 312 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
313 313 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
314 314 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
315 315 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
316 316 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
317 317 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
318 318 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
319 319 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
320 320 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
321 321 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
322 322 pyroutes.register('gists_show', '/_admin/gists', []);
323 323 pyroutes.register('gists_new', '/_admin/gists/new', []);
324 324 pyroutes.register('gists_create', '/_admin/gists/create', []);
325 325 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
326 326 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
327 327 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
328 328 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
329 329 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
330 330 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
331 331 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
332 332 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
333 333 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
334 334 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
335 335 pyroutes.register('apiv2', '/_admin/api', []);
336 336 }
@@ -1,548 +1,548 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('New pull request')}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${_('New pull request')}
10 10 </%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16 <%def name="menu_bar_subnav()">
17 17 ${self.repo_menu(active='showpullrequest')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <div class="title">
23 23 ${self.repo_page_title(c.rhodecode_db_repo)}
24 24 </div>
25 25
26 26 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
27 27
28 28 ${self.breadcrumbs()}
29 29
30 30 <div class="box pr-summary">
31 31
32 32 <div class="summary-details block-left">
33 33
34 34
35 35 <div class="pr-details-title">
36 36 ${_('Pull request summary')}
37 37 </div>
38 38
39 39 <div class="form" style="padding-top: 10px">
40 40 <!-- fields -->
41 41
42 42 <div class="fields" >
43 43
44 44 <div class="field">
45 45 <div class="label">
46 46 <label for="pullrequest_title">${_('Title')}:</label>
47 47 </div>
48 48 <div class="input">
49 49 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
50 50 </div>
51 51 </div>
52 52
53 53 <div class="field">
54 54 <div class="label label-textarea">
55 55 <label for="pullrequest_desc">${_('Description')}:</label>
56 56 </div>
57 57 <div class="textarea text-area editor">
58 58 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
59 59 ${dt.markup_form('pullrequest_desc')}
60 60 </div>
61 61 </div>
62 62
63 63 <div class="field">
64 64 <div class="label label-textarea">
65 65 <label for="commit_flow">${_('Commit flow')}:</label>
66 66 </div>
67 67
68 68 ## TODO: johbo: Abusing the "content" class here to get the
69 69 ## desired effect. Should be replaced by a proper solution.
70 70
71 71 ##ORG
72 72 <div class="content">
73 73 <strong>${_('Source repository')}:</strong>
74 74 ${c.rhodecode_db_repo.description}
75 75 </div>
76 76 <div class="content">
77 77 ${h.hidden('source_repo')}
78 78 ${h.hidden('source_ref')}
79 79 </div>
80 80
81 81 ##OTHER, most Probably the PARENT OF THIS FORK
82 82 <div class="content">
83 83 ## filled with JS
84 84 <div id="target_repo_desc"></div>
85 85 </div>
86 86
87 87 <div class="content">
88 88 ${h.hidden('target_repo')}
89 89 ${h.hidden('target_ref')}
90 90 <span id="target_ref_loading" style="display: none">
91 91 ${_('Loading refs...')}
92 92 </span>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="field">
97 97 <div class="label label-textarea">
98 98 <label for="pullrequest_submit"></label>
99 99 </div>
100 100 <div class="input">
101 101 <div class="pr-submit-button">
102 102 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
103 103 </div>
104 104 <div id="pr_open_message"></div>
105 105 </div>
106 106 </div>
107 107
108 108 <div class="pr-spacing-container"></div>
109 109 </div>
110 110 </div>
111 111 </div>
112 112 <div>
113 113 ## AUTHOR
114 114 <div class="reviewers-title block-right">
115 115 <div class="pr-details-title">
116 116 ${_('Author of this pull request')}
117 117 </div>
118 118 </div>
119 119 <div class="block-right pr-details-content reviewers">
120 120 <ul class="group_members">
121 121 <li>
122 122 ${self.gravatar_with_user(c.rhodecode_user.email, 16)}
123 123 </li>
124 124 </ul>
125 125 </div>
126 126
127 127 ## REVIEW RULES
128 128 <div id="review_rules" style="display: none" class="reviewers-title block-right">
129 129 <div class="pr-details-title">
130 130 ${_('Reviewer rules')}
131 131 </div>
132 132 <div class="pr-reviewer-rules">
133 133 ## review rules will be appended here, by default reviewers logic
134 134 </div>
135 135 </div>
136 136
137 137 ## REVIEWERS
138 138 <div class="reviewers-title block-right">
139 139 <div class="pr-details-title">
140 140 ${_('Pull request reviewers')}
141 141 <span class="calculate-reviewers"> - ${_('loading...')}</span>
142 142 </div>
143 143 </div>
144 144 <div id="reviewers" class="block-right pr-details-content reviewers">
145 145 ## members goes here, filled via JS based on initial selection !
146 146 <input type="hidden" name="__start__" value="review_members:sequence">
147 147 <ul id="review_members" class="group_members"></ul>
148 148 <input type="hidden" name="__end__" value="review_members:sequence">
149 149 <div id="add_reviewer_input" class='ac'>
150 150 <div class="reviewer_ac">
151 151 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
152 152 <div id="reviewers_container"></div>
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 </div>
158 158 <div class="box">
159 159 <div>
160 160 ## overview pulled by ajax
161 161 <div id="pull_request_overview"></div>
162 162 </div>
163 163 </div>
164 164 ${h.end_form()}
165 165 </div>
166 166
167 167 <script type="text/javascript">
168 168 $(function(){
169 169 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
170 170 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
171 171 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
172 172 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
173 173
174 174 var $pullRequestForm = $('#pull_request_form');
175 175 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
176 176 var $sourceRepo = $('#source_repo', $pullRequestForm);
177 177 var $targetRepo = $('#target_repo', $pullRequestForm);
178 178 var $sourceRef = $('#source_ref', $pullRequestForm);
179 179 var $targetRef = $('#target_ref', $pullRequestForm);
180 180
181 181 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
182 182 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
183 183
184 184 var targetRepo = function() { return $targetRepo.eq(0).val() };
185 185 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
186 186
187 187 var calculateContainerWidth = function() {
188 188 var maxWidth = 0;
189 189 var repoSelect2Containers = ['#source_repo', '#target_repo'];
190 190 $.each(repoSelect2Containers, function(idx, value) {
191 191 $(value).select2('container').width('auto');
192 192 var curWidth = $(value).select2('container').width();
193 193 if (maxWidth <= curWidth) {
194 194 maxWidth = curWidth;
195 195 }
196 196 $.each(repoSelect2Containers, function(idx, value) {
197 197 $(value).select2('container').width(maxWidth + 10);
198 198 });
199 199 });
200 200 };
201 201
202 202 var initRefSelection = function(selectedRef) {
203 203 return function(element, callback) {
204 204 // translate our select2 id into a text, it's a mapping to show
205 205 // simple label when selecting by internal ID.
206 206 var id, refData;
207 207 if (selectedRef === undefined || selectedRef === null) {
208 208 id = element.val();
209 209 refData = element.val().split(':');
210 210
211 211 if (refData.length !== 3){
212 212 refData = ["", "", ""]
213 213 }
214 214 } else {
215 215 id = selectedRef;
216 216 refData = selectedRef.split(':');
217 217 }
218 218
219 219 var text = refData[1];
220 220 if (refData[0] === 'rev') {
221 221 text = text.substring(0, 12);
222 222 }
223 223
224 224 var data = {id: id, text: text};
225 225 callback(data);
226 226 };
227 227 };
228 228
229 229 var formatRefSelection = function(data, container, escapeMarkup) {
230 230 var prefix = '';
231 231 var refData = data.id.split(':');
232 232 if (refData[0] === 'branch') {
233 233 prefix = '<i class="icon-branch"></i>';
234 234 }
235 235 else if (refData[0] === 'book') {
236 236 prefix = '<i class="icon-bookmark"></i>';
237 237 }
238 238 else if (refData[0] === 'tag') {
239 239 prefix = '<i class="icon-tag"></i>';
240 240 }
241 241
242 242 var originalOption = data.element;
243 243 return prefix + escapeMarkup(data.text);
244 244 };formatSelection:
245 245
246 246 // custom code mirror
247 247 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
248 248
249 249 reviewersController = new ReviewersController();
250 250
251 251 var queryTargetRepo = function(self, query) {
252 252 // cache ALL results if query is empty
253 253 var cacheKey = query.term || '__';
254 254 var cachedData = self.cachedDataSource[cacheKey];
255 255
256 256 if (cachedData) {
257 257 query.callback({results: cachedData.results});
258 258 } else {
259 259 $.ajax({
260 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': templateContext.repo_name}),
260 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
261 261 data: {query: query.term},
262 262 dataType: 'json',
263 263 type: 'GET',
264 264 success: function(data) {
265 265 self.cachedDataSource[cacheKey] = data;
266 266 query.callback({results: data.results});
267 267 },
268 268 error: function(data, textStatus, errorThrown) {
269 269 alert(
270 270 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
271 271 }
272 272 });
273 273 }
274 274 };
275 275
276 276 var queryTargetRefs = function(initialData, query) {
277 277 var data = {results: []};
278 278 // filter initialData
279 279 $.each(initialData, function() {
280 280 var section = this.text;
281 281 var children = [];
282 282 $.each(this.children, function() {
283 283 if (query.term.length === 0 ||
284 284 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
285 285 children.push({'id': this.id, 'text': this.text})
286 286 }
287 287 });
288 288 data.results.push({'text': section, 'children': children})
289 289 });
290 290 query.callback({results: data.results});
291 291 };
292 292
293 293 var loadRepoRefDiffPreview = function() {
294 294
295 295 var url_data = {
296 296 'repo_name': targetRepo(),
297 297 'target_repo': sourceRepo(),
298 298 'source_ref': targetRef()[2],
299 299 'source_ref_type': 'rev',
300 300 'target_ref': sourceRef()[2],
301 301 'target_ref_type': 'rev',
302 302 'merge': true,
303 303 '_': Date.now() // bypass browser caching
304 304 }; // gather the source/target ref and repo here
305 305
306 306 if (sourceRef().length !== 3 || targetRef().length !== 3) {
307 307 prButtonLock(true, "${_('Please select source and target')}");
308 308 return;
309 309 }
310 310 var url = pyroutes.url('repo_compare', url_data);
311 311
312 312 // lock PR button, so we cannot send PR before it's calculated
313 313 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
314 314
315 315 if (loadRepoRefDiffPreview._currentRequest) {
316 316 loadRepoRefDiffPreview._currentRequest.abort();
317 317 }
318 318
319 319 loadRepoRefDiffPreview._currentRequest = $.get(url)
320 320 .error(function(data, textStatus, errorThrown) {
321 321 if (textStatus !== 'abort') {
322 322 alert(
323 323 "Error while processing request.\nError code {0} ({1}).".format(
324 324 data.status, data.statusText));
325 325 }
326 326
327 327 })
328 328 .done(function(data) {
329 329 loadRepoRefDiffPreview._currentRequest = null;
330 330 $('#pull_request_overview').html(data);
331 331
332 332 var commitElements = $(data).find('tr[commit_id]');
333 333
334 334 var prTitleAndDesc = getTitleAndDescription(
335 335 sourceRef()[1], commitElements, 5);
336 336
337 337 var title = prTitleAndDesc[0];
338 338 var proposedDescription = prTitleAndDesc[1];
339 339
340 340 var useGeneratedTitle = (
341 341 $('#pullrequest_title').hasClass('autogenerated-title') ||
342 342 $('#pullrequest_title').val() === "");
343 343
344 344 if (title && useGeneratedTitle) {
345 345 // use generated title if we haven't specified our own
346 346 $('#pullrequest_title').val(title);
347 347 $('#pullrequest_title').addClass('autogenerated-title');
348 348
349 349 }
350 350
351 351 var useGeneratedDescription = (
352 352 !codeMirrorInstance._userDefinedValue ||
353 353 codeMirrorInstance.getValue() === "");
354 354
355 355 if (proposedDescription && useGeneratedDescription) {
356 356 // set proposed content, if we haven't defined our own,
357 357 // or we don't have description written
358 358 codeMirrorInstance._userDefinedValue = false; // reset state
359 359 codeMirrorInstance.setValue(proposedDescription);
360 360 }
361 361
362 362 // refresh our codeMirror so events kicks in and it's change aware
363 363 codeMirrorInstance.refresh();
364 364
365 365 var msg = '';
366 366 if (commitElements.length === 1) {
367 367 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
368 368 } else {
369 369 msg = "${_ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
370 370 }
371 371
372 372 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
373 373
374 374 if (commitElements.length) {
375 375 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
376 376 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
377 377 }
378 378 else {
379 379 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
380 380 }
381 381
382 382
383 383 });
384 384 };
385 385
386 386 var Select2Box = function(element, overrides) {
387 387 var globalDefaults = {
388 388 dropdownAutoWidth: true,
389 389 containerCssClass: "drop-menu",
390 390 dropdownCssClass: "drop-menu-dropdown"
391 391 };
392 392
393 393 var initSelect2 = function(defaultOptions) {
394 394 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
395 395 element.select2(options);
396 396 };
397 397
398 398 return {
399 399 initRef: function() {
400 400 var defaultOptions = {
401 401 minimumResultsForSearch: 5,
402 402 formatSelection: formatRefSelection
403 403 };
404 404
405 405 initSelect2(defaultOptions);
406 406 },
407 407
408 408 initRepo: function(defaultValue, readOnly) {
409 409 var defaultOptions = {
410 410 initSelection : function (element, callback) {
411 411 var data = {id: defaultValue, text: defaultValue};
412 412 callback(data);
413 413 }
414 414 };
415 415
416 416 initSelect2(defaultOptions);
417 417
418 418 element.select2('val', defaultSourceRepo);
419 419 if (readOnly === true) {
420 420 element.select2('readonly', true);
421 421 }
422 422 }
423 423 };
424 424 };
425 425
426 426 var initTargetRefs = function(refsData, selectedRef) {
427 427
428 428 Select2Box($targetRef, {
429 429 placeholder: "${_('Select commit reference')}",
430 430 query: function(query) {
431 431 queryTargetRefs(refsData, query);
432 432 },
433 433 initSelection : initRefSelection(selectedRef)
434 434 }).initRef();
435 435
436 436 if (!(selectedRef === undefined)) {
437 437 $targetRef.select2('val', selectedRef);
438 438 }
439 439 };
440 440
441 441 var targetRepoChanged = function(repoData) {
442 442 // generate new DESC of target repo displayed next to select
443 443 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
444 444 $('#target_repo_desc').html(
445 445 "<strong>${_('Target repository')}</strong>: {0}. <a href=\"{1}\">Switch base, and use as source.</a>".format(repoData['description'], prLink)
446 446 );
447 447
448 448 // generate dynamic select2 for refs.
449 449 initTargetRefs(repoData['refs']['select2_refs'],
450 450 repoData['refs']['selected_ref']);
451 451
452 452 };
453 453
454 454 var sourceRefSelect2 = Select2Box($sourceRef, {
455 455 placeholder: "${_('Select commit reference')}",
456 456 query: function(query) {
457 457 var initialData = defaultSourceRepoData['refs']['select2_refs'];
458 458 queryTargetRefs(initialData, query)
459 459 },
460 460 initSelection: initRefSelection()
461 461 }
462 462 );
463 463
464 464 var sourceRepoSelect2 = Select2Box($sourceRepo, {
465 465 query: function(query) {}
466 466 });
467 467
468 468 var targetRepoSelect2 = Select2Box($targetRepo, {
469 469 cachedDataSource: {},
470 470 query: $.debounce(250, function(query) {
471 471 queryTargetRepo(this, query);
472 472 }),
473 473 formatResult: formatRepoResult
474 474 });
475 475
476 476 sourceRefSelect2.initRef();
477 477
478 478 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
479 479
480 480 targetRepoSelect2.initRepo(defaultTargetRepo, false);
481 481
482 482 $sourceRef.on('change', function(e){
483 483 loadRepoRefDiffPreview();
484 484 reviewersController.loadDefaultReviewers(
485 485 sourceRepo(), sourceRef(), targetRepo(), targetRef());
486 486 });
487 487
488 488 $targetRef.on('change', function(e){
489 489 loadRepoRefDiffPreview();
490 490 reviewersController.loadDefaultReviewers(
491 491 sourceRepo(), sourceRef(), targetRepo(), targetRef());
492 492 });
493 493
494 494 $targetRepo.on('change', function(e){
495 495 var repoName = $(this).val();
496 496 calculateContainerWidth();
497 497 $targetRef.select2('destroy');
498 498 $('#target_ref_loading').show();
499 499
500 500 $.ajax({
501 501 url: pyroutes.url('pullrequest_repo_refs',
502 502 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
503 503 data: {},
504 504 dataType: 'json',
505 505 type: 'GET',
506 506 success: function(data) {
507 507 $('#target_ref_loading').hide();
508 508 targetRepoChanged(data);
509 509 loadRepoRefDiffPreview();
510 510 },
511 511 error: function(data, textStatus, errorThrown) {
512 512 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
513 513 }
514 514 })
515 515
516 516 });
517 517
518 518 $pullRequestForm.on('submit', function(e){
519 519 // Flush changes into textarea
520 520 codeMirrorInstance.save();
521 521 prButtonLock(true, null, 'all');
522 522 });
523 523
524 524 prButtonLock(true, "${_('Please select source and target')}", 'all');
525 525
526 526 // auto-load on init, the target refs select2
527 527 calculateContainerWidth();
528 528 targetRepoChanged(defaultTargetRepoData);
529 529
530 530 $('#pullrequest_title').on('keyup', function(e){
531 531 $(this).removeClass('autogenerated-title');
532 532 });
533 533
534 534 % if c.default_source_ref:
535 535 // in case we have a pre-selected value, use it now
536 536 $sourceRef.select2('val', '${c.default_source_ref}');
537 537 // diff preview load
538 538 loadRepoRefDiffPreview();
539 539 // default reviewers
540 540 reviewersController.loadDefaultReviewers(
541 541 sourceRepo(), sourceRef(), targetRepo(), targetRef());
542 542 % endif
543 543
544 544 ReviewerAutoComplete('#user');
545 545 });
546 546 </script>
547 547
548 548 </%def>
General Comments 0
You need to be logged in to leave comments. Login now