##// END OF EJS Templates
pull-requests: fixed creation of pr after new serialized commits data.
marcink -
r4517:7cc4ee55 stable
parent child Browse files
Show More
@@ -1,2235 +1,2237 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import os
30 30
31 31 import datetime
32 32 import urllib
33 33 import collections
34 34
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.translation import lazy_ugettext
40 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 41 from rhodecode.lib import audit_logger
42 42 from rhodecode.lib.compat import OrderedDict
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.markup_renderer import (
45 45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 46 from rhodecode.lib.utils2 import (
47 47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 48 get_current_rhodecode_user)
49 49 from rhodecode.lib.vcs.backends.base import (
50 50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 51 TargetRefMissing, SourceRefMissing)
52 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 CommitDoesNotExistError, EmptyRepositoryError)
55 55 from rhodecode.model import BaseModel
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (
59 59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.notification import NotificationModel, \
63 63 EmailNotificationModel
64 64 from rhodecode.model.scm import ScmModel
65 65 from rhodecode.model.settings import VcsSettingsModel
66 66
67 67
68 68 log = logging.getLogger(__name__)
69 69
70 70
71 71 # Data structure to hold the response data when updating commits during a pull
72 72 # request update.
73 73 class UpdateResponse(object):
74 74
75 75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 76 commit_changes, source_changed, target_changed):
77 77
78 78 self.executed = executed
79 79 self.reason = reason
80 80 self.new = new
81 81 self.old = old
82 82 self.common_ancestor_id = common_ancestor_id
83 83 self.changes = commit_changes
84 84 self.source_changed = source_changed
85 85 self.target_changed = target_changed
86 86
87 87
88 88 def get_diff_info(
89 89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 90 get_commit_authors=True):
91 91 """
92 92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 93 This is also used for default reviewers logic
94 94 """
95 95
96 96 source_scm = source_repo.scm_instance()
97 97 target_scm = target_repo.scm_instance()
98 98
99 99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 100 if not ancestor_id:
101 101 raise ValueError(
102 102 'cannot calculate diff info without a common ancestor. '
103 103 'Make sure both repositories are related, and have a common forking commit.')
104 104
105 105 # case here is that want a simple diff without incoming commits,
106 106 # previewing what will be merged based only on commits in the source.
107 107 log.debug('Using ancestor %s as source_ref instead of %s',
108 108 ancestor_id, source_ref)
109 109
110 110 # source of changes now is the common ancestor
111 111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 112 # target commit becomes the source ref as it is the last commit
113 113 # for diff generation this logic gives proper diff
114 114 target_commit = source_scm.get_commit(commit_id=source_ref)
115 115
116 116 vcs_diff = \
117 117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 118 ignore_whitespace=False, context=3)
119 119
120 120 diff_processor = diffs.DiffProcessor(
121 121 vcs_diff, format='newdiff', diff_limit=None,
122 122 file_limit=None, show_full_diff=True)
123 123
124 124 _parsed = diff_processor.prepare()
125 125
126 126 all_files = []
127 127 all_files_changes = []
128 128 changed_lines = {}
129 129 stats = [0, 0]
130 130 for f in _parsed:
131 131 all_files.append(f['filename'])
132 132 all_files_changes.append({
133 133 'filename': f['filename'],
134 134 'stats': f['stats']
135 135 })
136 136 stats[0] += f['stats']['added']
137 137 stats[1] += f['stats']['deleted']
138 138
139 139 changed_lines[f['filename']] = []
140 140 if len(f['chunks']) < 2:
141 141 continue
142 142 # first line is "context" information
143 143 for chunks in f['chunks'][1:]:
144 144 for chunk in chunks['lines']:
145 145 if chunk['action'] not in ('del', 'mod'):
146 146 continue
147 147 changed_lines[f['filename']].append(chunk['old_lineno'])
148 148
149 149 commit_authors = []
150 150 user_counts = {}
151 151 email_counts = {}
152 152 author_counts = {}
153 153 _commit_cache = {}
154 154
155 155 commits = []
156 156 if get_commit_authors:
157 157 log.debug('Obtaining commit authors from set of commits')
158 158 _compare_data = target_scm.compare(
159 159 target_ref, source_ref, source_scm, merge=True,
160 160 pre_load=["author", "date", "message"]
161 161 )
162 162
163 163 for commit in _compare_data:
164 164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 165 # at this function which is later called via JSON serialization
166 166 serialized_commit = dict(
167 167 author=commit.author,
168 168 date=commit.date,
169 169 message=commit.message,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
170 172 )
171 173 commits.append(serialized_commit)
172 174 user = User.get_from_cs_author(serialized_commit['author'])
173 175 if user and user not in commit_authors:
174 176 commit_authors.append(user)
175 177
176 178 # lines
177 179 if get_authors:
178 180 log.debug('Calculating authors of changed files')
179 181 target_commit = source_repo.get_commit(ancestor_id)
180 182
181 183 for fname, lines in changed_lines.items():
182 184
183 185 try:
184 186 node = target_commit.get_node(fname, pre_load=["is_binary"])
185 187 except Exception:
186 188 log.exception("Failed to load node with path %s", fname)
187 189 continue
188 190
189 191 if not isinstance(node, FileNode):
190 192 continue
191 193
192 194 # NOTE(marcink): for binary node we don't do annotation, just use last author
193 195 if node.is_binary:
194 196 author = node.last_commit.author
195 197 email = node.last_commit.author_email
196 198
197 199 user = User.get_from_cs_author(author)
198 200 if user:
199 201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
200 202 author_counts[author] = author_counts.get(author, 0) + 1
201 203 email_counts[email] = email_counts.get(email, 0) + 1
202 204
203 205 continue
204 206
205 207 for annotation in node.annotate:
206 208 line_no, commit_id, get_commit_func, line_text = annotation
207 209 if line_no in lines:
208 210 if commit_id not in _commit_cache:
209 211 _commit_cache[commit_id] = get_commit_func()
210 212 commit = _commit_cache[commit_id]
211 213 author = commit.author
212 214 email = commit.author_email
213 215 user = User.get_from_cs_author(author)
214 216 if user:
215 217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
216 218 author_counts[author] = author_counts.get(author, 0) + 1
217 219 email_counts[email] = email_counts.get(email, 0) + 1
218 220
219 221 log.debug('Default reviewers processing finished')
220 222
221 223 return {
222 224 'commits': commits,
223 225 'files': all_files_changes,
224 226 'stats': stats,
225 227 'ancestor': ancestor_id,
226 228 # original authors of modified files
227 229 'original_authors': {
228 230 'users': user_counts,
229 231 'authors': author_counts,
230 232 'emails': email_counts,
231 233 },
232 234 'commit_authors': commit_authors
233 235 }
234 236
235 237
236 238 class PullRequestModel(BaseModel):
237 239
238 240 cls = PullRequest
239 241
240 242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
241 243
242 244 UPDATE_STATUS_MESSAGES = {
243 245 UpdateFailureReason.NONE: lazy_ugettext(
244 246 'Pull request update successful.'),
245 247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
246 248 'Pull request update failed because of an unknown error.'),
247 249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
248 250 'No update needed because the source and target have not changed.'),
249 251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
250 252 'Pull request cannot be updated because the reference type is '
251 253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
252 254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
253 255 'This pull request cannot be updated because the target '
254 256 'reference is missing.'),
255 257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
256 258 'This pull request cannot be updated because the source '
257 259 'reference is missing.'),
258 260 }
259 261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
260 262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
261 263
262 264 def __get_pull_request(self, pull_request):
263 265 return self._get_instance((
264 266 PullRequest, PullRequestVersion), pull_request)
265 267
266 268 def _check_perms(self, perms, pull_request, user, api=False):
267 269 if not api:
268 270 return h.HasRepoPermissionAny(*perms)(
269 271 user=user, repo_name=pull_request.target_repo.repo_name)
270 272 else:
271 273 return h.HasRepoPermissionAnyApi(*perms)(
272 274 user=user, repo_name=pull_request.target_repo.repo_name)
273 275
274 276 def check_user_read(self, pull_request, user, api=False):
275 277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
276 278 return self._check_perms(_perms, pull_request, user, api)
277 279
278 280 def check_user_merge(self, pull_request, user, api=False):
279 281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
280 282 return self._check_perms(_perms, pull_request, user, api)
281 283
282 284 def check_user_update(self, pull_request, user, api=False):
283 285 owner = user.user_id == pull_request.user_id
284 286 return self.check_user_merge(pull_request, user, api) or owner
285 287
286 288 def check_user_delete(self, pull_request, user):
287 289 owner = user.user_id == pull_request.user_id
288 290 _perms = ('repository.admin',)
289 291 return self._check_perms(_perms, pull_request, user) or owner
290 292
291 293 def is_user_reviewer(self, pull_request, user):
292 294 return user.user_id in [
293 295 x.user_id for x in
294 296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
295 297 if x.user
296 298 ]
297 299
298 300 def check_user_change_status(self, pull_request, user, api=False):
299 301 return self.check_user_update(pull_request, user, api) \
300 302 or self.is_user_reviewer(pull_request, user)
301 303
302 304 def check_user_comment(self, pull_request, user):
303 305 owner = user.user_id == pull_request.user_id
304 306 return self.check_user_read(pull_request, user) or owner
305 307
306 308 def get(self, pull_request):
307 309 return self.__get_pull_request(pull_request)
308 310
309 311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
310 312 statuses=None, opened_by=None, order_by=None,
311 313 order_dir='desc', only_created=False):
312 314 repo = None
313 315 if repo_name:
314 316 repo = self._get_repo(repo_name)
315 317
316 318 q = PullRequest.query()
317 319
318 320 if search_q:
319 321 like_expression = u'%{}%'.format(safe_unicode(search_q))
320 322 q = q.join(User)
321 323 q = q.filter(or_(
322 324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
323 325 User.username.ilike(like_expression),
324 326 PullRequest.title.ilike(like_expression),
325 327 PullRequest.description.ilike(like_expression),
326 328 ))
327 329
328 330 # source or target
329 331 if repo and source:
330 332 q = q.filter(PullRequest.source_repo == repo)
331 333 elif repo:
332 334 q = q.filter(PullRequest.target_repo == repo)
333 335
334 336 # closed,opened
335 337 if statuses:
336 338 q = q.filter(PullRequest.status.in_(statuses))
337 339
338 340 # opened by filter
339 341 if opened_by:
340 342 q = q.filter(PullRequest.user_id.in_(opened_by))
341 343
342 344 # only get those that are in "created" state
343 345 if only_created:
344 346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
345 347
346 348 if order_by:
347 349 order_map = {
348 350 'name_raw': PullRequest.pull_request_id,
349 351 'id': PullRequest.pull_request_id,
350 352 'title': PullRequest.title,
351 353 'updated_on_raw': PullRequest.updated_on,
352 354 'target_repo': PullRequest.target_repo_id
353 355 }
354 356 if order_dir == 'asc':
355 357 q = q.order_by(order_map[order_by].asc())
356 358 else:
357 359 q = q.order_by(order_map[order_by].desc())
358 360
359 361 return q
360 362
361 363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
362 364 opened_by=None):
363 365 """
364 366 Count the number of pull requests for a specific repository.
365 367
366 368 :param repo_name: target or source repo
367 369 :param search_q: filter by text
368 370 :param source: boolean flag to specify if repo_name refers to source
369 371 :param statuses: list of pull request statuses
370 372 :param opened_by: author user of the pull request
371 373 :returns: int number of pull requests
372 374 """
373 375 q = self._prepare_get_all_query(
374 376 repo_name, search_q=search_q, source=source, statuses=statuses,
375 377 opened_by=opened_by)
376 378
377 379 return q.count()
378 380
379 381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
380 382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
381 383 """
382 384 Get all pull requests for a specific repository.
383 385
384 386 :param repo_name: target or source repo
385 387 :param search_q: filter by text
386 388 :param source: boolean flag to specify if repo_name refers to source
387 389 :param statuses: list of pull request statuses
388 390 :param opened_by: author user of the pull request
389 391 :param offset: pagination offset
390 392 :param length: length of returned list
391 393 :param order_by: order of the returned list
392 394 :param order_dir: 'asc' or 'desc' ordering direction
393 395 :returns: list of pull requests
394 396 """
395 397 q = self._prepare_get_all_query(
396 398 repo_name, search_q=search_q, source=source, statuses=statuses,
397 399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
398 400
399 401 if length:
400 402 pull_requests = q.limit(length).offset(offset).all()
401 403 else:
402 404 pull_requests = q.all()
403 405
404 406 return pull_requests
405 407
406 408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
407 409 opened_by=None):
408 410 """
409 411 Count the number of pull requests for a specific repository that are
410 412 awaiting review.
411 413
412 414 :param repo_name: target or source repo
413 415 :param search_q: filter by text
414 416 :param source: boolean flag to specify if repo_name refers to source
415 417 :param statuses: list of pull request statuses
416 418 :param opened_by: author user of the pull request
417 419 :returns: int number of pull requests
418 420 """
419 421 pull_requests = self.get_awaiting_review(
420 422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
421 423
422 424 return len(pull_requests)
423 425
424 426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
425 427 opened_by=None, offset=0, length=None,
426 428 order_by=None, order_dir='desc'):
427 429 """
428 430 Get all pull requests for a specific repository that are awaiting
429 431 review.
430 432
431 433 :param repo_name: target or source repo
432 434 :param search_q: filter by text
433 435 :param source: boolean flag to specify if repo_name refers to source
434 436 :param statuses: list of pull request statuses
435 437 :param opened_by: author user of the pull request
436 438 :param offset: pagination offset
437 439 :param length: length of returned list
438 440 :param order_by: order of the returned list
439 441 :param order_dir: 'asc' or 'desc' ordering direction
440 442 :returns: list of pull requests
441 443 """
442 444 pull_requests = self.get_all(
443 445 repo_name, search_q=search_q, source=source, statuses=statuses,
444 446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
445 447
446 448 _filtered_pull_requests = []
447 449 for pr in pull_requests:
448 450 status = pr.calculated_review_status()
449 451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
450 452 ChangesetStatus.STATUS_UNDER_REVIEW]:
451 453 _filtered_pull_requests.append(pr)
452 454 if length:
453 455 return _filtered_pull_requests[offset:offset+length]
454 456 else:
455 457 return _filtered_pull_requests
456 458
457 459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
458 460 opened_by=None, user_id=None):
459 461 """
460 462 Count the number of pull requests for a specific repository that are
461 463 awaiting review from a specific user.
462 464
463 465 :param repo_name: target or source repo
464 466 :param search_q: filter by text
465 467 :param source: boolean flag to specify if repo_name refers to source
466 468 :param statuses: list of pull request statuses
467 469 :param opened_by: author user of the pull request
468 470 :param user_id: reviewer user of the pull request
469 471 :returns: int number of pull requests
470 472 """
471 473 pull_requests = self.get_awaiting_my_review(
472 474 repo_name, search_q=search_q, source=source, statuses=statuses,
473 475 opened_by=opened_by, user_id=user_id)
474 476
475 477 return len(pull_requests)
476 478
477 479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
478 480 opened_by=None, user_id=None, offset=0,
479 481 length=None, order_by=None, order_dir='desc'):
480 482 """
481 483 Get all pull requests for a specific repository that are awaiting
482 484 review from a specific user.
483 485
484 486 :param repo_name: target or source repo
485 487 :param search_q: filter by text
486 488 :param source: boolean flag to specify if repo_name refers to source
487 489 :param statuses: list of pull request statuses
488 490 :param opened_by: author user of the pull request
489 491 :param user_id: reviewer user of the pull request
490 492 :param offset: pagination offset
491 493 :param length: length of returned list
492 494 :param order_by: order of the returned list
493 495 :param order_dir: 'asc' or 'desc' ordering direction
494 496 :returns: list of pull requests
495 497 """
496 498 pull_requests = self.get_all(
497 499 repo_name, search_q=search_q, source=source, statuses=statuses,
498 500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
499 501
500 502 _my = PullRequestModel().get_not_reviewed(user_id)
501 503 my_participation = []
502 504 for pr in pull_requests:
503 505 if pr in _my:
504 506 my_participation.append(pr)
505 507 _filtered_pull_requests = my_participation
506 508 if length:
507 509 return _filtered_pull_requests[offset:offset+length]
508 510 else:
509 511 return _filtered_pull_requests
510 512
511 513 def get_not_reviewed(self, user_id):
512 514 return [
513 515 x.pull_request for x in PullRequestReviewers.query().filter(
514 516 PullRequestReviewers.user_id == user_id).all()
515 517 ]
516 518
517 519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
518 520 order_by=None, order_dir='desc'):
519 521 q = PullRequest.query()
520 522 if user_id:
521 523 reviewers_subquery = Session().query(
522 524 PullRequestReviewers.pull_request_id).filter(
523 525 PullRequestReviewers.user_id == user_id).subquery()
524 526 user_filter = or_(
525 527 PullRequest.user_id == user_id,
526 528 PullRequest.pull_request_id.in_(reviewers_subquery)
527 529 )
528 530 q = PullRequest.query().filter(user_filter)
529 531
530 532 # closed,opened
531 533 if statuses:
532 534 q = q.filter(PullRequest.status.in_(statuses))
533 535
534 536 if query:
535 537 like_expression = u'%{}%'.format(safe_unicode(query))
536 538 q = q.join(User)
537 539 q = q.filter(or_(
538 540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
539 541 User.username.ilike(like_expression),
540 542 PullRequest.title.ilike(like_expression),
541 543 PullRequest.description.ilike(like_expression),
542 544 ))
543 545 if order_by:
544 546 order_map = {
545 547 'name_raw': PullRequest.pull_request_id,
546 548 'title': PullRequest.title,
547 549 'updated_on_raw': PullRequest.updated_on,
548 550 'target_repo': PullRequest.target_repo_id
549 551 }
550 552 if order_dir == 'asc':
551 553 q = q.order_by(order_map[order_by].asc())
552 554 else:
553 555 q = q.order_by(order_map[order_by].desc())
554 556
555 557 return q
556 558
557 559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
558 560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
559 561 return q.count()
560 562
561 563 def get_im_participating_in(
562 564 self, user_id=None, statuses=None, query='', offset=0,
563 565 length=None, order_by=None, order_dir='desc'):
564 566 """
565 567 Get all Pull requests that i'm participating in, or i have opened
566 568 """
567 569
568 570 q = self._prepare_participating_query(
569 571 user_id, statuses=statuses, query=query, order_by=order_by,
570 572 order_dir=order_dir)
571 573
572 574 if length:
573 575 pull_requests = q.limit(length).offset(offset).all()
574 576 else:
575 577 pull_requests = q.all()
576 578
577 579 return pull_requests
578 580
579 581 def get_versions(self, pull_request):
580 582 """
581 583 returns version of pull request sorted by ID descending
582 584 """
583 585 return PullRequestVersion.query()\
584 586 .filter(PullRequestVersion.pull_request == pull_request)\
585 587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
586 588 .all()
587 589
588 590 def get_pr_version(self, pull_request_id, version=None):
589 591 at_version = None
590 592
591 593 if version and version == 'latest':
592 594 pull_request_ver = PullRequest.get(pull_request_id)
593 595 pull_request_obj = pull_request_ver
594 596 _org_pull_request_obj = pull_request_obj
595 597 at_version = 'latest'
596 598 elif version:
597 599 pull_request_ver = PullRequestVersion.get_or_404(version)
598 600 pull_request_obj = pull_request_ver
599 601 _org_pull_request_obj = pull_request_ver.pull_request
600 602 at_version = pull_request_ver.pull_request_version_id
601 603 else:
602 604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
603 605 pull_request_id)
604 606
605 607 pull_request_display_obj = PullRequest.get_pr_display_object(
606 608 pull_request_obj, _org_pull_request_obj)
607 609
608 610 return _org_pull_request_obj, pull_request_obj, \
609 611 pull_request_display_obj, at_version
610 612
611 613 def create(self, created_by, source_repo, source_ref, target_repo,
612 614 target_ref, revisions, reviewers, observers, title, description=None,
613 615 common_ancestor_id=None,
614 616 description_renderer=None,
615 617 reviewer_data=None, translator=None, auth_user=None):
616 618 translator = translator or get_current_request().translate
617 619
618 620 created_by_user = self._get_user(created_by)
619 621 auth_user = auth_user or created_by_user.AuthUser()
620 622 source_repo = self._get_repo(source_repo)
621 623 target_repo = self._get_repo(target_repo)
622 624
623 625 pull_request = PullRequest()
624 626 pull_request.source_repo = source_repo
625 627 pull_request.source_ref = source_ref
626 628 pull_request.target_repo = target_repo
627 629 pull_request.target_ref = target_ref
628 630 pull_request.revisions = revisions
629 631 pull_request.title = title
630 632 pull_request.description = description
631 633 pull_request.description_renderer = description_renderer
632 634 pull_request.author = created_by_user
633 635 pull_request.reviewer_data = reviewer_data
634 636 pull_request.pull_request_state = pull_request.STATE_CREATING
635 637 pull_request.common_ancestor_id = common_ancestor_id
636 638
637 639 Session().add(pull_request)
638 640 Session().flush()
639 641
640 642 reviewer_ids = set()
641 643 # members / reviewers
642 644 for reviewer_object in reviewers:
643 645 user_id, reasons, mandatory, role, rules = reviewer_object
644 646 user = self._get_user(user_id)
645 647
646 648 # skip duplicates
647 649 if user.user_id in reviewer_ids:
648 650 continue
649 651
650 652 reviewer_ids.add(user.user_id)
651 653
652 654 reviewer = PullRequestReviewers()
653 655 reviewer.user = user
654 656 reviewer.pull_request = pull_request
655 657 reviewer.reasons = reasons
656 658 reviewer.mandatory = mandatory
657 659 reviewer.role = role
658 660
659 661 # NOTE(marcink): pick only first rule for now
660 662 rule_id = list(rules)[0] if rules else None
661 663 rule = RepoReviewRule.get(rule_id) if rule_id else None
662 664 if rule:
663 665 review_group = rule.user_group_vote_rule(user_id)
664 666 # we check if this particular reviewer is member of a voting group
665 667 if review_group:
666 668 # NOTE(marcink):
667 669 # can be that user is member of more but we pick the first same,
668 670 # same as default reviewers algo
669 671 review_group = review_group[0]
670 672
671 673 rule_data = {
672 674 'rule_name':
673 675 rule.review_rule_name,
674 676 'rule_user_group_entry_id':
675 677 review_group.repo_review_rule_users_group_id,
676 678 'rule_user_group_name':
677 679 review_group.users_group.users_group_name,
678 680 'rule_user_group_members':
679 681 [x.user.username for x in review_group.users_group.members],
680 682 'rule_user_group_members_id':
681 683 [x.user.user_id for x in review_group.users_group.members],
682 684 }
683 685 # e.g {'vote_rule': -1, 'mandatory': True}
684 686 rule_data.update(review_group.rule_data())
685 687
686 688 reviewer.rule_data = rule_data
687 689
688 690 Session().add(reviewer)
689 691 Session().flush()
690 692
691 693 for observer_object in observers:
692 694 user_id, reasons, mandatory, role, rules = observer_object
693 695 user = self._get_user(user_id)
694 696
695 697 # skip duplicates from reviewers
696 698 if user.user_id in reviewer_ids:
697 699 continue
698 700
699 701 #reviewer_ids.add(user.user_id)
700 702
701 703 observer = PullRequestReviewers()
702 704 observer.user = user
703 705 observer.pull_request = pull_request
704 706 observer.reasons = reasons
705 707 observer.mandatory = mandatory
706 708 observer.role = role
707 709
708 710 # NOTE(marcink): pick only first rule for now
709 711 rule_id = list(rules)[0] if rules else None
710 712 rule = RepoReviewRule.get(rule_id) if rule_id else None
711 713 if rule:
712 714 # TODO(marcink): do we need this for observers ??
713 715 pass
714 716
715 717 Session().add(observer)
716 718 Session().flush()
717 719
718 720 # Set approval status to "Under Review" for all commits which are
719 721 # part of this pull request.
720 722 ChangesetStatusModel().set_status(
721 723 repo=target_repo,
722 724 status=ChangesetStatus.STATUS_UNDER_REVIEW,
723 725 user=created_by_user,
724 726 pull_request=pull_request
725 727 )
726 728 # we commit early at this point. This has to do with a fact
727 729 # that before queries do some row-locking. And because of that
728 730 # we need to commit and finish transaction before below validate call
729 731 # that for large repos could be long resulting in long row locks
730 732 Session().commit()
731 733
732 734 # prepare workspace, and run initial merge simulation. Set state during that
733 735 # operation
734 736 pull_request = PullRequest.get(pull_request.pull_request_id)
735 737
736 738 # set as merging, for merge simulation, and if finished to created so we mark
737 739 # simulation is working fine
738 740 with pull_request.set_state(PullRequest.STATE_MERGING,
739 741 final_state=PullRequest.STATE_CREATED) as state_obj:
740 742 MergeCheck.validate(
741 743 pull_request, auth_user=auth_user, translator=translator)
742 744
743 745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
744 746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
745 747
746 748 creation_data = pull_request.get_api_data(with_merge_state=False)
747 749 self._log_audit_action(
748 750 'repo.pull_request.create', {'data': creation_data},
749 751 auth_user, pull_request)
750 752
751 753 return pull_request
752 754
753 755 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
754 756 pull_request = self.__get_pull_request(pull_request)
755 757 target_scm = pull_request.target_repo.scm_instance()
756 758 if action == 'create':
757 759 trigger_hook = hooks_utils.trigger_create_pull_request_hook
758 760 elif action == 'merge':
759 761 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
760 762 elif action == 'close':
761 763 trigger_hook = hooks_utils.trigger_close_pull_request_hook
762 764 elif action == 'review_status_change':
763 765 trigger_hook = hooks_utils.trigger_review_pull_request_hook
764 766 elif action == 'update':
765 767 trigger_hook = hooks_utils.trigger_update_pull_request_hook
766 768 elif action == 'comment':
767 769 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
768 770 elif action == 'comment_edit':
769 771 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
770 772 else:
771 773 return
772 774
773 775 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
774 776 pull_request, action, trigger_hook)
775 777 trigger_hook(
776 778 username=user.username,
777 779 repo_name=pull_request.target_repo.repo_name,
778 780 repo_type=target_scm.alias,
779 781 pull_request=pull_request,
780 782 data=data)
781 783
782 784 def _get_commit_ids(self, pull_request):
783 785 """
784 786 Return the commit ids of the merged pull request.
785 787
786 788 This method is not dealing correctly yet with the lack of autoupdates
787 789 nor with the implicit target updates.
788 790 For example: if a commit in the source repo is already in the target it
789 791 will be reported anyways.
790 792 """
791 793 merge_rev = pull_request.merge_rev
792 794 if merge_rev is None:
793 795 raise ValueError('This pull request was not merged yet')
794 796
795 797 commit_ids = list(pull_request.revisions)
796 798 if merge_rev not in commit_ids:
797 799 commit_ids.append(merge_rev)
798 800
799 801 return commit_ids
800 802
801 803 def merge_repo(self, pull_request, user, extras):
802 804 log.debug("Merging pull request %s", pull_request.pull_request_id)
803 805 extras['user_agent'] = 'internal-merge'
804 806 merge_state = self._merge_pull_request(pull_request, user, extras)
805 807 if merge_state.executed:
806 808 log.debug("Merge was successful, updating the pull request comments.")
807 809 self._comment_and_close_pr(pull_request, user, merge_state)
808 810
809 811 self._log_audit_action(
810 812 'repo.pull_request.merge',
811 813 {'merge_state': merge_state.__dict__},
812 814 user, pull_request)
813 815
814 816 else:
815 817 log.warn("Merge failed, not updating the pull request.")
816 818 return merge_state
817 819
818 820 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
819 821 target_vcs = pull_request.target_repo.scm_instance()
820 822 source_vcs = pull_request.source_repo.scm_instance()
821 823
822 824 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
823 825 pr_id=pull_request.pull_request_id,
824 826 pr_title=pull_request.title,
825 827 source_repo=source_vcs.name,
826 828 source_ref_name=pull_request.source_ref_parts.name,
827 829 target_repo=target_vcs.name,
828 830 target_ref_name=pull_request.target_ref_parts.name,
829 831 )
830 832
831 833 workspace_id = self._workspace_id(pull_request)
832 834 repo_id = pull_request.target_repo.repo_id
833 835 use_rebase = self._use_rebase_for_merging(pull_request)
834 836 close_branch = self._close_branch_before_merging(pull_request)
835 837 user_name = self._user_name_for_merging(pull_request, user)
836 838
837 839 target_ref = self._refresh_reference(
838 840 pull_request.target_ref_parts, target_vcs)
839 841
840 842 callback_daemon, extras = prepare_callback_daemon(
841 843 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
842 844 host=vcs_settings.HOOKS_HOST,
843 845 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
844 846
845 847 with callback_daemon:
846 848 # TODO: johbo: Implement a clean way to run a config_override
847 849 # for a single call.
848 850 target_vcs.config.set(
849 851 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
850 852
851 853 merge_state = target_vcs.merge(
852 854 repo_id, workspace_id, target_ref, source_vcs,
853 855 pull_request.source_ref_parts,
854 856 user_name=user_name, user_email=user.email,
855 857 message=message, use_rebase=use_rebase,
856 858 close_branch=close_branch)
857 859 return merge_state
858 860
859 861 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
860 862 pull_request.merge_rev = merge_state.merge_ref.commit_id
861 863 pull_request.updated_on = datetime.datetime.now()
862 864 close_msg = close_msg or 'Pull request merged and closed'
863 865
864 866 CommentsModel().create(
865 867 text=safe_unicode(close_msg),
866 868 repo=pull_request.target_repo.repo_id,
867 869 user=user.user_id,
868 870 pull_request=pull_request.pull_request_id,
869 871 f_path=None,
870 872 line_no=None,
871 873 closing_pr=True
872 874 )
873 875
874 876 Session().add(pull_request)
875 877 Session().flush()
876 878 # TODO: paris: replace invalidation with less radical solution
877 879 ScmModel().mark_for_invalidation(
878 880 pull_request.target_repo.repo_name)
879 881 self.trigger_pull_request_hook(pull_request, user, 'merge')
880 882
881 883 def has_valid_update_type(self, pull_request):
882 884 source_ref_type = pull_request.source_ref_parts.type
883 885 return source_ref_type in self.REF_TYPES
884 886
885 887 def get_flow_commits(self, pull_request):
886 888
887 889 # source repo
888 890 source_ref_name = pull_request.source_ref_parts.name
889 891 source_ref_type = pull_request.source_ref_parts.type
890 892 source_ref_id = pull_request.source_ref_parts.commit_id
891 893 source_repo = pull_request.source_repo.scm_instance()
892 894
893 895 try:
894 896 if source_ref_type in self.REF_TYPES:
895 897 source_commit = source_repo.get_commit(source_ref_name)
896 898 else:
897 899 source_commit = source_repo.get_commit(source_ref_id)
898 900 except CommitDoesNotExistError:
899 901 raise SourceRefMissing()
900 902
901 903 # target repo
902 904 target_ref_name = pull_request.target_ref_parts.name
903 905 target_ref_type = pull_request.target_ref_parts.type
904 906 target_ref_id = pull_request.target_ref_parts.commit_id
905 907 target_repo = pull_request.target_repo.scm_instance()
906 908
907 909 try:
908 910 if target_ref_type in self.REF_TYPES:
909 911 target_commit = target_repo.get_commit(target_ref_name)
910 912 else:
911 913 target_commit = target_repo.get_commit(target_ref_id)
912 914 except CommitDoesNotExistError:
913 915 raise TargetRefMissing()
914 916
915 917 return source_commit, target_commit
916 918
917 919 def update_commits(self, pull_request, updating_user):
918 920 """
919 921 Get the updated list of commits for the pull request
920 922 and return the new pull request version and the list
921 923 of commits processed by this update action
922 924
923 925 updating_user is the user_object who triggered the update
924 926 """
925 927 pull_request = self.__get_pull_request(pull_request)
926 928 source_ref_type = pull_request.source_ref_parts.type
927 929 source_ref_name = pull_request.source_ref_parts.name
928 930 source_ref_id = pull_request.source_ref_parts.commit_id
929 931
930 932 target_ref_type = pull_request.target_ref_parts.type
931 933 target_ref_name = pull_request.target_ref_parts.name
932 934 target_ref_id = pull_request.target_ref_parts.commit_id
933 935
934 936 if not self.has_valid_update_type(pull_request):
935 937 log.debug("Skipping update of pull request %s due to ref type: %s",
936 938 pull_request, source_ref_type)
937 939 return UpdateResponse(
938 940 executed=False,
939 941 reason=UpdateFailureReason.WRONG_REF_TYPE,
940 942 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
941 943 source_changed=False, target_changed=False)
942 944
943 945 try:
944 946 source_commit, target_commit = self.get_flow_commits(pull_request)
945 947 except SourceRefMissing:
946 948 return UpdateResponse(
947 949 executed=False,
948 950 reason=UpdateFailureReason.MISSING_SOURCE_REF,
949 951 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
950 952 source_changed=False, target_changed=False)
951 953 except TargetRefMissing:
952 954 return UpdateResponse(
953 955 executed=False,
954 956 reason=UpdateFailureReason.MISSING_TARGET_REF,
955 957 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
956 958 source_changed=False, target_changed=False)
957 959
958 960 source_changed = source_ref_id != source_commit.raw_id
959 961 target_changed = target_ref_id != target_commit.raw_id
960 962
961 963 if not (source_changed or target_changed):
962 964 log.debug("Nothing changed in pull request %s", pull_request)
963 965 return UpdateResponse(
964 966 executed=False,
965 967 reason=UpdateFailureReason.NO_CHANGE,
966 968 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
967 969 source_changed=target_changed, target_changed=source_changed)
968 970
969 971 change_in_found = 'target repo' if target_changed else 'source repo'
970 972 log.debug('Updating pull request because of change in %s detected',
971 973 change_in_found)
972 974
973 975 # Finally there is a need for an update, in case of source change
974 976 # we create a new version, else just an update
975 977 if source_changed:
976 978 pull_request_version = self._create_version_from_snapshot(pull_request)
977 979 self._link_comments_to_version(pull_request_version)
978 980 else:
979 981 try:
980 982 ver = pull_request.versions[-1]
981 983 except IndexError:
982 984 ver = None
983 985
984 986 pull_request.pull_request_version_id = \
985 987 ver.pull_request_version_id if ver else None
986 988 pull_request_version = pull_request
987 989
988 990 source_repo = pull_request.source_repo.scm_instance()
989 991 target_repo = pull_request.target_repo.scm_instance()
990 992
991 993 # re-compute commit ids
992 994 old_commit_ids = pull_request.revisions
993 995 pre_load = ["author", "date", "message", "branch"]
994 996 commit_ranges = target_repo.compare(
995 997 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
996 998 pre_load=pre_load)
997 999
998 1000 target_ref = target_commit.raw_id
999 1001 source_ref = source_commit.raw_id
1000 1002 ancestor_commit_id = target_repo.get_common_ancestor(
1001 1003 target_ref, source_ref, source_repo)
1002 1004
1003 1005 if not ancestor_commit_id:
1004 1006 raise ValueError(
1005 1007 'cannot calculate diff info without a common ancestor. '
1006 1008 'Make sure both repositories are related, and have a common forking commit.')
1007 1009
1008 1010 pull_request.common_ancestor_id = ancestor_commit_id
1009 1011
1010 1012 pull_request.source_ref = '%s:%s:%s' % (
1011 1013 source_ref_type, source_ref_name, source_commit.raw_id)
1012 1014 pull_request.target_ref = '%s:%s:%s' % (
1013 1015 target_ref_type, target_ref_name, ancestor_commit_id)
1014 1016
1015 1017 pull_request.revisions = [
1016 1018 commit.raw_id for commit in reversed(commit_ranges)]
1017 1019 pull_request.updated_on = datetime.datetime.now()
1018 1020 Session().add(pull_request)
1019 1021 new_commit_ids = pull_request.revisions
1020 1022
1021 1023 old_diff_data, new_diff_data = self._generate_update_diffs(
1022 1024 pull_request, pull_request_version)
1023 1025
1024 1026 # calculate commit and file changes
1025 1027 commit_changes = self._calculate_commit_id_changes(
1026 1028 old_commit_ids, new_commit_ids)
1027 1029 file_changes = self._calculate_file_changes(
1028 1030 old_diff_data, new_diff_data)
1029 1031
1030 1032 # set comments as outdated if DIFFS changed
1031 1033 CommentsModel().outdate_comments(
1032 1034 pull_request, old_diff_data=old_diff_data,
1033 1035 new_diff_data=new_diff_data)
1034 1036
1035 1037 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1036 1038 file_node_changes = (
1037 1039 file_changes.added or file_changes.modified or file_changes.removed)
1038 1040 pr_has_changes = valid_commit_changes or file_node_changes
1039 1041
1040 1042 # Add an automatic comment to the pull request, in case
1041 1043 # anything has changed
1042 1044 if pr_has_changes:
1043 1045 update_comment = CommentsModel().create(
1044 1046 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1045 1047 repo=pull_request.target_repo,
1046 1048 user=pull_request.author,
1047 1049 pull_request=pull_request,
1048 1050 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1049 1051
1050 1052 # Update status to "Under Review" for added commits
1051 1053 for commit_id in commit_changes.added:
1052 1054 ChangesetStatusModel().set_status(
1053 1055 repo=pull_request.source_repo,
1054 1056 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1055 1057 comment=update_comment,
1056 1058 user=pull_request.author,
1057 1059 pull_request=pull_request,
1058 1060 revision=commit_id)
1059 1061
1060 1062 # send update email to users
1061 1063 try:
1062 1064 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1063 1065 ancestor_commit_id=ancestor_commit_id,
1064 1066 commit_changes=commit_changes,
1065 1067 file_changes=file_changes)
1066 1068 except Exception:
1067 1069 log.exception('Failed to send email notification to users')
1068 1070
1069 1071 log.debug(
1070 1072 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1071 1073 'removed_ids: %s', pull_request.pull_request_id,
1072 1074 commit_changes.added, commit_changes.common, commit_changes.removed)
1073 1075 log.debug(
1074 1076 'Updated pull request with the following file changes: %s',
1075 1077 file_changes)
1076 1078
1077 1079 log.info(
1078 1080 "Updated pull request %s from commit %s to commit %s, "
1079 1081 "stored new version %s of this pull request.",
1080 1082 pull_request.pull_request_id, source_ref_id,
1081 1083 pull_request.source_ref_parts.commit_id,
1082 1084 pull_request_version.pull_request_version_id)
1083 1085 Session().commit()
1084 1086 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1085 1087
1086 1088 return UpdateResponse(
1087 1089 executed=True, reason=UpdateFailureReason.NONE,
1088 1090 old=pull_request, new=pull_request_version,
1089 1091 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1090 1092 source_changed=source_changed, target_changed=target_changed)
1091 1093
1092 1094 def _create_version_from_snapshot(self, pull_request):
1093 1095 version = PullRequestVersion()
1094 1096 version.title = pull_request.title
1095 1097 version.description = pull_request.description
1096 1098 version.status = pull_request.status
1097 1099 version.pull_request_state = pull_request.pull_request_state
1098 1100 version.created_on = datetime.datetime.now()
1099 1101 version.updated_on = pull_request.updated_on
1100 1102 version.user_id = pull_request.user_id
1101 1103 version.source_repo = pull_request.source_repo
1102 1104 version.source_ref = pull_request.source_ref
1103 1105 version.target_repo = pull_request.target_repo
1104 1106 version.target_ref = pull_request.target_ref
1105 1107
1106 1108 version._last_merge_source_rev = pull_request._last_merge_source_rev
1107 1109 version._last_merge_target_rev = pull_request._last_merge_target_rev
1108 1110 version.last_merge_status = pull_request.last_merge_status
1109 1111 version.last_merge_metadata = pull_request.last_merge_metadata
1110 1112 version.shadow_merge_ref = pull_request.shadow_merge_ref
1111 1113 version.merge_rev = pull_request.merge_rev
1112 1114 version.reviewer_data = pull_request.reviewer_data
1113 1115
1114 1116 version.revisions = pull_request.revisions
1115 1117 version.common_ancestor_id = pull_request.common_ancestor_id
1116 1118 version.pull_request = pull_request
1117 1119 Session().add(version)
1118 1120 Session().flush()
1119 1121
1120 1122 return version
1121 1123
1122 1124 def _generate_update_diffs(self, pull_request, pull_request_version):
1123 1125
1124 1126 diff_context = (
1125 1127 self.DIFF_CONTEXT +
1126 1128 CommentsModel.needed_extra_diff_context())
1127 1129 hide_whitespace_changes = False
1128 1130 source_repo = pull_request_version.source_repo
1129 1131 source_ref_id = pull_request_version.source_ref_parts.commit_id
1130 1132 target_ref_id = pull_request_version.target_ref_parts.commit_id
1131 1133 old_diff = self._get_diff_from_pr_or_version(
1132 1134 source_repo, source_ref_id, target_ref_id,
1133 1135 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1134 1136
1135 1137 source_repo = pull_request.source_repo
1136 1138 source_ref_id = pull_request.source_ref_parts.commit_id
1137 1139 target_ref_id = pull_request.target_ref_parts.commit_id
1138 1140
1139 1141 new_diff = self._get_diff_from_pr_or_version(
1140 1142 source_repo, source_ref_id, target_ref_id,
1141 1143 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1142 1144
1143 1145 old_diff_data = diffs.DiffProcessor(old_diff)
1144 1146 old_diff_data.prepare()
1145 1147 new_diff_data = diffs.DiffProcessor(new_diff)
1146 1148 new_diff_data.prepare()
1147 1149
1148 1150 return old_diff_data, new_diff_data
1149 1151
1150 1152 def _link_comments_to_version(self, pull_request_version):
1151 1153 """
1152 1154 Link all unlinked comments of this pull request to the given version.
1153 1155
1154 1156 :param pull_request_version: The `PullRequestVersion` to which
1155 1157 the comments shall be linked.
1156 1158
1157 1159 """
1158 1160 pull_request = pull_request_version.pull_request
1159 1161 comments = ChangesetComment.query()\
1160 1162 .filter(
1161 1163 # TODO: johbo: Should we query for the repo at all here?
1162 1164 # Pending decision on how comments of PRs are to be related
1163 1165 # to either the source repo, the target repo or no repo at all.
1164 1166 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1165 1167 ChangesetComment.pull_request == pull_request,
1166 1168 ChangesetComment.pull_request_version == None)\
1167 1169 .order_by(ChangesetComment.comment_id.asc())
1168 1170
1169 1171 # TODO: johbo: Find out why this breaks if it is done in a bulk
1170 1172 # operation.
1171 1173 for comment in comments:
1172 1174 comment.pull_request_version_id = (
1173 1175 pull_request_version.pull_request_version_id)
1174 1176 Session().add(comment)
1175 1177
1176 1178 def _calculate_commit_id_changes(self, old_ids, new_ids):
1177 1179 added = [x for x in new_ids if x not in old_ids]
1178 1180 common = [x for x in new_ids if x in old_ids]
1179 1181 removed = [x for x in old_ids if x not in new_ids]
1180 1182 total = new_ids
1181 1183 return ChangeTuple(added, common, removed, total)
1182 1184
1183 1185 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1184 1186
1185 1187 old_files = OrderedDict()
1186 1188 for diff_data in old_diff_data.parsed_diff:
1187 1189 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1188 1190
1189 1191 added_files = []
1190 1192 modified_files = []
1191 1193 removed_files = []
1192 1194 for diff_data in new_diff_data.parsed_diff:
1193 1195 new_filename = diff_data['filename']
1194 1196 new_hash = md5_safe(diff_data['raw_diff'])
1195 1197
1196 1198 old_hash = old_files.get(new_filename)
1197 1199 if not old_hash:
1198 1200 # file is not present in old diff, we have to figure out from parsed diff
1199 1201 # operation ADD/REMOVE
1200 1202 operations_dict = diff_data['stats']['ops']
1201 1203 if diffs.DEL_FILENODE in operations_dict:
1202 1204 removed_files.append(new_filename)
1203 1205 else:
1204 1206 added_files.append(new_filename)
1205 1207 else:
1206 1208 if new_hash != old_hash:
1207 1209 modified_files.append(new_filename)
1208 1210 # now remove a file from old, since we have seen it already
1209 1211 del old_files[new_filename]
1210 1212
1211 1213 # removed files is when there are present in old, but not in NEW,
1212 1214 # since we remove old files that are present in new diff, left-overs
1213 1215 # if any should be the removed files
1214 1216 removed_files.extend(old_files.keys())
1215 1217
1216 1218 return FileChangeTuple(added_files, modified_files, removed_files)
1217 1219
1218 1220 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1219 1221 """
1220 1222 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1221 1223 so it's always looking the same disregarding on which default
1222 1224 renderer system is using.
1223 1225
1224 1226 :param ancestor_commit_id: ancestor raw_id
1225 1227 :param changes: changes named tuple
1226 1228 :param file_changes: file changes named tuple
1227 1229
1228 1230 """
1229 1231 new_status = ChangesetStatus.get_status_lbl(
1230 1232 ChangesetStatus.STATUS_UNDER_REVIEW)
1231 1233
1232 1234 changed_files = (
1233 1235 file_changes.added + file_changes.modified + file_changes.removed)
1234 1236
1235 1237 params = {
1236 1238 'under_review_label': new_status,
1237 1239 'added_commits': changes.added,
1238 1240 'removed_commits': changes.removed,
1239 1241 'changed_files': changed_files,
1240 1242 'added_files': file_changes.added,
1241 1243 'modified_files': file_changes.modified,
1242 1244 'removed_files': file_changes.removed,
1243 1245 'ancestor_commit_id': ancestor_commit_id
1244 1246 }
1245 1247 renderer = RstTemplateRenderer()
1246 1248 return renderer.render('pull_request_update.mako', **params)
1247 1249
1248 1250 def edit(self, pull_request, title, description, description_renderer, user):
1249 1251 pull_request = self.__get_pull_request(pull_request)
1250 1252 old_data = pull_request.get_api_data(with_merge_state=False)
1251 1253 if pull_request.is_closed():
1252 1254 raise ValueError('This pull request is closed')
1253 1255 if title:
1254 1256 pull_request.title = title
1255 1257 pull_request.description = description
1256 1258 pull_request.updated_on = datetime.datetime.now()
1257 1259 pull_request.description_renderer = description_renderer
1258 1260 Session().add(pull_request)
1259 1261 self._log_audit_action(
1260 1262 'repo.pull_request.edit', {'old_data': old_data},
1261 1263 user, pull_request)
1262 1264
1263 1265 def update_reviewers(self, pull_request, reviewer_data, user):
1264 1266 """
1265 1267 Update the reviewers in the pull request
1266 1268
1267 1269 :param pull_request: the pr to update
1268 1270 :param reviewer_data: list of tuples
1269 1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1270 1272 :param user: current use who triggers this action
1271 1273 """
1272 1274
1273 1275 pull_request = self.__get_pull_request(pull_request)
1274 1276 if pull_request.is_closed():
1275 1277 raise ValueError('This pull request is closed')
1276 1278
1277 1279 reviewers = {}
1278 1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1279 1281 if isinstance(user_id, (int, compat.string_types)):
1280 1282 user_id = self._get_user(user_id).user_id
1281 1283 reviewers[user_id] = {
1282 1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1283 1285
1284 1286 reviewers_ids = set(reviewers.keys())
1285 1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1286 1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1287 1289
1288 1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1289 1291
1290 1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1291 1293 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1292 1294
1293 1295 log.debug("Adding %s reviewers", ids_to_add)
1294 1296 log.debug("Removing %s reviewers", ids_to_remove)
1295 1297 changed = False
1296 1298 added_audit_reviewers = []
1297 1299 removed_audit_reviewers = []
1298 1300
1299 1301 for uid in ids_to_add:
1300 1302 changed = True
1301 1303 _usr = self._get_user(uid)
1302 1304 reviewer = PullRequestReviewers()
1303 1305 reviewer.user = _usr
1304 1306 reviewer.pull_request = pull_request
1305 1307 reviewer.reasons = reviewers[uid]['reasons']
1306 1308 # NOTE(marcink): mandatory shouldn't be changed now
1307 1309 # reviewer.mandatory = reviewers[uid]['reasons']
1308 1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1309 1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1310 1312 Session().add(reviewer)
1311 1313 added_audit_reviewers.append(reviewer.get_dict())
1312 1314
1313 1315 for uid in ids_to_remove:
1314 1316 changed = True
1315 1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1316 1318 # This is an edge case that handles previous state of having the same reviewer twice.
1317 1319 # this CAN happen due to the lack of DB checks
1318 1320 reviewers = PullRequestReviewers.query()\
1319 1321 .filter(PullRequestReviewers.user_id == uid,
1320 1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1321 1323 PullRequestReviewers.pull_request == pull_request)\
1322 1324 .all()
1323 1325
1324 1326 for obj in reviewers:
1325 1327 added_audit_reviewers.append(obj.get_dict())
1326 1328 Session().delete(obj)
1327 1329
1328 1330 if changed:
1329 1331 Session().expire_all()
1330 1332 pull_request.updated_on = datetime.datetime.now()
1331 1333 Session().add(pull_request)
1332 1334
1333 1335 # finally store audit logs
1334 1336 for user_data in added_audit_reviewers:
1335 1337 self._log_audit_action(
1336 1338 'repo.pull_request.reviewer.add', {'data': user_data},
1337 1339 user, pull_request)
1338 1340 for user_data in removed_audit_reviewers:
1339 1341 self._log_audit_action(
1340 1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1341 1343 user, pull_request)
1342 1344
1343 1345 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1344 1346 return ids_to_add, ids_to_remove
1345 1347
1346 1348 def update_observers(self, pull_request, observer_data, user):
1347 1349 """
1348 1350 Update the observers in the pull request
1349 1351
1350 1352 :param pull_request: the pr to update
1351 1353 :param observer_data: list of tuples
1352 1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1353 1355 :param user: current use who triggers this action
1354 1356 """
1355 1357 pull_request = self.__get_pull_request(pull_request)
1356 1358 if pull_request.is_closed():
1357 1359 raise ValueError('This pull request is closed')
1358 1360
1359 1361 observers = {}
1360 1362 for user_id, reasons, mandatory, role, rules in observer_data:
1361 1363 if isinstance(user_id, (int, compat.string_types)):
1362 1364 user_id = self._get_user(user_id).user_id
1363 1365 observers[user_id] = {
1364 1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1365 1367
1366 1368 observers_ids = set(observers.keys())
1367 1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1368 1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1369 1371
1370 1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1371 1373
1372 1374 ids_to_add = observers_ids.difference(current_observers_ids)
1373 1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1374 1376
1375 1377 log.debug("Adding %s observer", ids_to_add)
1376 1378 log.debug("Removing %s observer", ids_to_remove)
1377 1379 changed = False
1378 1380 added_audit_observers = []
1379 1381 removed_audit_observers = []
1380 1382
1381 1383 for uid in ids_to_add:
1382 1384 changed = True
1383 1385 _usr = self._get_user(uid)
1384 1386 observer = PullRequestReviewers()
1385 1387 observer.user = _usr
1386 1388 observer.pull_request = pull_request
1387 1389 observer.reasons = observers[uid]['reasons']
1388 1390 # NOTE(marcink): mandatory shouldn't be changed now
1389 1391 # observer.mandatory = observer[uid]['reasons']
1390 1392
1391 1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1392 1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1393 1395 Session().add(observer)
1394 1396 added_audit_observers.append(observer.get_dict())
1395 1397
1396 1398 for uid in ids_to_remove:
1397 1399 changed = True
1398 1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1399 1401 # This is an edge case that handles previous state of having the same reviewer twice.
1400 1402 # this CAN happen due to the lack of DB checks
1401 1403 observers = PullRequestReviewers.query()\
1402 1404 .filter(PullRequestReviewers.user_id == uid,
1403 1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1404 1406 PullRequestReviewers.pull_request == pull_request)\
1405 1407 .all()
1406 1408
1407 1409 for obj in observers:
1408 1410 added_audit_observers.append(obj.get_dict())
1409 1411 Session().delete(obj)
1410 1412
1411 1413 if changed:
1412 1414 Session().expire_all()
1413 1415 pull_request.updated_on = datetime.datetime.now()
1414 1416 Session().add(pull_request)
1415 1417
1416 1418 # finally store audit logs
1417 1419 for user_data in added_audit_observers:
1418 1420 self._log_audit_action(
1419 1421 'repo.pull_request.observer.add', {'data': user_data},
1420 1422 user, pull_request)
1421 1423 for user_data in removed_audit_observers:
1422 1424 self._log_audit_action(
1423 1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1424 1426 user, pull_request)
1425 1427
1426 1428 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1427 1429 return ids_to_add, ids_to_remove
1428 1430
1429 1431 def get_url(self, pull_request, request=None, permalink=False):
1430 1432 if not request:
1431 1433 request = get_current_request()
1432 1434
1433 1435 if permalink:
1434 1436 return request.route_url(
1435 1437 'pull_requests_global',
1436 1438 pull_request_id=pull_request.pull_request_id,)
1437 1439 else:
1438 1440 return request.route_url('pullrequest_show',
1439 1441 repo_name=safe_str(pull_request.target_repo.repo_name),
1440 1442 pull_request_id=pull_request.pull_request_id,)
1441 1443
1442 1444 def get_shadow_clone_url(self, pull_request, request=None):
1443 1445 """
1444 1446 Returns qualified url pointing to the shadow repository. If this pull
1445 1447 request is closed there is no shadow repository and ``None`` will be
1446 1448 returned.
1447 1449 """
1448 1450 if pull_request.is_closed():
1449 1451 return None
1450 1452 else:
1451 1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1452 1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1453 1455
1454 1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1455 1457 # notification to reviewers/observers
1456 1458 if not user_ids:
1457 1459 return
1458 1460
1459 1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1460 1462
1461 1463 pull_request_obj = pull_request
1462 1464 # get the current participants of this pull request
1463 1465 recipients = user_ids
1464 1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1465 1467
1466 1468 pr_source_repo = pull_request_obj.source_repo
1467 1469 pr_target_repo = pull_request_obj.target_repo
1468 1470
1469 1471 pr_url = h.route_url('pullrequest_show',
1470 1472 repo_name=pr_target_repo.repo_name,
1471 1473 pull_request_id=pull_request_obj.pull_request_id,)
1472 1474
1473 1475 # set some variables for email notification
1474 1476 pr_target_repo_url = h.route_url(
1475 1477 'repo_summary', repo_name=pr_target_repo.repo_name)
1476 1478
1477 1479 pr_source_repo_url = h.route_url(
1478 1480 'repo_summary', repo_name=pr_source_repo.repo_name)
1479 1481
1480 1482 # pull request specifics
1481 1483 pull_request_commits = [
1482 1484 (x.raw_id, x.message)
1483 1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1484 1486
1485 1487 current_rhodecode_user = user
1486 1488 kwargs = {
1487 1489 'user': current_rhodecode_user,
1488 1490 'pull_request_author': pull_request.author,
1489 1491 'pull_request': pull_request_obj,
1490 1492 'pull_request_commits': pull_request_commits,
1491 1493
1492 1494 'pull_request_target_repo': pr_target_repo,
1493 1495 'pull_request_target_repo_url': pr_target_repo_url,
1494 1496
1495 1497 'pull_request_source_repo': pr_source_repo,
1496 1498 'pull_request_source_repo_url': pr_source_repo_url,
1497 1499
1498 1500 'pull_request_url': pr_url,
1499 1501 'thread_ids': [pr_url],
1500 1502 'user_role': role
1501 1503 }
1502 1504
1503 1505 # pre-generate the subject for notification itself
1504 1506 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1505 1507 notification_type, **kwargs)
1506 1508
1507 1509 # create notification objects, and emails
1508 1510 NotificationModel().create(
1509 1511 created_by=current_rhodecode_user,
1510 1512 notification_subject=subject,
1511 1513 notification_body=body_plaintext,
1512 1514 notification_type=notification_type,
1513 1515 recipients=recipients,
1514 1516 email_kwargs=kwargs,
1515 1517 )
1516 1518
1517 1519 def notify_reviewers(self, pull_request, reviewers_ids, user):
1518 1520 return self._notify_reviewers(pull_request, reviewers_ids,
1519 1521 PullRequestReviewers.ROLE_REVIEWER, user)
1520 1522
1521 1523 def notify_observers(self, pull_request, observers_ids, user):
1522 1524 return self._notify_reviewers(pull_request, observers_ids,
1523 1525 PullRequestReviewers.ROLE_OBSERVER, user)
1524 1526
1525 1527 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1526 1528 commit_changes, file_changes):
1527 1529
1528 1530 updating_user_id = updating_user.user_id
1529 1531 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1530 1532 # NOTE(marcink): send notification to all other users except to
1531 1533 # person who updated the PR
1532 1534 recipients = reviewers.difference(set([updating_user_id]))
1533 1535
1534 1536 log.debug('Notify following recipients about pull-request update %s', recipients)
1535 1537
1536 1538 pull_request_obj = pull_request
1537 1539
1538 1540 # send email about the update
1539 1541 changed_files = (
1540 1542 file_changes.added + file_changes.modified + file_changes.removed)
1541 1543
1542 1544 pr_source_repo = pull_request_obj.source_repo
1543 1545 pr_target_repo = pull_request_obj.target_repo
1544 1546
1545 1547 pr_url = h.route_url('pullrequest_show',
1546 1548 repo_name=pr_target_repo.repo_name,
1547 1549 pull_request_id=pull_request_obj.pull_request_id,)
1548 1550
1549 1551 # set some variables for email notification
1550 1552 pr_target_repo_url = h.route_url(
1551 1553 'repo_summary', repo_name=pr_target_repo.repo_name)
1552 1554
1553 1555 pr_source_repo_url = h.route_url(
1554 1556 'repo_summary', repo_name=pr_source_repo.repo_name)
1555 1557
1556 1558 email_kwargs = {
1557 1559 'date': datetime.datetime.now(),
1558 1560 'updating_user': updating_user,
1559 1561
1560 1562 'pull_request': pull_request_obj,
1561 1563
1562 1564 'pull_request_target_repo': pr_target_repo,
1563 1565 'pull_request_target_repo_url': pr_target_repo_url,
1564 1566
1565 1567 'pull_request_source_repo': pr_source_repo,
1566 1568 'pull_request_source_repo_url': pr_source_repo_url,
1567 1569
1568 1570 'pull_request_url': pr_url,
1569 1571
1570 1572 'ancestor_commit_id': ancestor_commit_id,
1571 1573 'added_commits': commit_changes.added,
1572 1574 'removed_commits': commit_changes.removed,
1573 1575 'changed_files': changed_files,
1574 1576 'added_files': file_changes.added,
1575 1577 'modified_files': file_changes.modified,
1576 1578 'removed_files': file_changes.removed,
1577 1579 'thread_ids': [pr_url],
1578 1580 }
1579 1581
1580 1582 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1581 1583 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1582 1584
1583 1585 # create notification objects, and emails
1584 1586 NotificationModel().create(
1585 1587 created_by=updating_user,
1586 1588 notification_subject=subject,
1587 1589 notification_body=body_plaintext,
1588 1590 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1589 1591 recipients=recipients,
1590 1592 email_kwargs=email_kwargs,
1591 1593 )
1592 1594
1593 1595 def delete(self, pull_request, user=None):
1594 1596 if not user:
1595 1597 user = getattr(get_current_rhodecode_user(), 'username', None)
1596 1598
1597 1599 pull_request = self.__get_pull_request(pull_request)
1598 1600 old_data = pull_request.get_api_data(with_merge_state=False)
1599 1601 self._cleanup_merge_workspace(pull_request)
1600 1602 self._log_audit_action(
1601 1603 'repo.pull_request.delete', {'old_data': old_data},
1602 1604 user, pull_request)
1603 1605 Session().delete(pull_request)
1604 1606
1605 1607 def close_pull_request(self, pull_request, user):
1606 1608 pull_request = self.__get_pull_request(pull_request)
1607 1609 self._cleanup_merge_workspace(pull_request)
1608 1610 pull_request.status = PullRequest.STATUS_CLOSED
1609 1611 pull_request.updated_on = datetime.datetime.now()
1610 1612 Session().add(pull_request)
1611 1613 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1612 1614
1613 1615 pr_data = pull_request.get_api_data(with_merge_state=False)
1614 1616 self._log_audit_action(
1615 1617 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1616 1618
1617 1619 def close_pull_request_with_comment(
1618 1620 self, pull_request, user, repo, message=None, auth_user=None):
1619 1621
1620 1622 pull_request_review_status = pull_request.calculated_review_status()
1621 1623
1622 1624 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1623 1625 # approved only if we have voting consent
1624 1626 status = ChangesetStatus.STATUS_APPROVED
1625 1627 else:
1626 1628 status = ChangesetStatus.STATUS_REJECTED
1627 1629 status_lbl = ChangesetStatus.get_status_lbl(status)
1628 1630
1629 1631 default_message = (
1630 1632 'Closing with status change {transition_icon} {status}.'
1631 1633 ).format(transition_icon='>', status=status_lbl)
1632 1634 text = message or default_message
1633 1635
1634 1636 # create a comment, and link it to new status
1635 1637 comment = CommentsModel().create(
1636 1638 text=text,
1637 1639 repo=repo.repo_id,
1638 1640 user=user.user_id,
1639 1641 pull_request=pull_request.pull_request_id,
1640 1642 status_change=status_lbl,
1641 1643 status_change_type=status,
1642 1644 closing_pr=True,
1643 1645 auth_user=auth_user,
1644 1646 )
1645 1647
1646 1648 # calculate old status before we change it
1647 1649 old_calculated_status = pull_request.calculated_review_status()
1648 1650 ChangesetStatusModel().set_status(
1649 1651 repo.repo_id,
1650 1652 status,
1651 1653 user.user_id,
1652 1654 comment=comment,
1653 1655 pull_request=pull_request.pull_request_id
1654 1656 )
1655 1657
1656 1658 Session().flush()
1657 1659
1658 1660 self.trigger_pull_request_hook(pull_request, user, 'comment',
1659 1661 data={'comment': comment})
1660 1662
1661 1663 # we now calculate the status of pull request again, and based on that
1662 1664 # calculation trigger status change. This might happen in cases
1663 1665 # that non-reviewer admin closes a pr, which means his vote doesn't
1664 1666 # change the status, while if he's a reviewer this might change it.
1665 1667 calculated_status = pull_request.calculated_review_status()
1666 1668 if old_calculated_status != calculated_status:
1667 1669 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1668 1670 data={'status': calculated_status})
1669 1671
1670 1672 # finally close the PR
1671 1673 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1672 1674
1673 1675 return comment, status
1674 1676
1675 1677 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1676 1678 _ = translator or get_current_request().translate
1677 1679
1678 1680 if not self._is_merge_enabled(pull_request):
1679 1681 return None, False, _('Server-side pull request merging is disabled.')
1680 1682
1681 1683 if pull_request.is_closed():
1682 1684 return None, False, _('This pull request is closed.')
1683 1685
1684 1686 merge_possible, msg = self._check_repo_requirements(
1685 1687 target=pull_request.target_repo, source=pull_request.source_repo,
1686 1688 translator=_)
1687 1689 if not merge_possible:
1688 1690 return None, merge_possible, msg
1689 1691
1690 1692 try:
1691 1693 merge_response = self._try_merge(
1692 1694 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1693 1695 log.debug("Merge response: %s", merge_response)
1694 1696 return merge_response, merge_response.possible, merge_response.merge_status_message
1695 1697 except NotImplementedError:
1696 1698 return None, False, _('Pull request merging is not supported.')
1697 1699
1698 1700 def _check_repo_requirements(self, target, source, translator):
1699 1701 """
1700 1702 Check if `target` and `source` have compatible requirements.
1701 1703
1702 1704 Currently this is just checking for largefiles.
1703 1705 """
1704 1706 _ = translator
1705 1707 target_has_largefiles = self._has_largefiles(target)
1706 1708 source_has_largefiles = self._has_largefiles(source)
1707 1709 merge_possible = True
1708 1710 message = u''
1709 1711
1710 1712 if target_has_largefiles != source_has_largefiles:
1711 1713 merge_possible = False
1712 1714 if source_has_largefiles:
1713 1715 message = _(
1714 1716 'Target repository large files support is disabled.')
1715 1717 else:
1716 1718 message = _(
1717 1719 'Source repository large files support is disabled.')
1718 1720
1719 1721 return merge_possible, message
1720 1722
1721 1723 def _has_largefiles(self, repo):
1722 1724 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1723 1725 'extensions', 'largefiles')
1724 1726 return largefiles_ui and largefiles_ui[0].active
1725 1727
1726 1728 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1727 1729 """
1728 1730 Try to merge the pull request and return the merge status.
1729 1731 """
1730 1732 log.debug(
1731 1733 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1732 1734 pull_request.pull_request_id, force_shadow_repo_refresh)
1733 1735 target_vcs = pull_request.target_repo.scm_instance()
1734 1736 # Refresh the target reference.
1735 1737 try:
1736 1738 target_ref = self._refresh_reference(
1737 1739 pull_request.target_ref_parts, target_vcs)
1738 1740 except CommitDoesNotExistError:
1739 1741 merge_state = MergeResponse(
1740 1742 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1741 1743 metadata={'target_ref': pull_request.target_ref_parts})
1742 1744 return merge_state
1743 1745
1744 1746 target_locked = pull_request.target_repo.locked
1745 1747 if target_locked and target_locked[0]:
1746 1748 locked_by = 'user:{}'.format(target_locked[0])
1747 1749 log.debug("The target repository is locked by %s.", locked_by)
1748 1750 merge_state = MergeResponse(
1749 1751 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1750 1752 metadata={'locked_by': locked_by})
1751 1753 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1752 1754 pull_request, target_ref):
1753 1755 log.debug("Refreshing the merge status of the repository.")
1754 1756 merge_state = self._refresh_merge_state(
1755 1757 pull_request, target_vcs, target_ref)
1756 1758 else:
1757 1759 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1758 1760 metadata = {
1759 1761 'unresolved_files': '',
1760 1762 'target_ref': pull_request.target_ref_parts,
1761 1763 'source_ref': pull_request.source_ref_parts,
1762 1764 }
1763 1765 if pull_request.last_merge_metadata:
1764 1766 metadata.update(pull_request.last_merge_metadata_parsed)
1765 1767
1766 1768 if not possible and target_ref.type == 'branch':
1767 1769 # NOTE(marcink): case for mercurial multiple heads on branch
1768 1770 heads = target_vcs._heads(target_ref.name)
1769 1771 if len(heads) != 1:
1770 1772 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1771 1773 metadata.update({
1772 1774 'heads': heads
1773 1775 })
1774 1776
1775 1777 merge_state = MergeResponse(
1776 1778 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1777 1779
1778 1780 return merge_state
1779 1781
1780 1782 def _refresh_reference(self, reference, vcs_repository):
1781 1783 if reference.type in self.UPDATABLE_REF_TYPES:
1782 1784 name_or_id = reference.name
1783 1785 else:
1784 1786 name_or_id = reference.commit_id
1785 1787
1786 1788 refreshed_commit = vcs_repository.get_commit(name_or_id)
1787 1789 refreshed_reference = Reference(
1788 1790 reference.type, reference.name, refreshed_commit.raw_id)
1789 1791 return refreshed_reference
1790 1792
1791 1793 def _needs_merge_state_refresh(self, pull_request, target_reference):
1792 1794 return not(
1793 1795 pull_request.revisions and
1794 1796 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1795 1797 target_reference.commit_id == pull_request._last_merge_target_rev)
1796 1798
1797 1799 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1798 1800 workspace_id = self._workspace_id(pull_request)
1799 1801 source_vcs = pull_request.source_repo.scm_instance()
1800 1802 repo_id = pull_request.target_repo.repo_id
1801 1803 use_rebase = self._use_rebase_for_merging(pull_request)
1802 1804 close_branch = self._close_branch_before_merging(pull_request)
1803 1805 merge_state = target_vcs.merge(
1804 1806 repo_id, workspace_id,
1805 1807 target_reference, source_vcs, pull_request.source_ref_parts,
1806 1808 dry_run=True, use_rebase=use_rebase,
1807 1809 close_branch=close_branch)
1808 1810
1809 1811 # Do not store the response if there was an unknown error.
1810 1812 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1811 1813 pull_request._last_merge_source_rev = \
1812 1814 pull_request.source_ref_parts.commit_id
1813 1815 pull_request._last_merge_target_rev = target_reference.commit_id
1814 1816 pull_request.last_merge_status = merge_state.failure_reason
1815 1817 pull_request.last_merge_metadata = merge_state.metadata
1816 1818
1817 1819 pull_request.shadow_merge_ref = merge_state.merge_ref
1818 1820 Session().add(pull_request)
1819 1821 Session().commit()
1820 1822
1821 1823 return merge_state
1822 1824
1823 1825 def _workspace_id(self, pull_request):
1824 1826 workspace_id = 'pr-%s' % pull_request.pull_request_id
1825 1827 return workspace_id
1826 1828
1827 1829 def generate_repo_data(self, repo, commit_id=None, branch=None,
1828 1830 bookmark=None, translator=None):
1829 1831 from rhodecode.model.repo import RepoModel
1830 1832
1831 1833 all_refs, selected_ref = \
1832 1834 self._get_repo_pullrequest_sources(
1833 1835 repo.scm_instance(), commit_id=commit_id,
1834 1836 branch=branch, bookmark=bookmark, translator=translator)
1835 1837
1836 1838 refs_select2 = []
1837 1839 for element in all_refs:
1838 1840 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1839 1841 refs_select2.append({'text': element[1], 'children': children})
1840 1842
1841 1843 return {
1842 1844 'user': {
1843 1845 'user_id': repo.user.user_id,
1844 1846 'username': repo.user.username,
1845 1847 'firstname': repo.user.first_name,
1846 1848 'lastname': repo.user.last_name,
1847 1849 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1848 1850 },
1849 1851 'name': repo.repo_name,
1850 1852 'link': RepoModel().get_url(repo),
1851 1853 'description': h.chop_at_smart(repo.description_safe, '\n'),
1852 1854 'refs': {
1853 1855 'all_refs': all_refs,
1854 1856 'selected_ref': selected_ref,
1855 1857 'select2_refs': refs_select2
1856 1858 }
1857 1859 }
1858 1860
1859 1861 def generate_pullrequest_title(self, source, source_ref, target):
1860 1862 return u'{source}#{at_ref} to {target}'.format(
1861 1863 source=source,
1862 1864 at_ref=source_ref,
1863 1865 target=target,
1864 1866 )
1865 1867
1866 1868 def _cleanup_merge_workspace(self, pull_request):
1867 1869 # Merging related cleanup
1868 1870 repo_id = pull_request.target_repo.repo_id
1869 1871 target_scm = pull_request.target_repo.scm_instance()
1870 1872 workspace_id = self._workspace_id(pull_request)
1871 1873
1872 1874 try:
1873 1875 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1874 1876 except NotImplementedError:
1875 1877 pass
1876 1878
1877 1879 def _get_repo_pullrequest_sources(
1878 1880 self, repo, commit_id=None, branch=None, bookmark=None,
1879 1881 translator=None):
1880 1882 """
1881 1883 Return a structure with repo's interesting commits, suitable for
1882 1884 the selectors in pullrequest controller
1883 1885
1884 1886 :param commit_id: a commit that must be in the list somehow
1885 1887 and selected by default
1886 1888 :param branch: a branch that must be in the list and selected
1887 1889 by default - even if closed
1888 1890 :param bookmark: a bookmark that must be in the list and selected
1889 1891 """
1890 1892 _ = translator or get_current_request().translate
1891 1893
1892 1894 commit_id = safe_str(commit_id) if commit_id else None
1893 1895 branch = safe_unicode(branch) if branch else None
1894 1896 bookmark = safe_unicode(bookmark) if bookmark else None
1895 1897
1896 1898 selected = None
1897 1899
1898 1900 # order matters: first source that has commit_id in it will be selected
1899 1901 sources = []
1900 1902 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1901 1903 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1902 1904
1903 1905 if commit_id:
1904 1906 ref_commit = (h.short_id(commit_id), commit_id)
1905 1907 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1906 1908
1907 1909 sources.append(
1908 1910 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1909 1911 )
1910 1912
1911 1913 groups = []
1912 1914
1913 1915 for group_key, ref_list, group_name, match in sources:
1914 1916 group_refs = []
1915 1917 for ref_name, ref_id in ref_list:
1916 1918 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1917 1919 group_refs.append((ref_key, ref_name))
1918 1920
1919 1921 if not selected:
1920 1922 if set([commit_id, match]) & set([ref_id, ref_name]):
1921 1923 selected = ref_key
1922 1924
1923 1925 if group_refs:
1924 1926 groups.append((group_refs, group_name))
1925 1927
1926 1928 if not selected:
1927 1929 ref = commit_id or branch or bookmark
1928 1930 if ref:
1929 1931 raise CommitDoesNotExistError(
1930 1932 u'No commit refs could be found matching: {}'.format(ref))
1931 1933 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1932 1934 selected = u'branch:{}:{}'.format(
1933 1935 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1934 1936 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1935 1937 )
1936 1938 elif repo.commit_ids:
1937 1939 # make the user select in this case
1938 1940 selected = None
1939 1941 else:
1940 1942 raise EmptyRepositoryError()
1941 1943 return groups, selected
1942 1944
1943 1945 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1944 1946 hide_whitespace_changes, diff_context):
1945 1947
1946 1948 return self._get_diff_from_pr_or_version(
1947 1949 source_repo, source_ref_id, target_ref_id,
1948 1950 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1949 1951
1950 1952 def _get_diff_from_pr_or_version(
1951 1953 self, source_repo, source_ref_id, target_ref_id,
1952 1954 hide_whitespace_changes, diff_context):
1953 1955
1954 1956 target_commit = source_repo.get_commit(
1955 1957 commit_id=safe_str(target_ref_id))
1956 1958 source_commit = source_repo.get_commit(
1957 1959 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1958 1960 if isinstance(source_repo, Repository):
1959 1961 vcs_repo = source_repo.scm_instance()
1960 1962 else:
1961 1963 vcs_repo = source_repo
1962 1964
1963 1965 # TODO: johbo: In the context of an update, we cannot reach
1964 1966 # the old commit anymore with our normal mechanisms. It needs
1965 1967 # some sort of special support in the vcs layer to avoid this
1966 1968 # workaround.
1967 1969 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1968 1970 vcs_repo.alias == 'git'):
1969 1971 source_commit.raw_id = safe_str(source_ref_id)
1970 1972
1971 1973 log.debug('calculating diff between '
1972 1974 'source_ref:%s and target_ref:%s for repo `%s`',
1973 1975 target_ref_id, source_ref_id,
1974 1976 safe_unicode(vcs_repo.path))
1975 1977
1976 1978 vcs_diff = vcs_repo.get_diff(
1977 1979 commit1=target_commit, commit2=source_commit,
1978 1980 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1979 1981 return vcs_diff
1980 1982
1981 1983 def _is_merge_enabled(self, pull_request):
1982 1984 return self._get_general_setting(
1983 1985 pull_request, 'rhodecode_pr_merge_enabled')
1984 1986
1985 1987 def _use_rebase_for_merging(self, pull_request):
1986 1988 repo_type = pull_request.target_repo.repo_type
1987 1989 if repo_type == 'hg':
1988 1990 return self._get_general_setting(
1989 1991 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1990 1992 elif repo_type == 'git':
1991 1993 return self._get_general_setting(
1992 1994 pull_request, 'rhodecode_git_use_rebase_for_merging')
1993 1995
1994 1996 return False
1995 1997
1996 1998 def _user_name_for_merging(self, pull_request, user):
1997 1999 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1998 2000 if env_user_name_attr and hasattr(user, env_user_name_attr):
1999 2001 user_name_attr = env_user_name_attr
2000 2002 else:
2001 2003 user_name_attr = 'short_contact'
2002 2004
2003 2005 user_name = getattr(user, user_name_attr)
2004 2006 return user_name
2005 2007
2006 2008 def _close_branch_before_merging(self, pull_request):
2007 2009 repo_type = pull_request.target_repo.repo_type
2008 2010 if repo_type == 'hg':
2009 2011 return self._get_general_setting(
2010 2012 pull_request, 'rhodecode_hg_close_branch_before_merging')
2011 2013 elif repo_type == 'git':
2012 2014 return self._get_general_setting(
2013 2015 pull_request, 'rhodecode_git_close_branch_before_merging')
2014 2016
2015 2017 return False
2016 2018
2017 2019 def _get_general_setting(self, pull_request, settings_key, default=False):
2018 2020 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2019 2021 settings = settings_model.get_general_settings()
2020 2022 return settings.get(settings_key, default)
2021 2023
2022 2024 def _log_audit_action(self, action, action_data, user, pull_request):
2023 2025 audit_logger.store(
2024 2026 action=action,
2025 2027 action_data=action_data,
2026 2028 user=user,
2027 2029 repo=pull_request.target_repo)
2028 2030
2029 2031 def get_reviewer_functions(self):
2030 2032 """
2031 2033 Fetches functions for validation and fetching default reviewers.
2032 2034 If available we use the EE package, else we fallback to CE
2033 2035 package functions
2034 2036 """
2035 2037 try:
2036 2038 from rc_reviewers.utils import get_default_reviewers_data
2037 2039 from rc_reviewers.utils import validate_default_reviewers
2038 2040 from rc_reviewers.utils import validate_observers
2039 2041 except ImportError:
2040 2042 from rhodecode.apps.repository.utils import get_default_reviewers_data
2041 2043 from rhodecode.apps.repository.utils import validate_default_reviewers
2042 2044 from rhodecode.apps.repository.utils import validate_observers
2043 2045
2044 2046 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2045 2047
2046 2048
2047 2049 class MergeCheck(object):
2048 2050 """
2049 2051 Perform Merge Checks and returns a check object which stores information
2050 2052 about merge errors, and merge conditions
2051 2053 """
2052 2054 TODO_CHECK = 'todo'
2053 2055 PERM_CHECK = 'perm'
2054 2056 REVIEW_CHECK = 'review'
2055 2057 MERGE_CHECK = 'merge'
2056 2058 WIP_CHECK = 'wip'
2057 2059
2058 2060 def __init__(self):
2059 2061 self.review_status = None
2060 2062 self.merge_possible = None
2061 2063 self.merge_msg = ''
2062 2064 self.merge_response = None
2063 2065 self.failed = None
2064 2066 self.errors = []
2065 2067 self.error_details = OrderedDict()
2066 2068 self.source_commit = AttributeDict()
2067 2069 self.target_commit = AttributeDict()
2068 2070
2069 2071 def __repr__(self):
2070 2072 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2071 2073 self.merge_possible, self.failed, self.errors)
2072 2074
2073 2075 def push_error(self, error_type, message, error_key, details):
2074 2076 self.failed = True
2075 2077 self.errors.append([error_type, message])
2076 2078 self.error_details[error_key] = dict(
2077 2079 details=details,
2078 2080 error_type=error_type,
2079 2081 message=message
2080 2082 )
2081 2083
2082 2084 @classmethod
2083 2085 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2084 2086 force_shadow_repo_refresh=False):
2085 2087 _ = translator
2086 2088 merge_check = cls()
2087 2089
2088 2090 # title has WIP:
2089 2091 if pull_request.work_in_progress:
2090 2092 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2091 2093
2092 2094 msg = _('WIP marker in title prevents from accidental merge.')
2093 2095 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2094 2096 if fail_early:
2095 2097 return merge_check
2096 2098
2097 2099 # permissions to merge
2098 2100 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2099 2101 if not user_allowed_to_merge:
2100 2102 log.debug("MergeCheck: cannot merge, approval is pending.")
2101 2103
2102 2104 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2103 2105 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2104 2106 if fail_early:
2105 2107 return merge_check
2106 2108
2107 2109 # permission to merge into the target branch
2108 2110 target_commit_id = pull_request.target_ref_parts.commit_id
2109 2111 if pull_request.target_ref_parts.type == 'branch':
2110 2112 branch_name = pull_request.target_ref_parts.name
2111 2113 else:
2112 2114 # for mercurial we can always figure out the branch from the commit
2113 2115 # in case of bookmark
2114 2116 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2115 2117 branch_name = target_commit.branch
2116 2118
2117 2119 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2118 2120 pull_request.target_repo.repo_name, branch_name)
2119 2121 if branch_perm and branch_perm == 'branch.none':
2120 2122 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2121 2123 branch_name, rule)
2122 2124 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2123 2125 if fail_early:
2124 2126 return merge_check
2125 2127
2126 2128 # review status, must be always present
2127 2129 review_status = pull_request.calculated_review_status()
2128 2130 merge_check.review_status = review_status
2129 2131
2130 2132 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2131 2133 if not status_approved:
2132 2134 log.debug("MergeCheck: cannot merge, approval is pending.")
2133 2135
2134 2136 msg = _('Pull request reviewer approval is pending.')
2135 2137
2136 2138 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2137 2139
2138 2140 if fail_early:
2139 2141 return merge_check
2140 2142
2141 2143 # left over TODOs
2142 2144 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2143 2145 if todos:
2144 2146 log.debug("MergeCheck: cannot merge, {} "
2145 2147 "unresolved TODOs left.".format(len(todos)))
2146 2148
2147 2149 if len(todos) == 1:
2148 2150 msg = _('Cannot merge, {} TODO still not resolved.').format(
2149 2151 len(todos))
2150 2152 else:
2151 2153 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2152 2154 len(todos))
2153 2155
2154 2156 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2155 2157
2156 2158 if fail_early:
2157 2159 return merge_check
2158 2160
2159 2161 # merge possible, here is the filesystem simulation + shadow repo
2160 2162 merge_response, merge_status, msg = PullRequestModel().merge_status(
2161 2163 pull_request, translator=translator,
2162 2164 force_shadow_repo_refresh=force_shadow_repo_refresh)
2163 2165
2164 2166 merge_check.merge_possible = merge_status
2165 2167 merge_check.merge_msg = msg
2166 2168 merge_check.merge_response = merge_response
2167 2169
2168 2170 source_ref_id = pull_request.source_ref_parts.commit_id
2169 2171 target_ref_id = pull_request.target_ref_parts.commit_id
2170 2172
2171 2173 try:
2172 2174 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2173 2175 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2174 2176 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2175 2177 merge_check.source_commit.current_raw_id = source_commit.raw_id
2176 2178 merge_check.source_commit.previous_raw_id = source_ref_id
2177 2179
2178 2180 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2179 2181 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2180 2182 merge_check.target_commit.current_raw_id = target_commit.raw_id
2181 2183 merge_check.target_commit.previous_raw_id = target_ref_id
2182 2184 except (SourceRefMissing, TargetRefMissing):
2183 2185 pass
2184 2186
2185 2187 if not merge_status:
2186 2188 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2187 2189 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2188 2190
2189 2191 if fail_early:
2190 2192 return merge_check
2191 2193
2192 2194 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2193 2195 return merge_check
2194 2196
2195 2197 @classmethod
2196 2198 def get_merge_conditions(cls, pull_request, translator):
2197 2199 _ = translator
2198 2200 merge_details = {}
2199 2201
2200 2202 model = PullRequestModel()
2201 2203 use_rebase = model._use_rebase_for_merging(pull_request)
2202 2204
2203 2205 if use_rebase:
2204 2206 merge_details['merge_strategy'] = dict(
2205 2207 details={},
2206 2208 message=_('Merge strategy: rebase')
2207 2209 )
2208 2210 else:
2209 2211 merge_details['merge_strategy'] = dict(
2210 2212 details={},
2211 2213 message=_('Merge strategy: explicit merge commit')
2212 2214 )
2213 2215
2214 2216 close_branch = model._close_branch_before_merging(pull_request)
2215 2217 if close_branch:
2216 2218 repo_type = pull_request.target_repo.repo_type
2217 2219 close_msg = ''
2218 2220 if repo_type == 'hg':
2219 2221 close_msg = _('Source branch will be closed before the merge.')
2220 2222 elif repo_type == 'git':
2221 2223 close_msg = _('Source branch will be deleted after the merge.')
2222 2224
2223 2225 merge_details['close_branch'] = dict(
2224 2226 details={},
2225 2227 message=close_msg
2226 2228 )
2227 2229
2228 2230 return merge_details
2229 2231
2230 2232
2231 2233 ChangeTuple = collections.namedtuple(
2232 2234 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2233 2235
2234 2236 FileChangeTuple = collections.namedtuple(
2235 2237 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,640 +1,642 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 };formatSelection:
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 312
313 313 var commitElements = data['commits'];
314 314 var files = data['files'];
315 315 var added = data['stats'][0]
316 316 var deleted = data['stats'][1]
317 317 var commonAncestorId = data['ancestor'];
318 318 var _sourceRefType = sourceRef()[0];
319 319 var _sourceRefName = sourceRef()[1];
320 320 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
321 321
322 322 var title = prTitleAndDesc[0];
323 323 var proposedDescription = prTitleAndDesc[1];
324 324
325 325 var useGeneratedTitle = (
326 326 $('#pullrequest_title').hasClass('autogenerated-title') ||
327 327 $('#pullrequest_title').val() === "");
328 328
329 329 if (title && useGeneratedTitle) {
330 330 // use generated title if we haven't specified our own
331 331 $('#pullrequest_title').val(title);
332 332 $('#pullrequest_title').addClass('autogenerated-title');
333 333
334 334 }
335 335
336 336 var useGeneratedDescription = (
337 337 !codeMirrorInstance._userDefinedValue ||
338 338 codeMirrorInstance.getValue() === "");
339 339
340 340 if (proposedDescription && useGeneratedDescription) {
341 341 // set proposed content, if we haven't defined our own,
342 342 // or we don't have description written
343 343 codeMirrorInstance._userDefinedValue = false; // reset state
344 344 codeMirrorInstance.setValue(proposedDescription);
345 345 }
346 346
347 347 // refresh our codeMirror so events kicks in and it's change aware
348 348 codeMirrorInstance.refresh();
349 349
350 350 var url_data = {
351 351 'repo_name': targetRepo(),
352 352 'target_repo': sourceRepo(),
353 353 'source_ref': targetRef()[2],
354 354 'source_ref_type': 'rev',
355 355 'target_ref': sourceRef()[2],
356 356 'target_ref_type': 'rev',
357 357 'merge': true,
358 358 '_': Date.now() // bypass browser caching
359 359 }; // gather the source/target ref and repo here
360 360 var url = pyroutes.url('repo_compare', url_data);
361 361
362 362 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
363 363 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
364 364
365
365 366 $.each(commitElements, function(idx, value) {
366 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
367 var commit_id = value["commit_id"]
368 msg += '<input type="hidden" name="revisions" value="{0}">'.format(commit_id);
367 369 });
368 370
369 371 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
370 372 msg += _ngettext(
371 373 'Compare summary: <strong>{0} commit</strong>',
372 374 'Compare summary: <strong>{0} commits</strong>',
373 375 commitElements.length).format(commitElements.length)
374 376
375 377 msg += '';
376 378 msg += _ngettext(
377 379 '<strong>, and {0} file</strong> changed.',
378 380 '<strong>, and {0} files</strong> changed.',
379 381 files.length).format(files.length)
380 382
381 383 msg += '\n Diff: <span class="op-added">{0} lines inserted</span>, <span class="op-deleted">{1} lines deleted </span>.'.format(added, deleted)
382 384
383 385 msg += '\n <a class="" id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
384 386
385 387 if (commitElements.length) {
386 388 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
387 389 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
388 390 }
389 391 else {
390 392 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
391 393 _gettext('There are no commits to merge.'));
392 394 prButtonLock(true, noCommitsMsg, 'compare');
393 395 }
394 396
395 397 //make both panels equal
396 398 $('.target-panel').height($('.source-panel').height())
397 399 };
398 400
399 401 reviewersController = new ReviewersController();
400 402 reviewersController.diffDataHandler = diffDataHandler;
401 403
402 404 var queryTargetRepo = function(self, query) {
403 405 // cache ALL results if query is empty
404 406 var cacheKey = query.term || '__';
405 407 var cachedData = self.cachedDataSource[cacheKey];
406 408
407 409 if (cachedData) {
408 410 query.callback({results: cachedData.results});
409 411 } else {
410 412 $.ajax({
411 413 url: pyroutes.url('pullrequest_repo_targets', {'repo_name': templateContext.repo_name}),
412 414 data: {query: query.term},
413 415 dataType: 'json',
414 416 type: 'GET',
415 417 success: function(data) {
416 418 self.cachedDataSource[cacheKey] = data;
417 419 query.callback({results: data.results});
418 420 },
419 421 error: function(jqXHR, textStatus, errorThrown) {
420 422 var prefix = "Error while fetching entries.\n"
421 423 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
422 424 ajaxErrorSwal(message);
423 425 }
424 426 });
425 427 }
426 428 };
427 429
428 430 var queryTargetRefs = function(initialData, query) {
429 431 var data = {results: []};
430 432 // filter initialData
431 433 $.each(initialData, function() {
432 434 var section = this.text;
433 435 var children = [];
434 436 $.each(this.children, function() {
435 437 if (query.term.length === 0 ||
436 438 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
437 439 children.push({'id': this.id, 'text': this.text})
438 440 }
439 441 });
440 442 data.results.push({'text': section, 'children': children})
441 443 });
442 444 query.callback({results: data.results});
443 445 };
444 446
445 447 var Select2Box = function(element, overrides) {
446 448 var globalDefaults = {
447 449 dropdownAutoWidth: true,
448 450 containerCssClass: "drop-menu",
449 451 dropdownCssClass: "drop-menu-dropdown"
450 452 };
451 453
452 454 var initSelect2 = function(defaultOptions) {
453 455 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
454 456 element.select2(options);
455 457 };
456 458
457 459 return {
458 460 initRef: function() {
459 461 var defaultOptions = {
460 462 minimumResultsForSearch: 5,
461 463 formatSelection: formatRefSelection
462 464 };
463 465
464 466 initSelect2(defaultOptions);
465 467 },
466 468
467 469 initRepo: function(defaultValue, readOnly) {
468 470 var defaultOptions = {
469 471 initSelection : function (element, callback) {
470 472 var data = {id: defaultValue, text: defaultValue};
471 473 callback(data);
472 474 }
473 475 };
474 476
475 477 initSelect2(defaultOptions);
476 478
477 479 element.select2('val', defaultSourceRepo);
478 480 if (readOnly === true) {
479 481 element.select2('readonly', true);
480 482 }
481 483 }
482 484 };
483 485 };
484 486
485 487 var initTargetRefs = function(refsData, selectedRef) {
486 488
487 489 Select2Box($targetRef, {
488 490 placeholder: "${_('Select commit reference')}",
489 491 query: function(query) {
490 492 queryTargetRefs(refsData, query);
491 493 },
492 494 initSelection : initRefSelection(selectedRef)
493 495 }).initRef();
494 496
495 497 if (!(selectedRef === undefined)) {
496 498 $targetRef.select2('val', selectedRef);
497 499 }
498 500 };
499 501
500 502 var targetRepoChanged = function(repoData) {
501 503 // generate new DESC of target repo displayed next to select
502 504
503 505 $('#target_repo_desc').html(repoData['description']);
504 506
505 507 var prLink = pyroutes.url('pullrequest_new', {'repo_name': repoData['name']});
506 508 var title = _gettext('Switch target repository with the source.')
507 509 $('#switch_base').html("<a class=\"tooltip\" title=\"{0}\" href=\"{1}\">Switch sides</a>".format(title, prLink))
508 510
509 511 // generate dynamic select2 for refs.
510 512 initTargetRefs(repoData['refs']['select2_refs'],
511 513 repoData['refs']['selected_ref']);
512 514
513 515 };
514 516
515 517 var sourceRefSelect2 = Select2Box($sourceRef, {
516 518 placeholder: "${_('Select commit reference')}",
517 519 query: function(query) {
518 520 var initialData = defaultSourceRepoData['refs']['select2_refs'];
519 521 queryTargetRefs(initialData, query)
520 522 },
521 523 initSelection: initRefSelection()
522 524 });
523 525
524 526 var sourceRepoSelect2 = Select2Box($sourceRepo, {
525 527 query: function(query) {}
526 528 });
527 529
528 530 var targetRepoSelect2 = Select2Box($targetRepo, {
529 531 cachedDataSource: {},
530 532 query: $.debounce(250, function(query) {
531 533 queryTargetRepo(this, query);
532 534 }),
533 535 formatResult: formatRepoResult
534 536 });
535 537
536 538 sourceRefSelect2.initRef();
537 539
538 540 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
539 541
540 542 targetRepoSelect2.initRepo(defaultTargetRepo, false);
541 543
542 544 $sourceRef.on('change', function(e){
543 545 reviewersController.loadDefaultReviewers(
544 546 sourceRepo(), sourceRef(), targetRepo(), targetRef());
545 547 });
546 548
547 549 $targetRef.on('change', function(e){
548 550 reviewersController.loadDefaultReviewers(
549 551 sourceRepo(), sourceRef(), targetRepo(), targetRef());
550 552 });
551 553
552 554 $targetRepo.on('change', function(e){
553 555 var repoName = $(this).val();
554 556 calculateContainerWidth();
555 557 $targetRef.select2('destroy');
556 558 $('#target_ref_loading').show();
557 559
558 560 $.ajax({
559 561 url: pyroutes.url('pullrequest_repo_refs',
560 562 {'repo_name': templateContext.repo_name, 'target_repo_name':repoName}),
561 563 data: {},
562 564 dataType: 'json',
563 565 type: 'GET',
564 566 success: function(data) {
565 567 $('#target_ref_loading').hide();
566 568 targetRepoChanged(data);
567 569 },
568 570 error: function(jqXHR, textStatus, errorThrown) {
569 571 var prefix = "Error while fetching entries.\n"
570 572 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
571 573 ajaxErrorSwal(message);
572 574 }
573 575 })
574 576
575 577 });
576 578
577 579 $pullRequestForm.on('submit', function(e){
578 580 // Flush changes into textarea
579 581 codeMirrorInstance.save();
580 582 prButtonLock(true, null, 'all');
581 583 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
582 584 });
583 585
584 586 prButtonLock(true, "${_('Please select source and target')}", 'all');
585 587
586 588 // auto-load on init, the target refs select2
587 589 calculateContainerWidth();
588 590 targetRepoChanged(defaultTargetRepoData);
589 591
590 592 $('#pullrequest_title').on('keyup', function(e){
591 593 $(this).removeClass('autogenerated-title');
592 594 });
593 595
594 596 % if c.default_source_ref:
595 597 // in case we have a pre-selected value, use it now
596 598 $sourceRef.select2('val', '${c.default_source_ref}');
597 599
598 600
599 601 // default reviewers / observers
600 602 reviewersController.loadDefaultReviewers(
601 603 sourceRepo(), sourceRef(), targetRepo(), targetRef());
602 604 % endif
603 605
604 606 ReviewerAutoComplete('#user', reviewersController);
605 607 ObserverAutoComplete('#observer', reviewersController);
606 608
607 609 // TODO, move this to another handler
608 610
609 611 var $reviewersBtn = $('#reviewers-btn');
610 612 var $reviewersContainer = $('#reviewers-container');
611 613
612 614 var $observersBtn = $('#observers-btn')
613 615 var $observersContainer = $('#observers-container');
614 616
615 617 $reviewersBtn.on('click', function (e) {
616 618
617 619 $observersContainer.hide();
618 620 $reviewersContainer.show();
619 621
620 622 $observersBtn.parent().removeClass('active');
621 623 $reviewersBtn.parent().addClass('active');
622 624 e.preventDefault();
623 625
624 626 })
625 627
626 628 $observersBtn.on('click', function (e) {
627 629
628 630 $reviewersContainer.hide();
629 631 $observersContainer.show();
630 632
631 633 $reviewersBtn.parent().removeClass('active');
632 634 $observersBtn.parent().addClass('active');
633 635 e.preventDefault();
634 636
635 637 })
636 638
637 639 });
638 640 </script>
639 641
640 642 </%def>
General Comments 0
You need to be logged in to leave comments. Login now