##// END OF EJS Templates
default-reviewers: handle no common ancestor case.
marcink -
r4520:10c5ceba stable
parent child Browse files
Show More
@@ -1,82 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 from pyramid.view import view_config
24 24
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 28 from rhodecode.lib.vcs.backends.base import Reference
29 29 from rhodecode.model.db import Repository
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RepoReviewRulesView(RepoAppView):
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37 return c
38 38
39 39 @LoginRequired()
40 40 @HasRepoPermissionAnyDecorator('repository.admin')
41 41 @view_config(
42 42 route_name='repo_reviewers', request_method='GET',
43 43 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
44 44 def repo_review_rules(self):
45 45 c = self.load_default_context()
46 46 c.active = 'reviewers'
47 47
48 48 return self._get_template_context(c)
49 49
50 50 @LoginRequired()
51 51 @HasRepoPermissionAnyDecorator(
52 52 'repository.read', 'repository.write', 'repository.admin')
53 53 @view_config(
54 54 route_name='repo_default_reviewers_data', request_method='GET',
55 55 renderer='json_ext')
56 56 def repo_default_reviewers_data(self):
57 57 self.load_default_context()
58 58
59 59 request = self.request
60 60 source_repo = self.db_repo
61 61 source_repo_name = source_repo.repo_name
62 62 target_repo_name = request.GET.get('target_repo', source_repo_name)
63 63 target_repo = Repository.get_by_repo_name(target_repo_name)
64 64
65 65 current_user = request.user.get_instance()
66 66
67 67 source_commit_id = request.GET['source_ref']
68 68 source_type = request.GET['source_ref_type']
69 69 source_name = request.GET['source_ref_name']
70 70
71 71 target_commit_id = request.GET['target_ref']
72 72 target_type = request.GET['target_ref_type']
73 73 target_name = request.GET['target_ref_name']
74 74
75 try:
75 76 review_data = get_default_reviewers_data(
76 77 current_user,
77 78 source_repo,
78 79 Reference(source_type, source_name, source_commit_id),
79 80 target_repo,
80 81 Reference(target_type, target_name, target_commit_id)
81 82 )
83 except ValueError:
84 # No common ancestor
85 msg = "No Common ancestor found between target and source reference"
86 log.exception(msg)
87 return {'diff_info': {'error': msg}}
88
82 89 return review_data
@@ -1,1188 +1,1191 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = value['message'];
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 var rawMessage = elements[0]['message'];
84 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
87 87 // use reference name
88 88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 89 var refType = sourceRefType;
90 90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 91 }
92 92
93 93 return [title, desc]
94 94 };
95 95
96 96
97 97 window.ReviewersController = function () {
98 98 var self = this;
99 99 this.$loadingIndicator = $('.calculate-reviewers');
100 100 this.$reviewRulesContainer = $('#review_rules');
101 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
102 102 this.$userRule = $('.pr-user-rule-container');
103 103 this.$reviewMembers = $('#review_members');
104 104 this.$observerMembers = $('#observer_members');
105 105
106 106 this.currentRequest = null;
107 107 this.diffData = null;
108 108 this.enabledRules = [];
109 109 // sync with db.py entries
110 110 this.ROLE_REVIEWER = 'reviewer';
111 111 this.ROLE_OBSERVER = 'observer'
112 112
113 113 //dummy handler, we might register our own later
114 114 this.diffDataHandler = function (data) {};
115 115
116 116 this.defaultForbidUsers = function () {
117 117 return [
118 118 {
119 119 'username': 'default',
120 120 'user_id': templateContext.default_user.user_id
121 121 }
122 122 ];
123 123 };
124 124
125 125 // init default forbidden users
126 126 this.forbidUsers = this.defaultForbidUsers();
127 127
128 128 this.hideReviewRules = function () {
129 129 self.$reviewRulesContainer.hide();
130 130 $(self.$userRule.selector).hide();
131 131 };
132 132
133 133 this.showReviewRules = function () {
134 134 self.$reviewRulesContainer.show();
135 135 $(self.$userRule.selector).show();
136 136 };
137 137
138 138 this.addRule = function (ruleText) {
139 139 self.showReviewRules();
140 140 self.enabledRules.push(ruleText);
141 141 return '<div>- {0}</div>'.format(ruleText)
142 142 };
143 143
144 144 this.increaseCounter = function(role) {
145 145 if (role === self.ROLE_REVIEWER) {
146 146 var $elem = $('#reviewers-cnt')
147 147 var cnt = parseInt($elem.data('count') || 0)
148 148 cnt +=1
149 149 $elem.html(cnt);
150 150 $elem.data('count', cnt);
151 151 }
152 152 else if (role === self.ROLE_OBSERVER) {
153 153 var $elem = $('#observers-cnt');
154 154 var cnt = parseInt($elem.data('count') || 0)
155 155 cnt +=1
156 156 $elem.html(cnt);
157 157 $elem.data('count', cnt);
158 158 }
159 159 }
160 160
161 161 this.resetCounter = function () {
162 162 var $elem = $('#reviewers-cnt');
163 163
164 164 $elem.data('count', 0);
165 165 $elem.html(0);
166 166
167 167 var $elem = $('#observers-cnt');
168 168
169 169 $elem.data('count', 0);
170 170 $elem.html(0);
171 171 }
172 172
173 173 this.loadReviewRules = function (data) {
174 174 self.diffData = data;
175 175
176 176 // reset forbidden Users
177 177 this.forbidUsers = self.defaultForbidUsers();
178 178
179 179 // reset state of review rules
180 180 self.$rulesList.html('');
181 181
182 182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 183 // default rule, case for older repo that don't have any rules stored
184 184 self.$rulesList.append(
185 185 self.addRule(
186 186 _gettext('All reviewers must vote.'))
187 187 );
188 188 return self.forbidUsers
189 189 }
190 190
191 191 if (data.rules.voting !== undefined) {
192 192 if (data.rules.voting < 0) {
193 193 self.$rulesList.append(
194 194 self.addRule(
195 195 _gettext('All individual reviewers must vote.'))
196 196 )
197 197 } else if (data.rules.voting === 1) {
198 198 self.$rulesList.append(
199 199 self.addRule(
200 200 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
201 201 )
202 202
203 203 } else {
204 204 self.$rulesList.append(
205 205 self.addRule(
206 206 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
207 207 )
208 208 }
209 209 }
210 210
211 211 if (data.rules.voting_groups !== undefined) {
212 212 $.each(data.rules.voting_groups, function (index, rule_data) {
213 213 self.$rulesList.append(
214 214 self.addRule(rule_data.text)
215 215 )
216 216 });
217 217 }
218 218
219 219 if (data.rules.use_code_authors_for_review) {
220 220 self.$rulesList.append(
221 221 self.addRule(
222 222 _gettext('Reviewers picked from source code changes.'))
223 223 )
224 224 }
225 225
226 226 if (data.rules.forbid_adding_reviewers) {
227 227 $('#add_reviewer_input').remove();
228 228 self.$rulesList.append(
229 229 self.addRule(
230 230 _gettext('Adding new reviewers is forbidden.'))
231 231 )
232 232 }
233 233
234 234 if (data.rules.forbid_author_to_review) {
235 235 self.forbidUsers.push(data.rules_data.pr_author);
236 236 self.$rulesList.append(
237 237 self.addRule(
238 238 _gettext('Author is not allowed to be a reviewer.'))
239 239 )
240 240 }
241 241
242 242 if (data.rules.forbid_commit_author_to_review) {
243 243
244 244 if (data.rules_data.forbidden_users) {
245 245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
246 246 self.forbidUsers.push(member_data)
247 247 });
248 248 }
249 249
250 250 self.$rulesList.append(
251 251 self.addRule(
252 252 _gettext('Commit Authors are not allowed to be a reviewer.'))
253 253 )
254 254 }
255 255
256 256 // we don't have any rules set, so we inform users about it
257 257 if (self.enabledRules.length === 0) {
258 258 self.addRule(
259 259 _gettext('No review rules set.'))
260 260 }
261 261
262 262 return self.forbidUsers
263 263 };
264 264
265 265 this.emptyTables = function () {
266 266 self.emptyReviewersTable();
267 267 self.emptyObserversTable();
268 268
269 269 // Also reset counters.
270 270 self.resetCounter();
271 271 }
272 272
273 273 this.emptyReviewersTable = function (withText) {
274 274 self.$reviewMembers.empty();
275 275 if (withText !== undefined) {
276 276 self.$reviewMembers.html(withText)
277 277 }
278 278 };
279 279
280 280 this.emptyObserversTable = function (withText) {
281 281 self.$observerMembers.empty();
282 282 if (withText !== undefined) {
283 283 self.$observerMembers.html(withText)
284 284 }
285 285 }
286 286
287 287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
288 288
289 289 if (self.currentRequest) {
290 290 // make sure we cleanup old running requests before triggering this again
291 291 self.currentRequest.abort();
292 292 }
293 293
294 294 self.$loadingIndicator.show();
295 295
296 296 // reset reviewer/observe members
297 297 self.emptyTables();
298 298
299 299 prButtonLock(true, null, 'reviewers');
300 300 $('#user').hide(); // hide user autocomplete before load
301 301 $('#observer').hide(); //hide observer autocomplete before load
302 302
303 303 // lock PR button, so we cannot send PR before it's calculated
304 304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
305 305
306 306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
307 307 // don't load defaults in case we're missing some refs...
308 308 self.$loadingIndicator.hide();
309 309 return
310 310 }
311 311
312 312 var url = pyroutes.url('repo_default_reviewers_data',
313 313 {
314 314 'repo_name': templateContext.repo_name,
315 315 'source_repo': sourceRepo,
316 316 'source_ref_type': sourceRef[0],
317 317 'source_ref_name': sourceRef[1],
318 318 'source_ref': sourceRef[2],
319 319 'target_repo': targetRepo,
320 320 'target_ref': targetRef[2],
321 321 'target_ref_type': sourceRef[0],
322 322 'target_ref_name': sourceRef[1]
323 323 });
324 324
325 325 self.currentRequest = $.ajax({
326 326 url: url,
327 327 headers: {'X-PARTIAL-XHR': true},
328 328 type: 'GET',
329 329 success: function (data) {
330 330
331 331 self.currentRequest = null;
332 332
333 333 // review rules
334 334 self.loadReviewRules(data);
335 self.handleDiffData(data["diff_info"]);
335 var diffHandled = self.handleDiffData(data["diff_info"]);
336 if (diffHandled === false) {
337 return
338 }
336 339
337 340 for (var i = 0; i < data.reviewers.length; i++) {
338 341 var reviewer = data.reviewers[i];
339 342 // load reviewer rules from the repo data
340 343 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
341 344 }
342 345
343 346
344 347 self.$loadingIndicator.hide();
345 348 prButtonLock(false, null, 'reviewers');
346 349
347 350 $('#user').show(); // show user autocomplete before load
348 351 $('#observer').show(); // show observer autocomplete before load
349 352
350 353 var commitElements = data["diff_info"]['commits'];
351 354
352 355 if (commitElements.length === 0) {
353 356 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
354 357 _gettext('There are no commits to merge.'));
355 358 prButtonLock(true, noCommitsMsg, 'all');
356 359
357 360 } else {
358 361 // un-lock PR button, so we cannot send PR before it's calculated
359 362 prButtonLock(false, null, 'compare');
360 363 }
361 364
362 365 },
363 366 error: function (jqXHR, textStatus, errorThrown) {
364 367 var prefix = "Loading diff and reviewers/observers failed\n"
365 368 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
366 369 ajaxErrorSwal(message);
367 370 }
368 371 });
369 372
370 373 };
371 374
372 375 // check those, refactor
373 376 this.removeMember = function (reviewer_id, mark_delete) {
374 377 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
375 378
376 379 if (typeof (mark_delete) === undefined) {
377 380 mark_delete = false;
378 381 }
379 382
380 383 if (mark_delete === true) {
381 384 if (reviewer) {
382 385 // now delete the input
383 386 $('#reviewer_{0} input'.format(reviewer_id)).remove();
384 387 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
385 388 // mark as to-delete
386 389 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
387 390 obj.addClass('to-delete');
388 391 obj.css({"text-decoration": "line-through", "opacity": 0.5});
389 392 }
390 393 } else {
391 394 $('#reviewer_{0}'.format(reviewer_id)).remove();
392 395 }
393 396 };
394 397
395 398 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
396 399
397 400 var id = reviewer_obj.user_id;
398 401 var username = reviewer_obj.username;
399 402
400 403 reasons = reasons || [];
401 404 mandatory = mandatory || false;
402 405 role = role || self.ROLE_REVIEWER
403 406
404 407 // register current set IDS to check if we don't have this ID already in
405 408 // and prevent duplicates
406 409 var currentIds = [];
407 410
408 411 $.each($('.reviewer_entry'), function (index, value) {
409 412 currentIds.push($(value).data('reviewerUserId'))
410 413 })
411 414
412 415 var userAllowedReview = function (userId) {
413 416 var allowed = true;
414 417 $.each(self.forbidUsers, function (index, member_data) {
415 418 if (parseInt(userId) === member_data['user_id']) {
416 419 allowed = false;
417 420 return false // breaks the loop
418 421 }
419 422 });
420 423 return allowed
421 424 };
422 425
423 426 var userAllowed = userAllowedReview(id);
424 427
425 428 if (!userAllowed) {
426 429 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
427 430 } else {
428 431 // only add if it's not there
429 432 var alreadyReviewer = currentIds.indexOf(id) != -1;
430 433
431 434 if (alreadyReviewer) {
432 435 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
433 436 } else {
434 437
435 438 var reviewerEntry = renderTemplate('reviewMemberEntry', {
436 439 'member': reviewer_obj,
437 440 'mandatory': mandatory,
438 441 'role': role,
439 442 'reasons': reasons,
440 443 'allowed_to_update': true,
441 444 'review_status': 'not_reviewed',
442 445 'review_status_label': _gettext('Not Reviewed'),
443 446 'user_group': reviewer_obj.user_group,
444 447 'create': true,
445 448 'rule_show': true,
446 449 })
447 450
448 451 if (role === self.ROLE_REVIEWER) {
449 452 $(self.$reviewMembers.selector).append(reviewerEntry);
450 453 self.increaseCounter(self.ROLE_REVIEWER);
451 454 $('#reviewer-empty-msg').remove()
452 455 }
453 456 else if (role === self.ROLE_OBSERVER) {
454 457 $(self.$observerMembers.selector).append(reviewerEntry);
455 458 self.increaseCounter(self.ROLE_OBSERVER);
456 459 $('#observer-empty-msg').remove();
457 460 }
458 461
459 462 tooltipActivate();
460 463 }
461 464 }
462 465
463 466 };
464 467
465 468 this.updateReviewers = function (repo_name, pull_request_id, role) {
466 469 if (role === 'reviewer') {
467 470 var postData = $('#reviewers input').serialize();
468 471 _updatePullRequest(repo_name, pull_request_id, postData);
469 472 } else if (role === 'observer') {
470 473 var postData = $('#observers input').serialize();
471 474 _updatePullRequest(repo_name, pull_request_id, postData);
472 475 }
473 476 };
474 477
475 478 this.handleDiffData = function (data) {
476 self.diffDataHandler(data)
479 return self.diffDataHandler(data)
477 480 }
478 481 };
479 482
480 483
481 484 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
482 485 var url = pyroutes.url(
483 486 'pullrequest_update',
484 487 {"repo_name": repo_name, "pull_request_id": pull_request_id});
485 488 if (typeof postData === 'string' ) {
486 489 postData += '&csrf_token=' + CSRF_TOKEN;
487 490 } else {
488 491 postData.csrf_token = CSRF_TOKEN;
489 492 }
490 493
491 494 var success = function(o) {
492 495 var redirectUrl = o['redirect_url'];
493 496 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
494 497 window.location = redirectUrl;
495 498 } else {
496 499 window.location.reload();
497 500 }
498 501 };
499 502
500 503 ajaxPOST(url, postData, success);
501 504 };
502 505
503 506 /**
504 507 * PULL REQUEST update commits
505 508 */
506 509 var updateCommits = function(repo_name, pull_request_id, force) {
507 510 var postData = {
508 511 'update_commits': true
509 512 };
510 513 if (force !== undefined && force === true) {
511 514 postData['force_refresh'] = true
512 515 }
513 516 _updatePullRequest(repo_name, pull_request_id, postData);
514 517 };
515 518
516 519
517 520 /**
518 521 * PULL REQUEST edit info
519 522 */
520 523 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
521 524 var url = pyroutes.url(
522 525 'pullrequest_update',
523 526 {"repo_name": repo_name, "pull_request_id": pull_request_id});
524 527
525 528 var postData = {
526 529 'title': title,
527 530 'description': description,
528 531 'description_renderer': renderer,
529 532 'edit_pull_request': true,
530 533 'csrf_token': CSRF_TOKEN
531 534 };
532 535 var success = function(o) {
533 536 window.location.reload();
534 537 };
535 538 ajaxPOST(url, postData, success);
536 539 };
537 540
538 541
539 542 /**
540 543 * autocomplete handler for reviewers/observers
541 544 */
542 545 var autoCompleteHandler = function (inputId, controller, role) {
543 546
544 547 return function (element, data) {
545 548 var mandatory = false;
546 549 var reasons = [_gettext('added manually by "{0}"').format(
547 550 templateContext.rhodecode_user.username)];
548 551
549 552 // add whole user groups
550 553 if (data.value_type == 'user_group') {
551 554 reasons.push(_gettext('member of "{0}"').format(data.value_display));
552 555
553 556 $.each(data.members, function (index, member_data) {
554 557 var reviewer = member_data;
555 558 reviewer['user_id'] = member_data['id'];
556 559 reviewer['gravatar_link'] = member_data['icon_link'];
557 560 reviewer['user_link'] = member_data['profile_link'];
558 561 reviewer['rules'] = [];
559 562 controller.addMember(reviewer, reasons, mandatory, role);
560 563 })
561 564 }
562 565 // add single user
563 566 else {
564 567 var reviewer = data;
565 568 reviewer['user_id'] = data['id'];
566 569 reviewer['gravatar_link'] = data['icon_link'];
567 570 reviewer['user_link'] = data['profile_link'];
568 571 reviewer['rules'] = [];
569 572 controller.addMember(reviewer, reasons, mandatory, role);
570 573 }
571 574
572 575 $(inputId).val('');
573 576 }
574 577 }
575 578
576 579 /**
577 580 * Reviewer autocomplete
578 581 */
579 582 var ReviewerAutoComplete = function (inputId, controller) {
580 583 var self = this;
581 584 self.controller = controller;
582 585 self.inputId = inputId;
583 586 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
584 587
585 588 $(inputId).autocomplete({
586 589 serviceUrl: pyroutes.url('user_autocomplete_data'),
587 590 minChars: 2,
588 591 maxHeight: 400,
589 592 deferRequestBy: 300, //miliseconds
590 593 showNoSuggestionNotice: true,
591 594 tabDisabled: true,
592 595 autoSelectFirst: true,
593 596 params: {
594 597 user_id: templateContext.rhodecode_user.user_id,
595 598 user_groups: true,
596 599 user_groups_expand: true,
597 600 skip_default_user: true
598 601 },
599 602 formatResult: autocompleteFormatResult,
600 603 lookupFilter: autocompleteFilterResult,
601 604 onSelect: handler
602 605 });
603 606 };
604 607
605 608 /**
606 609 * Observers autocomplete
607 610 */
608 611 var ObserverAutoComplete = function(inputId, controller) {
609 612 var self = this;
610 613 self.controller = controller;
611 614 self.inputId = inputId;
612 615 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
613 616
614 617 $(inputId).autocomplete({
615 618 serviceUrl: pyroutes.url('user_autocomplete_data'),
616 619 minChars: 2,
617 620 maxHeight: 400,
618 621 deferRequestBy: 300, //miliseconds
619 622 showNoSuggestionNotice: true,
620 623 tabDisabled: true,
621 624 autoSelectFirst: true,
622 625 params: {
623 626 user_id: templateContext.rhodecode_user.user_id,
624 627 user_groups: true,
625 628 user_groups_expand: true,
626 629 skip_default_user: true
627 630 },
628 631 formatResult: autocompleteFormatResult,
629 632 lookupFilter: autocompleteFilterResult,
630 633 onSelect: handler
631 634 });
632 635 }
633 636
634 637
635 638 window.VersionController = function () {
636 639 var self = this;
637 640 this.$verSource = $('input[name=ver_source]');
638 641 this.$verTarget = $('input[name=ver_target]');
639 642 this.$showVersionDiff = $('#show-version-diff');
640 643
641 644 this.adjustRadioSelectors = function (curNode) {
642 645 var getVal = function (item) {
643 646 if (item === 'latest') {
644 647 return Number.MAX_SAFE_INTEGER
645 648 }
646 649 else {
647 650 return parseInt(item)
648 651 }
649 652 };
650 653
651 654 var curVal = getVal($(curNode).val());
652 655 var cleared = false;
653 656
654 657 $.each(self.$verSource, function (index, value) {
655 658 var elVal = getVal($(value).val());
656 659
657 660 if (elVal > curVal) {
658 661 if ($(value).is(':checked')) {
659 662 cleared = true;
660 663 }
661 664 $(value).attr('disabled', 'disabled');
662 665 $(value).removeAttr('checked');
663 666 $(value).css({'opacity': 0.1});
664 667 }
665 668 else {
666 669 $(value).css({'opacity': 1});
667 670 $(value).removeAttr('disabled');
668 671 }
669 672 });
670 673
671 674 if (cleared) {
672 675 // if we unchecked an active, set the next one to same loc.
673 676 $(this.$verSource).filter('[value={0}]'.format(
674 677 curVal)).attr('checked', 'checked');
675 678 }
676 679
677 680 self.setLockAction(false,
678 681 $(curNode).data('verPos'),
679 682 $(this.$verSource).filter(':checked').data('verPos')
680 683 );
681 684 };
682 685
683 686
684 687 this.attachVersionListener = function () {
685 688 self.$verTarget.change(function (e) {
686 689 self.adjustRadioSelectors(this)
687 690 });
688 691 self.$verSource.change(function (e) {
689 692 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
690 693 });
691 694 };
692 695
693 696 this.init = function () {
694 697
695 698 var curNode = self.$verTarget.filter(':checked');
696 699 self.adjustRadioSelectors(curNode);
697 700 self.setLockAction(true);
698 701 self.attachVersionListener();
699 702
700 703 };
701 704
702 705 this.setLockAction = function (state, selectedVersion, otherVersion) {
703 706 var $showVersionDiff = this.$showVersionDiff;
704 707
705 708 if (state) {
706 709 $showVersionDiff.attr('disabled', 'disabled');
707 710 $showVersionDiff.addClass('disabled');
708 711 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
709 712 }
710 713 else {
711 714 $showVersionDiff.removeAttr('disabled');
712 715 $showVersionDiff.removeClass('disabled');
713 716
714 717 if (selectedVersion == otherVersion) {
715 718 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
716 719 } else {
717 720 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
718 721 }
719 722 }
720 723
721 724 };
722 725
723 726 this.showVersionDiff = function () {
724 727 var target = self.$verTarget.filter(':checked');
725 728 var source = self.$verSource.filter(':checked');
726 729
727 730 if (target.val() && source.val()) {
728 731 var params = {
729 732 'pull_request_id': templateContext.pull_request_data.pull_request_id,
730 733 'repo_name': templateContext.repo_name,
731 734 'version': target.val(),
732 735 'from_version': source.val()
733 736 };
734 737 window.location = pyroutes.url('pullrequest_show', params)
735 738 }
736 739
737 740 return false;
738 741 };
739 742
740 743 this.toggleVersionView = function (elem) {
741 744
742 745 if (this.$showVersionDiff.is(':visible')) {
743 746 $('.version-pr').hide();
744 747 this.$showVersionDiff.hide();
745 748 $(elem).html($(elem).data('toggleOn'))
746 749 } else {
747 750 $('.version-pr').show();
748 751 this.$showVersionDiff.show();
749 752 $(elem).html($(elem).data('toggleOff'))
750 753 }
751 754
752 755 return false
753 756 };
754 757
755 758 };
756 759
757 760
758 761 window.UpdatePrController = function () {
759 762 var self = this;
760 763 this.$updateCommits = $('#update_commits');
761 764 this.$updateCommitsSwitcher = $('#update_commits_switcher');
762 765
763 766 this.lockUpdateButton = function (label) {
764 767 self.$updateCommits.attr('disabled', 'disabled');
765 768 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
766 769
767 770 self.$updateCommits.addClass('disabled');
768 771 self.$updateCommitsSwitcher.addClass('disabled');
769 772
770 773 self.$updateCommits.removeClass('btn-primary');
771 774 self.$updateCommitsSwitcher.removeClass('btn-primary');
772 775
773 776 self.$updateCommits.text(_gettext(label));
774 777 };
775 778
776 779 this.isUpdateLocked = function () {
777 780 return self.$updateCommits.attr('disabled') !== undefined;
778 781 };
779 782
780 783 this.updateCommits = function (curNode) {
781 784 if (self.isUpdateLocked()) {
782 785 return
783 786 }
784 787 self.lockUpdateButton(_gettext('Updating...'));
785 788 updateCommits(
786 789 templateContext.repo_name,
787 790 templateContext.pull_request_data.pull_request_id);
788 791 };
789 792
790 793 this.forceUpdateCommits = function () {
791 794 if (self.isUpdateLocked()) {
792 795 return
793 796 }
794 797 self.lockUpdateButton(_gettext('Force updating...'));
795 798 var force = true;
796 799 updateCommits(
797 800 templateContext.repo_name,
798 801 templateContext.pull_request_data.pull_request_id, force);
799 802 };
800 803 };
801 804
802 805
803 806 /**
804 807 * Reviewer display panel
805 808 */
806 809 window.ReviewersPanel = {
807 810 editButton: null,
808 811 closeButton: null,
809 812 addButton: null,
810 813 removeButtons: null,
811 814 reviewRules: null,
812 815 setReviewers: null,
813 816 controller: null,
814 817
815 818 setSelectors: function () {
816 819 var self = this;
817 820 self.editButton = $('#open_edit_reviewers');
818 821 self.closeButton =$('#close_edit_reviewers');
819 822 self.addButton = $('#add_reviewer');
820 823 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
821 824 },
822 825
823 826 init: function (controller, reviewRules, setReviewers) {
824 827 var self = this;
825 828 self.setSelectors();
826 829
827 830 self.controller = controller;
828 831 self.reviewRules = reviewRules;
829 832 self.setReviewers = setReviewers;
830 833
831 834 self.editButton.on('click', function (e) {
832 835 self.edit();
833 836 });
834 837 self.closeButton.on('click', function (e) {
835 838 self.close();
836 839 self.renderReviewers();
837 840 });
838 841
839 842 self.renderReviewers();
840 843
841 844 },
842 845
843 846 renderReviewers: function () {
844 847 var self = this;
845 848
846 849 if (self.setReviewers.reviewers === undefined) {
847 850 return
848 851 }
849 852 if (self.setReviewers.reviewers.length === 0) {
850 853 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
851 854 return
852 855 }
853 856
854 857 self.controller.emptyReviewersTable();
855 858
856 859 $.each(self.setReviewers.reviewers, function (key, val) {
857 860
858 861 var member = val;
859 862 if (member.role === self.controller.ROLE_REVIEWER) {
860 863 var entry = renderTemplate('reviewMemberEntry', {
861 864 'member': member,
862 865 'mandatory': member.mandatory,
863 866 'role': member.role,
864 867 'reasons': member.reasons,
865 868 'allowed_to_update': member.allowed_to_update,
866 869 'review_status': member.review_status,
867 870 'review_status_label': member.review_status_label,
868 871 'user_group': member.user_group,
869 872 'create': false
870 873 });
871 874
872 875 $(self.controller.$reviewMembers.selector).append(entry)
873 876 }
874 877 });
875 878
876 879 tooltipActivate();
877 880 },
878 881
879 882 edit: function (event) {
880 883 var self = this;
881 884 self.editButton.hide();
882 885 self.closeButton.show();
883 886 self.addButton.show();
884 887 $(self.removeButtons.selector).css('visibility', 'visible');
885 888 // review rules
886 889 self.controller.loadReviewRules(this.reviewRules);
887 890 },
888 891
889 892 close: function (event) {
890 893 var self = this;
891 894 this.editButton.show();
892 895 this.closeButton.hide();
893 896 this.addButton.hide();
894 897 $(this.removeButtons.selector).css('visibility', 'hidden');
895 898 // hide review rules
896 899 self.controller.hideReviewRules();
897 900 }
898 901 };
899 902
900 903 /**
901 904 * Reviewer display panel
902 905 */
903 906 window.ObserversPanel = {
904 907 editButton: null,
905 908 closeButton: null,
906 909 addButton: null,
907 910 removeButtons: null,
908 911 reviewRules: null,
909 912 setReviewers: null,
910 913 controller: null,
911 914
912 915 setSelectors: function () {
913 916 var self = this;
914 917 self.editButton = $('#open_edit_observers');
915 918 self.closeButton =$('#close_edit_observers');
916 919 self.addButton = $('#add_observer');
917 920 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
918 921 },
919 922
920 923 init: function (controller, reviewRules, setReviewers) {
921 924 var self = this;
922 925 self.setSelectors();
923 926
924 927 self.controller = controller;
925 928 self.reviewRules = reviewRules;
926 929 self.setReviewers = setReviewers;
927 930
928 931 self.editButton.on('click', function (e) {
929 932 self.edit();
930 933 });
931 934 self.closeButton.on('click', function (e) {
932 935 self.close();
933 936 self.renderObservers();
934 937 });
935 938
936 939 self.renderObservers();
937 940
938 941 },
939 942
940 943 renderObservers: function () {
941 944 var self = this;
942 945 if (self.setReviewers.observers === undefined) {
943 946 return
944 947 }
945 948 if (self.setReviewers.observers.length === 0) {
946 949 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
947 950 return
948 951 }
949 952
950 953 self.controller.emptyObserversTable();
951 954
952 955 $.each(self.setReviewers.observers, function (key, val) {
953 956 var member = val;
954 957 if (member.role === self.controller.ROLE_OBSERVER) {
955 958 var entry = renderTemplate('reviewMemberEntry', {
956 959 'member': member,
957 960 'mandatory': member.mandatory,
958 961 'role': member.role,
959 962 'reasons': member.reasons,
960 963 'allowed_to_update': member.allowed_to_update,
961 964 'review_status': member.review_status,
962 965 'review_status_label': member.review_status_label,
963 966 'user_group': member.user_group,
964 967 'create': false
965 968 });
966 969
967 970 $(self.controller.$observerMembers.selector).append(entry)
968 971 }
969 972 });
970 973
971 974 tooltipActivate();
972 975 },
973 976
974 977 edit: function (event) {
975 978 this.editButton.hide();
976 979 this.closeButton.show();
977 980 this.addButton.show();
978 981 $(this.removeButtons.selector).css('visibility', 'visible');
979 982 },
980 983
981 984 close: function (event) {
982 985 this.editButton.show();
983 986 this.closeButton.hide();
984 987 this.addButton.hide();
985 988 $(this.removeButtons.selector).css('visibility', 'hidden');
986 989 }
987 990
988 991 };
989 992
990 993 window.PRDetails = {
991 994 editButton: null,
992 995 closeButton: null,
993 996 deleteButton: null,
994 997 viewFields: null,
995 998 editFields: null,
996 999
997 1000 setSelectors: function () {
998 1001 var self = this;
999 1002 self.editButton = $('#open_edit_pullrequest')
1000 1003 self.closeButton = $('#close_edit_pullrequest')
1001 1004 self.deleteButton = $('#delete_pullrequest')
1002 1005 self.viewFields = $('#pr-desc, #pr-title')
1003 1006 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
1004 1007 },
1005 1008
1006 1009 init: function () {
1007 1010 var self = this;
1008 1011 self.setSelectors();
1009 1012 self.editButton.on('click', function (e) {
1010 1013 self.edit();
1011 1014 });
1012 1015 self.closeButton.on('click', function (e) {
1013 1016 self.view();
1014 1017 });
1015 1018 },
1016 1019
1017 1020 edit: function (event) {
1018 1021 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1019 1022 this.viewFields.hide();
1020 1023 this.editButton.hide();
1021 1024 this.deleteButton.hide();
1022 1025 this.closeButton.show();
1023 1026 this.editFields.show();
1024 1027 cmInstance.refresh();
1025 1028 },
1026 1029
1027 1030 view: function (event) {
1028 1031 this.editButton.show();
1029 1032 this.deleteButton.show();
1030 1033 this.editFields.hide();
1031 1034 this.closeButton.hide();
1032 1035 this.viewFields.show();
1033 1036 }
1034 1037 };
1035 1038
1036 1039 /**
1037 1040 * OnLine presence using channelstream
1038 1041 */
1039 1042 window.ReviewerPresenceController = function (channel) {
1040 1043 var self = this;
1041 1044 this.channel = channel;
1042 1045 this.users = {};
1043 1046
1044 1047 this.storeUsers = function (users) {
1045 1048 self.users = {}
1046 1049 $.each(users, function (index, value) {
1047 1050 var userId = value.state.id;
1048 1051 self.users[userId] = value.state;
1049 1052 })
1050 1053 }
1051 1054
1052 1055 this.render = function () {
1053 1056 $.each($('.reviewer_entry'), function (index, value) {
1054 1057 var userData = $(value).data();
1055 1058 if (self.users[userData.reviewerUserId] !== undefined) {
1056 1059 $(value).find('.presence-state').show();
1057 1060 } else {
1058 1061 $(value).find('.presence-state').hide();
1059 1062 }
1060 1063 })
1061 1064 };
1062 1065
1063 1066 this.handlePresence = function (data) {
1064 1067 if (data.type == 'presence' && data.channel === self.channel) {
1065 1068 this.storeUsers(data.users);
1066 1069 this.render()
1067 1070 }
1068 1071 };
1069 1072
1070 1073 this.handleChannelUpdate = function (data) {
1071 1074 if (data.channel === this.channel) {
1072 1075 this.storeUsers(data.state.users);
1073 1076 this.render()
1074 1077 }
1075 1078
1076 1079 };
1077 1080
1078 1081 /* subscribe to the current presence */
1079 1082 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1080 1083 /* subscribe to updates e.g connect/disconnect */
1081 1084 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1082 1085
1083 1086 };
1084 1087
1085 1088 window.refreshComments = function (version) {
1086 1089 version = version || templateContext.pull_request_data.pull_request_version || '';
1087 1090
1088 1091 // Pull request case
1089 1092 if (templateContext.pull_request_data.pull_request_id !== null) {
1090 1093 var params = {
1091 1094 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1092 1095 'repo_name': templateContext.repo_name,
1093 1096 'version': version,
1094 1097 };
1095 1098 var loadUrl = pyroutes.url('pullrequest_comments', params);
1096 1099 } // commit case
1097 1100 else {
1098 1101 return
1099 1102 }
1100 1103
1101 1104 var currentIDs = []
1102 1105 $.each($('.comment'), function (idx, element) {
1103 1106 currentIDs.push($(element).data('commentId'));
1104 1107 });
1105 1108 var data = {"comments": currentIDs};
1106 1109
1107 1110 var $targetElem = $('.comments-content-table');
1108 1111 $targetElem.css('opacity', 0.3);
1109 1112
1110 1113 var success = function (data) {
1111 1114 var $counterElem = $('#comments-count');
1112 1115 var newCount = $(data).data('counter');
1113 1116 if (newCount !== undefined) {
1114 1117 var callback = function () {
1115 1118 $counterElem.animate({'opacity': 1.00}, 200)
1116 1119 $counterElem.html(newCount);
1117 1120 };
1118 1121 $counterElem.animate({'opacity': 0.15}, 200, callback);
1119 1122 }
1120 1123
1121 1124 $targetElem.css('opacity', 1);
1122 1125 $targetElem.html(data);
1123 1126 tooltipActivate();
1124 1127 }
1125 1128
1126 1129 ajaxPOST(loadUrl, data, success, null, {})
1127 1130
1128 1131 }
1129 1132
1130 1133 window.refreshTODOs = function (version) {
1131 1134 version = version || templateContext.pull_request_data.pull_request_version || '';
1132 1135 // Pull request case
1133 1136 if (templateContext.pull_request_data.pull_request_id !== null) {
1134 1137 var params = {
1135 1138 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1136 1139 'repo_name': templateContext.repo_name,
1137 1140 'version': version,
1138 1141 };
1139 1142 var loadUrl = pyroutes.url('pullrequest_comments', params);
1140 1143 } // commit case
1141 1144 else {
1142 1145 return
1143 1146 }
1144 1147
1145 1148 var currentIDs = []
1146 1149 $.each($('.comment'), function (idx, element) {
1147 1150 currentIDs.push($(element).data('commentId'));
1148 1151 });
1149 1152
1150 1153 var data = {"comments": currentIDs};
1151 1154 var $targetElem = $('.todos-content-table');
1152 1155 $targetElem.css('opacity', 0.3);
1153 1156
1154 1157 var success = function (data) {
1155 1158 var $counterElem = $('#todos-count')
1156 1159 var newCount = $(data).data('counter');
1157 1160 if (newCount !== undefined) {
1158 1161 var callback = function () {
1159 1162 $counterElem.animate({'opacity': 1.00}, 200)
1160 1163 $counterElem.html(newCount);
1161 1164 };
1162 1165 $counterElem.animate({'opacity': 0.15}, 200, callback);
1163 1166 }
1164 1167
1165 1168 $targetElem.css('opacity', 1);
1166 1169 $targetElem.html(data);
1167 1170 tooltipActivate();
1168 1171 }
1169 1172
1170 1173 ajaxPOST(loadUrl, data, success, null, {})
1171 1174
1172 1175 }
1173 1176
1174 1177 window.refreshAllComments = function (version) {
1175 1178 version = version || templateContext.pull_request_data.pull_request_version || '';
1176 1179
1177 1180 refreshComments(version);
1178 1181 refreshTODOs(version);
1179 1182 };
1180 1183
1181 1184 window.sidebarComment = function (commentId) {
1182 1185 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1183 1186 if (!jsonData) {
1184 1187 return 'Failed to load comment {0}'.format(commentId)
1185 1188 }
1186 1189 var funcData = JSON.parse(atob(jsonData));
1187 1190 return renderTemplate('sideBarCommentHovercard', funcData)
1188 1191 };
@@ -1,642 +1,650 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()"></%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="menu_bar_subnav()">
15 15 ${self.repo_menu(active='showpullrequest')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 <div class="box">
20 20 ${h.secure_form(h.route_path('pullrequest_create', repo_name=c.repo_name, _query=request.GET.mixed()), id='pull_request_form', request=request)}
21 21
22 22 <div class="box">
23 23
24 24 <div class="summary-details block-left">
25 25
26 26 <div class="form" style="padding-top: 10px">
27 27
28 28 <div class="fields" >
29 29
30 30 ## COMMIT FLOW
31 31 <div class="field">
32 32 <div class="label label-textarea">
33 33 <label for="commit_flow">${_('Commit flow')}:</label>
34 34 </div>
35 35
36 36 <div class="content">
37 37 <div class="flex-container">
38 38 <div style="width: 45%;">
39 39 <div class="panel panel-default source-panel">
40 40 <div class="panel-heading">
41 41 <h3 class="panel-title">${_('Source repository')}</h3>
42 42 </div>
43 43 <div class="panel-body">
44 44 <div style="display:none">${c.rhodecode_db_repo.description}</div>
45 45 ${h.hidden('source_repo')}
46 46 ${h.hidden('source_ref')}
47 47
48 48 <div id="pr_open_message"></div>
49 49 </div>
50 50 </div>
51 51 </div>
52 52
53 53 <div style="width: 90px; text-align: center; padding-top: 30px">
54 54 <div>
55 55 <i class="icon-right" style="font-size: 2.2em"></i>
56 56 </div>
57 57 <div style="position: relative; top: 10px">
58 58 <span class="tag tag">
59 59 <span id="switch_base"></span>
60 60 </span>
61 61 </div>
62 62
63 63 </div>
64 64
65 65 <div style="width: 45%;">
66 66
67 67 <div class="panel panel-default target-panel">
68 68 <div class="panel-heading">
69 69 <h3 class="panel-title">${_('Target repository')}</h3>
70 70 </div>
71 71 <div class="panel-body">
72 72 <div style="display:none" id="target_repo_desc"></div>
73 73 ${h.hidden('target_repo')}
74 74 ${h.hidden('target_ref')}
75 75 <span id="target_ref_loading" style="display: none">
76 76 ${_('Loading refs...')}
77 77 </span>
78 78 </div>
79 79 </div>
80 80
81 81 </div>
82 82 </div>
83 83
84 84 </div>
85 85
86 86 </div>
87 87
88 88 ## TITLE
89 89 <div class="field">
90 90 <div class="label">
91 91 <label for="pullrequest_title">${_('Title')}:</label>
92 92 </div>
93 93 <div class="input">
94 94 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
95 95 </div>
96 96 <p class="help-block">
97 97 Start the title with WIP: to prevent accidental merge of Work In Progress pull request before it's ready.
98 98 </p>
99 99 </div>
100 100
101 101 ## DESC
102 102 <div class="field">
103 103 <div class="label label-textarea">
104 104 <label for="pullrequest_desc">${_('Description')}:</label>
105 105 </div>
106 106 <div class="textarea text-area">
107 107 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
108 108 ${dt.markup_form('pullrequest_desc')}
109 109 </div>
110 110 </div>
111 111
112 112 ## REVIEWERS
113 113 <div class="field">
114 114 <div class="label label-textarea">
115 115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 116 </div>
117 117 <div class="content">
118 118 ## REVIEW RULES
119 119 <div id="review_rules" style="display: none" class="reviewers-title">
120 120 <div class="pr-details-title">
121 121 ${_('Reviewer rules')}
122 122 </div>
123 123 <div class="pr-reviewer-rules">
124 124 ## review rules will be appended here, by default reviewers logic
125 125 </div>
126 126 </div>
127 127
128 128 ## REVIEWERS / OBSERVERS
129 129 <div class="reviewers-title">
130 130
131 131 <ul class="nav-links clearfix">
132 132
133 133 ## TAB1 MANDATORY REVIEWERS
134 134 <li class="active">
135 135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 136 Reviewers
137 137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 138 </a>
139 139 </li>
140 140
141 141 ## TAB2 OBSERVERS
142 142 <li class="">
143 143 <a id="observers-btn" href="#showObservers" tabindex="-1">
144 144 Observers
145 145 <span id="observers-cnt" data-count="0" class="menulink-counter">0</span>
146 146 </a>
147 147 </li>
148 148
149 149 </ul>
150 150
151 151 ## TAB1 MANDATORY REVIEWERS
152 152 <div id="reviewers-container">
153 153 <span class="calculate-reviewers">
154 154 <h4>${_('loading...')}</h4>
155 155 </span>
156 156
157 157 <div id="reviewers" class="pr-details-content reviewers">
158 158 ## members goes here, filled via JS based on initial selection !
159 159 <input type="hidden" name="__start__" value="review_members:sequence">
160 160 <table id="review_members" class="group_members">
161 161 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
162 162 </table>
163 163 <input type="hidden" name="__end__" value="review_members:sequence">
164 164
165 165 <div id="add_reviewer_input" class='ac'>
166 166 <div class="reviewer_ac">
167 167 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
168 168 <div id="reviewers_container"></div>
169 169 </div>
170 170 </div>
171 171
172 172 </div>
173 173 </div>
174 174
175 175 ## TAB2 OBSERVERS
176 176 <div id="observers-container" style="display: none">
177 177 <span class="calculate-reviewers">
178 178 <h4>${_('loading...')}</h4>
179 179 </span>
180 180 % if c.rhodecode_edition_id == 'EE':
181 181 <div id="observers" class="pr-details-content observers">
182 182 ## members goes here, filled via JS based on initial selection !
183 183 <input type="hidden" name="__start__" value="observer_members:sequence">
184 184 <table id="observer_members" class="group_members">
185 185 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
186 186 </table>
187 187 <input type="hidden" name="__end__" value="observer_members:sequence">
188 188
189 189 <div id="add_observer_input" class='ac'>
190 190 <div class="observer_ac">
191 191 ${h.text('observer', class_='ac-input', placeholder=_('Add observer or observer group'))}
192 192 <div id="observers_container"></div>
193 193 </div>
194 194 </div>
195 195 </div>
196 196 % else:
197 197 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
198 198 <p>
199 199 Pull request observers allows adding users who don't need to leave mandatory votes, but need to be aware about certain changes.
200 200 </p>
201 201 % endif
202 202 </div>
203 203
204 204 </div>
205 205
206 206 </div>
207 207 </div>
208 208
209 209 ## SUBMIT
210 210 <div class="field">
211 211 <div class="label label-textarea">
212 212 <label for="pullrequest_submit"></label>
213 213 </div>
214 214 <div class="input">
215 215 <div class="pr-submit-button">
216 216 <input id="pr_submit" class="btn" name="save" type="submit" value="${_('Submit Pull Request')}">
217 217 </div>
218 218 </div>
219 219 </div>
220 220 </div>
221 221 </div>
222 222 </div>
223 223
224 224 </div>
225 225
226 226 ${h.end_form()}
227 227 </div>
228 228
229 229 <script type="text/javascript">
230 230 $(function(){
231 231 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
232 232 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
233 233 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
234 234 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
235 235
236 236 var $pullRequestForm = $('#pull_request_form');
237 237 var $pullRequestSubmit = $('#pr_submit', $pullRequestForm);
238 238 var $sourceRepo = $('#source_repo', $pullRequestForm);
239 239 var $targetRepo = $('#target_repo', $pullRequestForm);
240 240 var $sourceRef = $('#source_ref', $pullRequestForm);
241 241 var $targetRef = $('#target_ref', $pullRequestForm);
242 242
243 243 var sourceRepo = function() { return $sourceRepo.eq(0).val() };
244 244 var sourceRef = function() { return $sourceRef.eq(0).val().split(':') };
245 245
246 246 var targetRepo = function() { return $targetRepo.eq(0).val() };
247 247 var targetRef = function() { return $targetRef.eq(0).val().split(':') };
248 248
249 249 var calculateContainerWidth = function() {
250 250 var maxWidth = 0;
251 251 var repoSelect2Containers = ['#source_repo', '#target_repo'];
252 252 $.each(repoSelect2Containers, function(idx, value) {
253 253 $(value).select2('container').width('auto');
254 254 var curWidth = $(value).select2('container').width();
255 255 if (maxWidth <= curWidth) {
256 256 maxWidth = curWidth;
257 257 }
258 258 $.each(repoSelect2Containers, function(idx, value) {
259 259 $(value).select2('container').width(maxWidth + 10);
260 260 });
261 261 });
262 262 };
263 263
264 264 var initRefSelection = function(selectedRef) {
265 265 return function(element, callback) {
266 266 // translate our select2 id into a text, it's a mapping to show
267 267 // simple label when selecting by internal ID.
268 268 var id, refData;
269 269 if (selectedRef === undefined || selectedRef === null) {
270 270 id = element.val();
271 271 refData = element.val().split(':');
272 272
273 273 if (refData.length !== 3){
274 274 refData = ["", "", ""]
275 275 }
276 276 } else {
277 277 id = selectedRef;
278 278 refData = selectedRef.split(':');
279 279 }
280 280
281 281 var text = refData[1];
282 282 if (refData[0] === 'rev') {
283 283 text = text.substring(0, 12);
284 284 }
285 285
286 286 var data = {id: id, text: text};
287 287 callback(data);
288 288 };
289 289 };
290 290
291 291 var formatRefSelection = function(data, container, escapeMarkup) {
292 292 var prefix = '';
293 293 var refData = data.id.split(':');
294 294 if (refData[0] === 'branch') {
295 295 prefix = '<i class="icon-branch"></i>';
296 296 }
297 297 else if (refData[0] === 'book') {
298 298 prefix = '<i class="icon-bookmark"></i>';
299 299 }
300 300 else if (refData[0] === 'tag') {
301 301 prefix = '<i class="icon-tag"></i>';
302 302 }
303 303
304 304 var originalOption = data.element;
305 305 return prefix + escapeMarkup(data.text);
306 306 };
307 307
308 308 // custom code mirror
309 309 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
310 310
311 311 var diffDataHandler = function(data) {
312 if (data['error'] !== undefined) {
313 var noCommitsMsg = '<span class="alert-text-error">{0}</span>'.format(data['error']);
314 prButtonLock(true, noCommitsMsg, 'compare');
315 //make both panels equal
316 $('.target-panel').height($('.source-panel').height())
317 return false
318 }
312 319
313 320 var commitElements = data['commits'];
314 321 var files = data['files'];
315 322 var added = data['stats'][0]
316 323 var deleted = data['stats'][1]
317 324 var commonAncestorId = data['ancestor'];
318 325 var _sourceRefType = sourceRef()[0];
319 326 var _sourceRefName = sourceRef()[1];
320 327 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
321 328
322 329 var title = prTitleAndDesc[0];
323 330 var proposedDescription = prTitleAndDesc[1];
324 331
325 332 var useGeneratedTitle = (
326 333 $('#pullrequest_title').hasClass('autogenerated-title') ||
327 334 $('#pullrequest_title').val() === "");
328 335
329 336 if (title && useGeneratedTitle) {
330 337 // use generated title if we haven't specified our own
331 338 $('#pullrequest_title').val(title);
332 339 $('#pullrequest_title').addClass('autogenerated-title');
333 340
334 341 }
335 342
336 343 var useGeneratedDescription = (
337 344 !codeMirrorInstance._userDefinedValue ||
338 345 codeMirrorInstance.getValue() === "");
339 346
340 347 if (proposedDescription && useGeneratedDescription) {
341 348 // set proposed content, if we haven't defined our own,
342 349 // or we don't have description written
343 350 codeMirrorInstance._userDefinedValue = false; // reset state
344 351 codeMirrorInstance.setValue(proposedDescription);
345 352 }
346 353
347 354 // refresh our codeMirror so events kicks in and it's change aware
348 355 codeMirrorInstance.refresh();
349 356
350 357 var url_data = {
351 358 'repo_name': targetRepo(),
352 359 'target_repo': sourceRepo(),
353 360 'source_ref': targetRef()[2],
354 361 'source_ref_type': 'rev',
355 362 'target_ref': sourceRef()[2],
356 363 'target_ref_type': 'rev',
357 364 'merge': true,
358 365 '_': Date.now() // bypass browser caching
359 366 }; // gather the source/target ref and repo here
360 367 var url = pyroutes.url('repo_compare', url_data);
361 368
362 369 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
363 370 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
364 371
365 372
366 373 $.each(commitElements, function(idx, value) {
367 374 var commit_id = value["commit_id"]
368 375 msg += '<input type="hidden" name="revisions" value="{0}">'.format(commit_id);
369 376 });
370 377
371 378 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
372 379 msg += _ngettext(
373 380 'Compare summary: <strong>{0} commit</strong>',
374 381 'Compare summary: <strong>{0} commits</strong>',
375 382 commitElements.length).format(commitElements.length)
376 383
377 384 msg += '';
378 385 msg += _ngettext(
379 386 '<strong>, and {0} file</strong> changed.',
380 387 '<strong>, and {0} files</strong> changed.',
381 388 files.length).format(files.length)
382 389
383 390 msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
384 391
385 392 msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
386 393
387 394 if (commitElements.length) {
388 395 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
389 396 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
390 397 }
391 398 else {
392 399 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
393 400 _gettext('There are no commits to merge.'));
394 401 prButtonLock(true, noCommitsMsg, 'compare');
395 402 }
396 403
397 404 //make both panels equal
398 $('.target-panel').height($('.source-panel').height())
405 $('.target-panel').height($('.source-panel').height());
406 return true
399 407 };
400 408
401 409 reviewersController = new ReviewersController();
402 410 reviewersController.diffDataHandler = diffDataHandler;
403 411
404 412 var queryTargetRepo = function(self, query) {
405 413 // cache ALL results if query is empty
406 414 var cacheKey = query.term || '__';
407 415 var cachedData = self.cachedDataSource[cacheKey];
408 416
409 417 if (cachedData) {
410 418 query.callback({results: cachedData.results});
411 419 } else {
412 420 $.ajax({
413 421 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
414 422 data: {query: query.term},
415 423 dataType: 'json',
416 424 type: 'GET',
417 425 success: function(data) {
418 426 self.cachedDataSource[cacheKey] = data;
419 427 query.callback({results: data.results});
420 428 },
421 429 error: function(jqXHR, textStatus, errorThrown) {
422 430 var prefix = "Error while fetching entries.\n"
423 431 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
424 432 ajaxErrorSwal(message);
425 433 }
426 434 });
427 435 }
428 436 };
429 437
430 438 var queryTargetRefs = function(initialData, query) {
431 439 var data = {results: []};
432 440 // filter initialData
433 441 $.each(initialData, function() {
434 442 var section = this.text;
435 443 var children = [];
436 444 $.each(this.children, function() {
437 445 if (query.term.length === 0 ||
438 446 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
439 447 children.push({'id': this.id, 'text': this.text})
440 448 }
441 449 });
442 450 data.results.push({'text': section, 'children': children})
443 451 });
444 452 query.callback({results: data.results});
445 453 };
446 454
447 455 var Select2Box = function(element, overrides) {
448 456 var globalDefaults = {
449 457 dropdownAutoWidth: true,
450 458 containerCssClass: "drop-menu",
451 459 dropdownCssClass: "drop-menu-dropdown"
452 460 };
453 461
454 462 var initSelect2 = function(defaultOptions) {
455 463 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
456 464 element.select2(options);
457 465 };
458 466
459 467 return {
460 468 initRef: function() {
461 469 var defaultOptions = {
462 470 minimumResultsForSearch: 5,
463 471 formatSelection: formatRefSelection
464 472 };
465 473
466 474 initSelect2(defaultOptions);
467 475 },
468 476
469 477 initRepo: function(defaultValue, readOnly) {
470 478 var defaultOptions = {
471 479 initSelection : function (element, callback) {
472 480 var data = {id: defaultValue, text: defaultValue};
473 481 callback(data);
474 482 }
475 483 };
476 484
477 485 initSelect2(defaultOptions);
478 486
479 487 element.select2('val', defaultSourceRepo);
480 488 if (readOnly === true) {
481 489 element.select2('readonly', true);
482 490 }
483 491 }
484 492 };
485 493 };
486 494
487 495 var initTargetRefs = function(refsData, selectedRef) {
488 496
489 497 Select2Box($targetRef, {
490 498 placeholder: "${_('Select commit reference')}",
491 499 query: function(query) {
492 500 queryTargetRefs(refsData, query);
493 501 },
494 502 initSelection : initRefSelection(selectedRef)
495 503 }).initRef();
496 504
497 505 if (!(selectedRef === undefined)) {
498 506 $targetRef.select2('val', selectedRef);
499 507 }
500 508 };
501 509
502 510 var targetRepoChanged = function(repoData) {
503 511 // generate new DESC of target repo displayed next to select
504 512
505 513 $('#target_repo_desc').html(repoData['description']);
506 514
507 515 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
508 516 var title = _gettext('Switch target repository with the source.')
509 517 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
510 518
511 519 // generate dynamic select2 for refs.
512 520 initTargetRefs(repoData['refs']['select2_refs'],
513 521 repoData['refs']['selected_ref']);
514 522
515 523 };
516 524
517 525 var sourceRefSelect2 = Select2Box($sourceRef, {
518 526 placeholder: "${_('Select commit reference')}",
519 527 query: function(query) {
520 528 var initialData = defaultSourceRepoData['refs']['select2_refs'];
521 529 queryTargetRefs(initialData, query)
522 530 },
523 531 initSelection: initRefSelection()
524 532 });
525 533
526 534 var sourceRepoSelect2 = Select2Box($sourceRepo, {
527 535 query: function(query) {}
528 536 });
529 537
530 538 var targetRepoSelect2 = Select2Box($targetRepo, {
531 539 cachedDataSource: {},
532 540 query: $.debounce(250, function(query) {
533 541 queryTargetRepo(this, query);
534 542 }),
535 543 formatResult: formatRepoResult
536 544 });
537 545
538 546 sourceRefSelect2.initRef();
539 547
540 548 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
541 549
542 550 targetRepoSelect2.initRepo(defaultTargetRepo, false);
543 551
544 552 $sourceRef.on('change', function(e){
545 553 reviewersController.loadDefaultReviewers(
546 554 sourceRepo(), sourceRef(), targetRepo(), targetRef());
547 555 });
548 556
549 557 $targetRef.on('change', function(e){
550 558 reviewersController.loadDefaultReviewers(
551 559 sourceRepo(), sourceRef(), targetRepo(), targetRef());
552 560 });
553 561
554 562 $targetRepo.on('change', function(e){
555 563 var repoName = $(this).val();
556 564 calculateContainerWidth();
557 565 $targetRef.select2('destroy');
558 566 $('#target_ref_loading').show();
559 567
560 568 $.ajax({
561 569 url: pyroutes.url('pullrequest_repo_refs',
562 570 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
563 571 data: {},
564 572 dataType: 'json',
565 573 type: 'GET',
566 574 success: function(data) {
567 575 $('#target_ref_loading').hide();
568 576 targetRepoChanged(data);
569 577 },
570 578 error: function(jqXHR, textStatus, errorThrown) {
571 579 var prefix = "Error while fetching entries.\n"
572 580 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
573 581 ajaxErrorSwal(message);
574 582 }
575 583 })
576 584
577 585 });
578 586
579 587 $pullRequestForm.on('submit', function(e){
580 588 // Flush changes into textarea
581 589 codeMirrorInstance.save();
582 590 prButtonLock(true, null, 'all');
583 591 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
584 592 });
585 593
586 594 prButtonLock(true, "${_('Please select source and target')}", 'all');
587 595
588 596 // auto-load on init, the target refs select2
589 597 calculateContainerWidth();
590 598 targetRepoChanged(defaultTargetRepoData);
591 599
592 600 $('#pullrequest_title').on('keyup', function(e){
593 601 $(this).removeClass('autogenerated-title');
594 602 });
595 603
596 604 % if c.default_source_ref:
597 605 // in case we have a pre-selected value, use it now
598 606 $sourceRef.select2('val', '${c.default_source_ref}');
599 607
600 608
601 609 // default reviewers / observers
602 610 reviewersController.loadDefaultReviewers(
603 611 sourceRepo(), sourceRef(), targetRepo(), targetRef());
604 612 % endif
605 613
606 614 ReviewerAutoComplete('#user', reviewersController);
607 615 ObserverAutoComplete('#observer', reviewersController);
608 616
609 617 // TODO, move this to another handler
610 618
611 619 var $reviewersBtn = $('#reviewers-btn');
612 620 var $reviewersContainer = $('#reviewers-container');
613 621
614 622 var $observersBtn = $('#observers-btn')
615 623 var $observersContainer = $('#observers-container');
616 624
617 625 $reviewersBtn.on('click', function (e) {
618 626
619 627 $observersContainer.hide();
620 628 $reviewersContainer.show();
621 629
622 630 $observersBtn.parent().removeClass('active');
623 631 $reviewersBtn.parent().addClass('active');
624 632 e.preventDefault();
625 633
626 634 })
627 635
628 636 $observersBtn.on('click', function (e) {
629 637
630 638 $reviewersContainer.hide();
631 639 $observersContainer.show();
632 640
633 641 $reviewersBtn.parent().removeClass('active');
634 642 $observersBtn.parent().addClass('active');
635 643 e.preventDefault();
636 644
637 645 })
638 646
639 647 });
640 648 </script>
641 649
642 650 </%def>
General Comments 0
You need to be logged in to leave comments. Login now