##// END OF EJS Templates
events: fix bug, 'url' was not in dict
dan -
r392:b65452d3 default
parent child Browse files
Show More
@@ -1,71 +1,71 b''
1 1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from datetime import datetime
20 20 from marshmallow import Schema, fields
21 21 from pyramid.threadlocal import get_current_request
22 22 from rhodecode.lib.utils2 import AttributeDict
23 23
24 24
25 25 # this is a user object to be used for events caused by the system (eg. shell)
26 26 SYSTEM_USER = AttributeDict(dict(
27 27 username='__SYSTEM__'
28 28 ))
29 29
30 30
31 31 class UserSchema(Schema):
32 32 """
33 33 Marshmallow schema for a user
34 34 """
35 35 username = fields.Str()
36 36
37 37
38 38 class RhodecodeEventSchema(Schema):
39 39 """
40 40 Marshmallow schema for a rhodecode event
41 41 """
42 42 utc_timestamp = fields.DateTime()
43 43 actor = fields.Nested(UserSchema)
44 44 actor_ip = fields.Str()
45 45 name = fields.Str()
46 46
47 47
48 48 class RhodecodeEvent(object):
49 49 """
50 50 Base event class for all Rhodecode events
51 51 """
52 52 MarshmallowSchema = RhodecodeEventSchema
53 53
54 54 def __init__(self):
55 55 self.request = get_current_request()
56 56 self.utc_timestamp = datetime.utcnow()
57 57
58 58 @property
59 59 def actor(self):
60 60 if self.request:
61 61 return self.request.user.get_instance()
62 62 return SYSTEM_USER
63 63
64 64 @property
65 65 def actor_ip(self):
66 66 if self.request:
67 67 return self.request.user.ip_addr
68 68 return '<no ip available>'
69 69
70 70 def as_dict(self):
71 return self.MarshmallowSchema().dump(self).data No newline at end of file
71 return self.MarshmallowSchema().dump(self).data
@@ -1,1153 +1,1154 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pylons.i18n.translation import lazy_ugettext
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 36 from rhodecode.lib.compat import OrderedDict
37 37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 38 from rhodecode.lib.markup_renderer import (
39 39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 40 from rhodecode.lib.utils import action_logger
41 41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 42 from rhodecode.lib.vcs.backends.base import (
43 43 Reference, MergeResponse, MergeFailureReason)
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, EmptyRepositoryError)
46 46 from rhodecode.model import BaseModel
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import ChangesetCommentsModel
49 49 from rhodecode.model.db import (
50 50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
51 51 PullRequestVersion, ChangesetComment)
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.notification import NotificationModel, \
54 54 EmailNotificationModel
55 55 from rhodecode.model.scm import ScmModel
56 56 from rhodecode.model.settings import VcsSettingsModel
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class PullRequestModel(BaseModel):
63 63
64 64 cls = PullRequest
65 65
66 66 DIFF_CONTEXT = 3
67 67
68 68 MERGE_STATUS_MESSAGES = {
69 69 MergeFailureReason.NONE: lazy_ugettext(
70 70 'This pull request can be automatically merged.'),
71 71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 72 'This pull request cannot be merged because of an unhandled'
73 73 ' exception.'),
74 74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 75 'This pull request cannot be merged because of conflicts.'),
76 76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 77 'This pull request could not be merged because push to target'
78 78 ' failed.'),
79 79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 80 'This pull request cannot be merged because the target is not a'
81 81 ' head.'),
82 82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 83 'This pull request cannot be merged because the source contains'
84 84 ' more branches than the target.'),
85 85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 86 'This pull request cannot be merged because the target has'
87 87 ' multiple heads.'),
88 88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 89 'This pull request cannot be merged because the target repository'
90 90 ' is locked.'),
91 91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 92 'This pull request cannot be merged because the target or the '
93 93 'source reference is missing.'),
94 94 }
95 95
96 96 def __get_pull_request(self, pull_request):
97 97 return self._get_instance(PullRequest, pull_request)
98 98
99 99 def _check_perms(self, perms, pull_request, user, api=False):
100 100 if not api:
101 101 return h.HasRepoPermissionAny(*perms)(
102 102 user=user, repo_name=pull_request.target_repo.repo_name)
103 103 else:
104 104 return h.HasRepoPermissionAnyApi(*perms)(
105 105 user=user, repo_name=pull_request.target_repo.repo_name)
106 106
107 107 def check_user_read(self, pull_request, user, api=False):
108 108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 109 return self._check_perms(_perms, pull_request, user, api)
110 110
111 111 def check_user_merge(self, pull_request, user, api=False):
112 112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 113 return self._check_perms(_perms, pull_request, user, api)
114 114
115 115 def check_user_update(self, pull_request, user, api=False):
116 116 owner = user.user_id == pull_request.user_id
117 117 return self.check_user_merge(pull_request, user, api) or owner
118 118
119 119 def check_user_change_status(self, pull_request, user, api=False):
120 120 reviewer = user.user_id in [x.user_id for x in
121 121 pull_request.reviewers]
122 122 return self.check_user_update(pull_request, user, api) or reviewer
123 123
124 124 def get(self, pull_request):
125 125 return self.__get_pull_request(pull_request)
126 126
127 127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 128 opened_by=None, order_by=None,
129 129 order_dir='desc'):
130 130 repo = self._get_repo(repo_name)
131 131 q = PullRequest.query()
132 132 # source or target
133 133 if source:
134 134 q = q.filter(PullRequest.source_repo == repo)
135 135 else:
136 136 q = q.filter(PullRequest.target_repo == repo)
137 137
138 138 # closed,opened
139 139 if statuses:
140 140 q = q.filter(PullRequest.status.in_(statuses))
141 141
142 142 # opened by filter
143 143 if opened_by:
144 144 q = q.filter(PullRequest.user_id.in_(opened_by))
145 145
146 146 if order_by:
147 147 order_map = {
148 148 'name_raw': PullRequest.pull_request_id,
149 149 'title': PullRequest.title,
150 150 'updated_on_raw': PullRequest.updated_on
151 151 }
152 152 if order_dir == 'asc':
153 153 q = q.order_by(order_map[order_by].asc())
154 154 else:
155 155 q = q.order_by(order_map[order_by].desc())
156 156
157 157 return q
158 158
159 159 def count_all(self, repo_name, source=False, statuses=None,
160 160 opened_by=None):
161 161 """
162 162 Count the number of pull requests for a specific repository.
163 163
164 164 :param repo_name: target or source repo
165 165 :param source: boolean flag to specify if repo_name refers to source
166 166 :param statuses: list of pull request statuses
167 167 :param opened_by: author user of the pull request
168 168 :returns: int number of pull requests
169 169 """
170 170 q = self._prepare_get_all_query(
171 171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172 172
173 173 return q.count()
174 174
175 175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 176 offset=0, length=None, order_by=None, order_dir='desc'):
177 177 """
178 178 Get all pull requests for a specific repository.
179 179
180 180 :param repo_name: target or source repo
181 181 :param source: boolean flag to specify if repo_name refers to source
182 182 :param statuses: list of pull request statuses
183 183 :param opened_by: author user of the pull request
184 184 :param offset: pagination offset
185 185 :param length: length of returned list
186 186 :param order_by: order of the returned list
187 187 :param order_dir: 'asc' or 'desc' ordering direction
188 188 :returns: list of pull requests
189 189 """
190 190 q = self._prepare_get_all_query(
191 191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 192 order_by=order_by, order_dir=order_dir)
193 193
194 194 if length:
195 195 pull_requests = q.limit(length).offset(offset).all()
196 196 else:
197 197 pull_requests = q.all()
198 198
199 199 return pull_requests
200 200
201 201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 202 opened_by=None):
203 203 """
204 204 Count the number of pull requests for a specific repository that are
205 205 awaiting review.
206 206
207 207 :param repo_name: target or source repo
208 208 :param source: boolean flag to specify if repo_name refers to source
209 209 :param statuses: list of pull request statuses
210 210 :param opened_by: author user of the pull request
211 211 :returns: int number of pull requests
212 212 """
213 213 pull_requests = self.get_awaiting_review(
214 214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215 215
216 216 return len(pull_requests)
217 217
218 218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 219 opened_by=None, offset=0, length=None,
220 220 order_by=None, order_dir='desc'):
221 221 """
222 222 Get all pull requests for a specific repository that are awaiting
223 223 review.
224 224
225 225 :param repo_name: target or source repo
226 226 :param source: boolean flag to specify if repo_name refers to source
227 227 :param statuses: list of pull request statuses
228 228 :param opened_by: author user of the pull request
229 229 :param offset: pagination offset
230 230 :param length: length of returned list
231 231 :param order_by: order of the returned list
232 232 :param order_dir: 'asc' or 'desc' ordering direction
233 233 :returns: list of pull requests
234 234 """
235 235 pull_requests = self.get_all(
236 236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 237 order_by=order_by, order_dir=order_dir)
238 238
239 239 _filtered_pull_requests = []
240 240 for pr in pull_requests:
241 241 status = pr.calculated_review_status()
242 242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 244 _filtered_pull_requests.append(pr)
245 245 if length:
246 246 return _filtered_pull_requests[offset:offset+length]
247 247 else:
248 248 return _filtered_pull_requests
249 249
250 250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 251 opened_by=None, user_id=None):
252 252 """
253 253 Count the number of pull requests for a specific repository that are
254 254 awaiting review from a specific user.
255 255
256 256 :param repo_name: target or source repo
257 257 :param source: boolean flag to specify if repo_name refers to source
258 258 :param statuses: list of pull request statuses
259 259 :param opened_by: author user of the pull request
260 260 :param user_id: reviewer user of the pull request
261 261 :returns: int number of pull requests
262 262 """
263 263 pull_requests = self.get_awaiting_my_review(
264 264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 265 user_id=user_id)
266 266
267 267 return len(pull_requests)
268 268
269 269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 270 opened_by=None, user_id=None, offset=0,
271 271 length=None, order_by=None, order_dir='desc'):
272 272 """
273 273 Get all pull requests for a specific repository that are awaiting
274 274 review from a specific user.
275 275
276 276 :param repo_name: target or source repo
277 277 :param source: boolean flag to specify if repo_name refers to source
278 278 :param statuses: list of pull request statuses
279 279 :param opened_by: author user of the pull request
280 280 :param user_id: reviewer user of the pull request
281 281 :param offset: pagination offset
282 282 :param length: length of returned list
283 283 :param order_by: order of the returned list
284 284 :param order_dir: 'asc' or 'desc' ordering direction
285 285 :returns: list of pull requests
286 286 """
287 287 pull_requests = self.get_all(
288 288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 289 order_by=order_by, order_dir=order_dir)
290 290
291 291 _my = PullRequestModel().get_not_reviewed(user_id)
292 292 my_participation = []
293 293 for pr in pull_requests:
294 294 if pr in _my:
295 295 my_participation.append(pr)
296 296 _filtered_pull_requests = my_participation
297 297 if length:
298 298 return _filtered_pull_requests[offset:offset+length]
299 299 else:
300 300 return _filtered_pull_requests
301 301
302 302 def get_not_reviewed(self, user_id):
303 303 return [
304 304 x.pull_request for x in PullRequestReviewers.query().filter(
305 305 PullRequestReviewers.user_id == user_id).all()
306 306 ]
307 307
308 308 def get_versions(self, pull_request):
309 309 """
310 310 returns version of pull request sorted by ID descending
311 311 """
312 312 return PullRequestVersion.query()\
313 313 .filter(PullRequestVersion.pull_request == pull_request)\
314 314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 315 .all()
316 316
317 317 def create(self, created_by, source_repo, source_ref, target_repo,
318 318 target_ref, revisions, reviewers, title, description=None):
319 319 created_by_user = self._get_user(created_by)
320 320 source_repo = self._get_repo(source_repo)
321 321 target_repo = self._get_repo(target_repo)
322 322
323 323 pull_request = PullRequest()
324 324 pull_request.source_repo = source_repo
325 325 pull_request.source_ref = source_ref
326 326 pull_request.target_repo = target_repo
327 327 pull_request.target_ref = target_ref
328 328 pull_request.revisions = revisions
329 329 pull_request.title = title
330 330 pull_request.description = description
331 331 pull_request.author = created_by_user
332 332
333 333 Session().add(pull_request)
334 334 Session().flush()
335 335
336 336 # members / reviewers
337 337 for user_id in set(reviewers):
338 338 user = self._get_user(user_id)
339 339 reviewer = PullRequestReviewers(user, pull_request)
340 340 Session().add(reviewer)
341 341
342 342 # Set approval status to "Under Review" for all commits which are
343 343 # part of this pull request.
344 344 ChangesetStatusModel().set_status(
345 345 repo=target_repo,
346 346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 347 user=created_by_user,
348 348 pull_request=pull_request
349 349 )
350 350
351 351 self.notify_reviewers(pull_request, reviewers)
352 352 self._trigger_pull_request_hook(
353 353 pull_request, created_by_user, 'create')
354 354
355 355 return pull_request
356 356
357 357 def _trigger_pull_request_hook(self, pull_request, user, action):
358 358 pull_request = self.__get_pull_request(pull_request)
359 359 target_scm = pull_request.target_repo.scm_instance()
360 360 if action == 'create':
361 361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 362 elif action == 'merge':
363 363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 364 elif action == 'close':
365 365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 366 elif action == 'review_status_change':
367 367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 368 elif action == 'update':
369 369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 370 else:
371 371 return
372 372
373 373 trigger_hook(
374 374 username=user.username,
375 375 repo_name=pull_request.target_repo.repo_name,
376 376 repo_alias=target_scm.alias,
377 377 pull_request=pull_request)
378 378
379 379 def _get_commit_ids(self, pull_request):
380 380 """
381 381 Return the commit ids of the merged pull request.
382 382
383 383 This method is not dealing correctly yet with the lack of autoupdates
384 384 nor with the implicit target updates.
385 385 For example: if a commit in the source repo is already in the target it
386 386 will be reported anyways.
387 387 """
388 388 merge_rev = pull_request.merge_rev
389 389 if merge_rev is None:
390 390 raise ValueError('This pull request was not merged yet')
391 391
392 392 commit_ids = list(pull_request.revisions)
393 393 if merge_rev not in commit_ids:
394 394 commit_ids.append(merge_rev)
395 395
396 396 return commit_ids
397 397
398 398 def merge(self, pull_request, user, extras):
399 399 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 400 merge_state = self._merge_pull_request(pull_request, user, extras)
401 401 if merge_state.executed:
402 402 log.debug(
403 403 "Merge was successful, updating the pull request comments.")
404 404 self._comment_and_close_pr(pull_request, user, merge_state)
405 405 self._log_action('user_merged_pull_request', user, pull_request)
406 406 else:
407 407 log.warn("Merge failed, not updating the pull request.")
408 408 return merge_state
409 409
410 410 def _merge_pull_request(self, pull_request, user, extras):
411 411 target_vcs = pull_request.target_repo.scm_instance()
412 412 source_vcs = pull_request.source_repo.scm_instance()
413 413 target_ref = self._refresh_reference(
414 414 pull_request.target_ref_parts, target_vcs)
415 415
416 416 message = _(
417 417 'Merge pull request #%(pr_id)s from '
418 418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 419 'pr_id': pull_request.pull_request_id,
420 420 'source_repo': source_vcs.name,
421 421 'source_ref_name': pull_request.source_ref_parts.name,
422 422 'pr_title': pull_request.title
423 423 }
424 424
425 425 workspace_id = self._workspace_id(pull_request)
426 426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
427 427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
428 428 use_rebase = self._use_rebase_for_merging(pull_request)
429 429
430 430 callback_daemon, extras = prepare_callback_daemon(
431 431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
432 432
433 433 with callback_daemon:
434 434 # TODO: johbo: Implement a clean way to run a config_override
435 435 # for a single call.
436 436 target_vcs.config.set(
437 437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
438 438 merge_state = target_vcs.merge(
439 439 target_ref, source_vcs, pull_request.source_ref_parts,
440 440 workspace_id, user_name=user.username,
441 441 user_email=user.email, message=message, use_rebase=use_rebase)
442 442 return merge_state
443 443
444 444 def _comment_and_close_pr(self, pull_request, user, merge_state):
445 445 pull_request.merge_rev = merge_state.merge_commit_id
446 446 pull_request.updated_on = datetime.datetime.now()
447 447
448 448 ChangesetCommentsModel().create(
449 449 text=unicode(_('Pull request merged and closed')),
450 450 repo=pull_request.target_repo.repo_id,
451 451 user=user.user_id,
452 452 pull_request=pull_request.pull_request_id,
453 453 f_path=None,
454 454 line_no=None,
455 455 closing_pr=True
456 456 )
457 457
458 458 Session().add(pull_request)
459 459 Session().flush()
460 460 # TODO: paris: replace invalidation with less radical solution
461 461 ScmModel().mark_for_invalidation(
462 462 pull_request.target_repo.repo_name)
463 463 self._trigger_pull_request_hook(pull_request, user, 'merge')
464 464
465 465 def has_valid_update_type(self, pull_request):
466 466 source_ref_type = pull_request.source_ref_parts.type
467 467 return source_ref_type in ['book', 'branch', 'tag']
468 468
469 469 def update_commits(self, pull_request):
470 470 """
471 471 Get the updated list of commits for the pull request
472 472 and return the new pull request version and the list
473 473 of commits processed by this update action
474 474 """
475 475
476 476 pull_request = self.__get_pull_request(pull_request)
477 477 source_ref_type = pull_request.source_ref_parts.type
478 478 source_ref_name = pull_request.source_ref_parts.name
479 479 source_ref_id = pull_request.source_ref_parts.commit_id
480 480
481 481 if not self.has_valid_update_type(pull_request):
482 482 log.debug(
483 483 "Skipping update of pull request %s due to ref type: %s",
484 484 pull_request, source_ref_type)
485 485 return (None, None)
486 486
487 487 source_repo = pull_request.source_repo.scm_instance()
488 488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
489 489 if source_ref_id == source_commit.raw_id:
490 490 log.debug("Nothing changed in pull request %s", pull_request)
491 491 return (None, None)
492 492
493 493 # Finally there is a need for an update
494 494 pull_request_version = self._create_version_from_snapshot(pull_request)
495 495 self._link_comments_to_version(pull_request_version)
496 496
497 497 target_ref_type = pull_request.target_ref_parts.type
498 498 target_ref_name = pull_request.target_ref_parts.name
499 499 target_ref_id = pull_request.target_ref_parts.commit_id
500 500 target_repo = pull_request.target_repo.scm_instance()
501 501
502 502 if target_ref_type in ('tag', 'branch', 'book'):
503 503 target_commit = target_repo.get_commit(target_ref_name)
504 504 else:
505 505 target_commit = target_repo.get_commit(target_ref_id)
506 506
507 507 # re-compute commit ids
508 508 old_commit_ids = set(pull_request.revisions)
509 509 pre_load = ["author", "branch", "date", "message"]
510 510 commit_ranges = target_repo.compare(
511 511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
512 512 pre_load=pre_load)
513 513
514 514 ancestor = target_repo.get_common_ancestor(
515 515 target_commit.raw_id, source_commit.raw_id, source_repo)
516 516
517 517 pull_request.source_ref = '%s:%s:%s' % (
518 518 source_ref_type, source_ref_name, source_commit.raw_id)
519 519 pull_request.target_ref = '%s:%s:%s' % (
520 520 target_ref_type, target_ref_name, ancestor)
521 521 pull_request.revisions = [
522 522 commit.raw_id for commit in reversed(commit_ranges)]
523 523 pull_request.updated_on = datetime.datetime.now()
524 524 Session().add(pull_request)
525 525 new_commit_ids = set(pull_request.revisions)
526 526
527 527 changes = self._calculate_commit_id_changes(
528 528 old_commit_ids, new_commit_ids)
529 529
530 530 old_diff_data, new_diff_data = self._generate_update_diffs(
531 531 pull_request, pull_request_version)
532 532
533 533 ChangesetCommentsModel().outdate_comments(
534 534 pull_request, old_diff_data=old_diff_data,
535 535 new_diff_data=new_diff_data)
536 536
537 537 file_changes = self._calculate_file_changes(
538 538 old_diff_data, new_diff_data)
539 539
540 540 # Add an automatic comment to the pull request
541 541 update_comment = ChangesetCommentsModel().create(
542 542 text=self._render_update_message(changes, file_changes),
543 543 repo=pull_request.target_repo,
544 544 user=pull_request.author,
545 545 pull_request=pull_request,
546 546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
547 547
548 548 # Update status to "Under Review" for added commits
549 549 for commit_id in changes.added:
550 550 ChangesetStatusModel().set_status(
551 551 repo=pull_request.source_repo,
552 552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
553 553 comment=update_comment,
554 554 user=pull_request.author,
555 555 pull_request=pull_request,
556 556 revision=commit_id)
557 557
558 558 log.debug(
559 559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
560 560 'removed_ids: %s', pull_request.pull_request_id,
561 561 changes.added, changes.common, changes.removed)
562 562 log.debug('Updated pull request with the following file changes: %s',
563 563 file_changes)
564 564
565 565 log.info(
566 566 "Updated pull request %s from commit %s to commit %s, "
567 567 "stored new version %s of this pull request.",
568 568 pull_request.pull_request_id, source_ref_id,
569 569 pull_request.source_ref_parts.commit_id,
570 570 pull_request_version.pull_request_version_id)
571 571 Session().commit()
572 572 self._trigger_pull_request_hook(pull_request, pull_request.author,
573 573 'update')
574 574 return (pull_request_version, changes)
575 575
576 576 def _create_version_from_snapshot(self, pull_request):
577 577 version = PullRequestVersion()
578 578 version.title = pull_request.title
579 579 version.description = pull_request.description
580 580 version.status = pull_request.status
581 581 version.created_on = pull_request.created_on
582 582 version.updated_on = pull_request.updated_on
583 583 version.user_id = pull_request.user_id
584 584 version.source_repo = pull_request.source_repo
585 585 version.source_ref = pull_request.source_ref
586 586 version.target_repo = pull_request.target_repo
587 587 version.target_ref = pull_request.target_ref
588 588
589 589 version._last_merge_source_rev = pull_request._last_merge_source_rev
590 590 version._last_merge_target_rev = pull_request._last_merge_target_rev
591 591 version._last_merge_status = pull_request._last_merge_status
592 592 version.merge_rev = pull_request.merge_rev
593 593
594 594 version.revisions = pull_request.revisions
595 595 version.pull_request = pull_request
596 596 Session().add(version)
597 597 Session().flush()
598 598
599 599 return version
600 600
601 601 def _generate_update_diffs(self, pull_request, pull_request_version):
602 602 diff_context = (
603 603 self.DIFF_CONTEXT +
604 604 ChangesetCommentsModel.needed_extra_diff_context())
605 605 old_diff = self._get_diff_from_pr_or_version(
606 606 pull_request_version, context=diff_context)
607 607 new_diff = self._get_diff_from_pr_or_version(
608 608 pull_request, context=diff_context)
609 609
610 610 old_diff_data = diffs.DiffProcessor(old_diff)
611 611 old_diff_data.prepare()
612 612 new_diff_data = diffs.DiffProcessor(new_diff)
613 613 new_diff_data.prepare()
614 614
615 615 return old_diff_data, new_diff_data
616 616
617 617 def _link_comments_to_version(self, pull_request_version):
618 618 """
619 619 Link all unlinked comments of this pull request to the given version.
620 620
621 621 :param pull_request_version: The `PullRequestVersion` to which
622 622 the comments shall be linked.
623 623
624 624 """
625 625 pull_request = pull_request_version.pull_request
626 626 comments = ChangesetComment.query().filter(
627 627 # TODO: johbo: Should we query for the repo at all here?
628 628 # Pending decision on how comments of PRs are to be related
629 629 # to either the source repo, the target repo or no repo at all.
630 630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
631 631 ChangesetComment.pull_request == pull_request,
632 632 ChangesetComment.pull_request_version == None)
633 633
634 634 # TODO: johbo: Find out why this breaks if it is done in a bulk
635 635 # operation.
636 636 for comment in comments:
637 637 comment.pull_request_version_id = (
638 638 pull_request_version.pull_request_version_id)
639 639 Session().add(comment)
640 640
641 641 def _calculate_commit_id_changes(self, old_ids, new_ids):
642 642 added = new_ids.difference(old_ids)
643 643 common = old_ids.intersection(new_ids)
644 644 removed = old_ids.difference(new_ids)
645 645 return ChangeTuple(added, common, removed)
646 646
647 647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
648 648
649 649 old_files = OrderedDict()
650 650 for diff_data in old_diff_data.parsed_diff:
651 651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
652 652
653 653 added_files = []
654 654 modified_files = []
655 655 removed_files = []
656 656 for diff_data in new_diff_data.parsed_diff:
657 657 new_filename = diff_data['filename']
658 658 new_hash = md5_safe(diff_data['raw_diff'])
659 659
660 660 old_hash = old_files.get(new_filename)
661 661 if not old_hash:
662 662 # file is not present in old diff, means it's added
663 663 added_files.append(new_filename)
664 664 else:
665 665 if new_hash != old_hash:
666 666 modified_files.append(new_filename)
667 667 # now remove a file from old, since we have seen it already
668 668 del old_files[new_filename]
669 669
670 670 # removed files is when there are present in old, but not in NEW,
671 671 # since we remove old files that are present in new diff, left-overs
672 672 # if any should be the removed files
673 673 removed_files.extend(old_files.keys())
674 674
675 675 return FileChangeTuple(added_files, modified_files, removed_files)
676 676
677 677 def _render_update_message(self, changes, file_changes):
678 678 """
679 679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
680 680 so it's always looking the same disregarding on which default
681 681 renderer system is using.
682 682
683 683 :param changes: changes named tuple
684 684 :param file_changes: file changes named tuple
685 685
686 686 """
687 687 new_status = ChangesetStatus.get_status_lbl(
688 688 ChangesetStatus.STATUS_UNDER_REVIEW)
689 689
690 690 changed_files = (
691 691 file_changes.added + file_changes.modified + file_changes.removed)
692 692
693 693 params = {
694 694 'under_review_label': new_status,
695 695 'added_commits': changes.added,
696 696 'removed_commits': changes.removed,
697 697 'changed_files': changed_files,
698 698 'added_files': file_changes.added,
699 699 'modified_files': file_changes.modified,
700 700 'removed_files': file_changes.removed,
701 701 }
702 702 renderer = RstTemplateRenderer()
703 703 return renderer.render('pull_request_update.mako', **params)
704 704
705 705 def edit(self, pull_request, title, description):
706 706 pull_request = self.__get_pull_request(pull_request)
707 707 if pull_request.is_closed():
708 708 raise ValueError('This pull request is closed')
709 709 if title:
710 710 pull_request.title = title
711 711 pull_request.description = description
712 712 pull_request.updated_on = datetime.datetime.now()
713 713 Session().add(pull_request)
714 714
715 715 def update_reviewers(self, pull_request, reviewers_ids):
716 716 reviewers_ids = set(reviewers_ids)
717 717 pull_request = self.__get_pull_request(pull_request)
718 718 current_reviewers = PullRequestReviewers.query()\
719 719 .filter(PullRequestReviewers.pull_request ==
720 720 pull_request).all()
721 721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
722 722
723 723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
724 724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
725 725
726 726 log.debug("Adding %s reviewers", ids_to_add)
727 727 log.debug("Removing %s reviewers", ids_to_remove)
728 728 changed = False
729 729 for uid in ids_to_add:
730 730 changed = True
731 731 _usr = self._get_user(uid)
732 732 reviewer = PullRequestReviewers(_usr, pull_request)
733 733 Session().add(reviewer)
734 734
735 735 self.notify_reviewers(pull_request, ids_to_add)
736 736
737 737 for uid in ids_to_remove:
738 738 changed = True
739 739 reviewer = PullRequestReviewers.query()\
740 740 .filter(PullRequestReviewers.user_id == uid,
741 741 PullRequestReviewers.pull_request == pull_request)\
742 742 .scalar()
743 743 if reviewer:
744 744 Session().delete(reviewer)
745 745 if changed:
746 746 pull_request.updated_on = datetime.datetime.now()
747 747 Session().add(pull_request)
748 748
749 749 return ids_to_add, ids_to_remove
750 750
751 751 def get_url(self, pull_request):
752 return h.url('pullrequest_show', repo_name=self.target_repo.repo_name,
753 pull_request_id=self.pull_request_id,
754 qualified=True)
752 return h.url('pullrequest_show',
753 repo_name=pull_request.target_repo.repo_name,
754 pull_request_id=pull_request.pull_request_id,
755 qualified=True)
755 756
756 757 def notify_reviewers(self, pull_request, reviewers_ids):
757 758 # notification to reviewers
758 759 if not reviewers_ids:
759 760 return
760 761
761 762 pull_request_obj = pull_request
762 763 # get the current participants of this pull request
763 764 recipients = reviewers_ids
764 765 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765 766
766 767 pr_source_repo = pull_request_obj.source_repo
767 768 pr_target_repo = pull_request_obj.target_repo
768 769
769 770 pr_url = h.url(
770 771 'pullrequest_show',
771 772 repo_name=pr_target_repo.repo_name,
772 773 pull_request_id=pull_request_obj.pull_request_id,
773 774 qualified=True,)
774 775
775 776 # set some variables for email notification
776 777 pr_target_repo_url = h.url(
777 778 'summary_home',
778 779 repo_name=pr_target_repo.repo_name,
779 780 qualified=True)
780 781
781 782 pr_source_repo_url = h.url(
782 783 'summary_home',
783 784 repo_name=pr_source_repo.repo_name,
784 785 qualified=True)
785 786
786 787 # pull request specifics
787 788 pull_request_commits = [
788 789 (x.raw_id, x.message)
789 790 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790 791
791 792 kwargs = {
792 793 'user': pull_request.author,
793 794 'pull_request': pull_request_obj,
794 795 'pull_request_commits': pull_request_commits,
795 796
796 797 'pull_request_target_repo': pr_target_repo,
797 798 'pull_request_target_repo_url': pr_target_repo_url,
798 799
799 800 'pull_request_source_repo': pr_source_repo,
800 801 'pull_request_source_repo_url': pr_source_repo_url,
801 802
802 803 'pull_request_url': pr_url,
803 804 }
804 805
805 806 # pre-generate the subject for notification itself
806 807 (subject,
807 808 _h, _e, # we don't care about those
808 809 body_plaintext) = EmailNotificationModel().render_email(
809 810 notification_type, **kwargs)
810 811
811 812 # create notification objects, and emails
812 813 NotificationModel().create(
813 814 created_by=pull_request.author,
814 815 notification_subject=subject,
815 816 notification_body=body_plaintext,
816 817 notification_type=notification_type,
817 818 recipients=recipients,
818 819 email_kwargs=kwargs,
819 820 )
820 821
821 822 def delete(self, pull_request):
822 823 pull_request = self.__get_pull_request(pull_request)
823 824 self._cleanup_merge_workspace(pull_request)
824 825 Session().delete(pull_request)
825 826
826 827 def close_pull_request(self, pull_request, user):
827 828 pull_request = self.__get_pull_request(pull_request)
828 829 self._cleanup_merge_workspace(pull_request)
829 830 pull_request.status = PullRequest.STATUS_CLOSED
830 831 pull_request.updated_on = datetime.datetime.now()
831 832 Session().add(pull_request)
832 833 self._trigger_pull_request_hook(
833 834 pull_request, pull_request.author, 'close')
834 835 self._log_action('user_closed_pull_request', user, pull_request)
835 836
836 837 def close_pull_request_with_comment(self, pull_request, user, repo,
837 838 message=None):
838 839 status = ChangesetStatus.STATUS_REJECTED
839 840
840 841 if not message:
841 842 message = (
842 843 _('Status change %(transition_icon)s %(status)s') % {
843 844 'transition_icon': '>',
844 845 'status': ChangesetStatus.get_status_lbl(status)})
845 846
846 847 internal_message = _('Closing with') + ' ' + message
847 848
848 849 comm = ChangesetCommentsModel().create(
849 850 text=internal_message,
850 851 repo=repo.repo_id,
851 852 user=user.user_id,
852 853 pull_request=pull_request.pull_request_id,
853 854 f_path=None,
854 855 line_no=None,
855 856 status_change=ChangesetStatus.get_status_lbl(status),
856 857 closing_pr=True
857 858 )
858 859
859 860 ChangesetStatusModel().set_status(
860 861 repo.repo_id,
861 862 status,
862 863 user.user_id,
863 864 comm,
864 865 pull_request=pull_request.pull_request_id
865 866 )
866 867 Session().flush()
867 868
868 869 PullRequestModel().close_pull_request(
869 870 pull_request.pull_request_id, user)
870 871
871 872 def merge_status(self, pull_request):
872 873 if not self._is_merge_enabled(pull_request):
873 874 return False, _('Server-side pull request merging is disabled.')
874 875 if pull_request.is_closed():
875 876 return False, _('This pull request is closed.')
876 877 merge_possible, msg = self._check_repo_requirements(
877 878 target=pull_request.target_repo, source=pull_request.source_repo)
878 879 if not merge_possible:
879 880 return merge_possible, msg
880 881
881 882 try:
882 883 resp = self._try_merge(pull_request)
883 884 status = resp.possible, self.merge_status_message(
884 885 resp.failure_reason)
885 886 except NotImplementedError:
886 887 status = False, _('Pull request merging is not supported.')
887 888
888 889 return status
889 890
890 891 def _check_repo_requirements(self, target, source):
891 892 """
892 893 Check if `target` and `source` have compatible requirements.
893 894
894 895 Currently this is just checking for largefiles.
895 896 """
896 897 target_has_largefiles = self._has_largefiles(target)
897 898 source_has_largefiles = self._has_largefiles(source)
898 899 merge_possible = True
899 900 message = u''
900 901
901 902 if target_has_largefiles != source_has_largefiles:
902 903 merge_possible = False
903 904 if source_has_largefiles:
904 905 message = _(
905 906 'Target repository large files support is disabled.')
906 907 else:
907 908 message = _(
908 909 'Source repository large files support is disabled.')
909 910
910 911 return merge_possible, message
911 912
912 913 def _has_largefiles(self, repo):
913 914 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
914 915 'extensions', 'largefiles')
915 916 return largefiles_ui and largefiles_ui[0].active
916 917
917 918 def _try_merge(self, pull_request):
918 919 """
919 920 Try to merge the pull request and return the merge status.
920 921 """
921 922 log.debug(
922 923 "Trying out if the pull request %s can be merged.",
923 924 pull_request.pull_request_id)
924 925 target_vcs = pull_request.target_repo.scm_instance()
925 926 target_ref = self._refresh_reference(
926 927 pull_request.target_ref_parts, target_vcs)
927 928
928 929 target_locked = pull_request.target_repo.locked
929 930 if target_locked and target_locked[0]:
930 931 log.debug("The target repository is locked.")
931 932 merge_state = MergeResponse(
932 933 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
933 934 elif self._needs_merge_state_refresh(pull_request, target_ref):
934 935 log.debug("Refreshing the merge status of the repository.")
935 936 merge_state = self._refresh_merge_state(
936 937 pull_request, target_vcs, target_ref)
937 938 else:
938 939 possible = pull_request.\
939 940 _last_merge_status == MergeFailureReason.NONE
940 941 merge_state = MergeResponse(
941 942 possible, False, None, pull_request._last_merge_status)
942 943 log.debug("Merge response: %s", merge_state)
943 944 return merge_state
944 945
945 946 def _refresh_reference(self, reference, vcs_repository):
946 947 if reference.type in ('branch', 'book'):
947 948 name_or_id = reference.name
948 949 else:
949 950 name_or_id = reference.commit_id
950 951 refreshed_commit = vcs_repository.get_commit(name_or_id)
951 952 refreshed_reference = Reference(
952 953 reference.type, reference.name, refreshed_commit.raw_id)
953 954 return refreshed_reference
954 955
955 956 def _needs_merge_state_refresh(self, pull_request, target_reference):
956 957 return not(
957 958 pull_request.revisions and
958 959 pull_request.revisions[0] == pull_request._last_merge_source_rev and
959 960 target_reference.commit_id == pull_request._last_merge_target_rev)
960 961
961 962 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
962 963 workspace_id = self._workspace_id(pull_request)
963 964 source_vcs = pull_request.source_repo.scm_instance()
964 965 use_rebase = self._use_rebase_for_merging(pull_request)
965 966 merge_state = target_vcs.merge(
966 967 target_reference, source_vcs, pull_request.source_ref_parts,
967 968 workspace_id, dry_run=True, use_rebase=use_rebase)
968 969
969 970 # Do not store the response if there was an unknown error.
970 971 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
971 972 pull_request._last_merge_source_rev = pull_request.\
972 973 source_ref_parts.commit_id
973 974 pull_request._last_merge_target_rev = target_reference.commit_id
974 975 pull_request._last_merge_status = (
975 976 merge_state.failure_reason)
976 977 Session().add(pull_request)
977 978 Session().flush()
978 979
979 980 return merge_state
980 981
981 982 def _workspace_id(self, pull_request):
982 983 workspace_id = 'pr-%s' % pull_request.pull_request_id
983 984 return workspace_id
984 985
985 986 def merge_status_message(self, status_code):
986 987 """
987 988 Return a human friendly error message for the given merge status code.
988 989 """
989 990 return self.MERGE_STATUS_MESSAGES[status_code]
990 991
991 992 def generate_repo_data(self, repo, commit_id=None, branch=None,
992 993 bookmark=None):
993 994 all_refs, selected_ref = \
994 995 self._get_repo_pullrequest_sources(
995 996 repo.scm_instance(), commit_id=commit_id,
996 997 branch=branch, bookmark=bookmark)
997 998
998 999 refs_select2 = []
999 1000 for element in all_refs:
1000 1001 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1001 1002 refs_select2.append({'text': element[1], 'children': children})
1002 1003
1003 1004 return {
1004 1005 'user': {
1005 1006 'user_id': repo.user.user_id,
1006 1007 'username': repo.user.username,
1007 1008 'firstname': repo.user.firstname,
1008 1009 'lastname': repo.user.lastname,
1009 1010 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1010 1011 },
1011 1012 'description': h.chop_at_smart(repo.description, '\n'),
1012 1013 'refs': {
1013 1014 'all_refs': all_refs,
1014 1015 'selected_ref': selected_ref,
1015 1016 'select2_refs': refs_select2
1016 1017 }
1017 1018 }
1018 1019
1019 1020 def generate_pullrequest_title(self, source, source_ref, target):
1020 1021 return '{source}#{at_ref} to {target}'.format(
1021 1022 source=source,
1022 1023 at_ref=source_ref,
1023 1024 target=target,
1024 1025 )
1025 1026
1026 1027 def _cleanup_merge_workspace(self, pull_request):
1027 1028 # Merging related cleanup
1028 1029 target_scm = pull_request.target_repo.scm_instance()
1029 1030 workspace_id = 'pr-%s' % pull_request.pull_request_id
1030 1031
1031 1032 try:
1032 1033 target_scm.cleanup_merge_workspace(workspace_id)
1033 1034 except NotImplementedError:
1034 1035 pass
1035 1036
1036 1037 def _get_repo_pullrequest_sources(
1037 1038 self, repo, commit_id=None, branch=None, bookmark=None):
1038 1039 """
1039 1040 Return a structure with repo's interesting commits, suitable for
1040 1041 the selectors in pullrequest controller
1041 1042
1042 1043 :param commit_id: a commit that must be in the list somehow
1043 1044 and selected by default
1044 1045 :param branch: a branch that must be in the list and selected
1045 1046 by default - even if closed
1046 1047 :param bookmark: a bookmark that must be in the list and selected
1047 1048 """
1048 1049
1049 1050 commit_id = safe_str(commit_id) if commit_id else None
1050 1051 branch = safe_str(branch) if branch else None
1051 1052 bookmark = safe_str(bookmark) if bookmark else None
1052 1053
1053 1054 selected = None
1054 1055
1055 1056 # order matters: first source that has commit_id in it will be selected
1056 1057 sources = []
1057 1058 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1058 1059 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1059 1060
1060 1061 if commit_id:
1061 1062 ref_commit = (h.short_id(commit_id), commit_id)
1062 1063 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1063 1064
1064 1065 sources.append(
1065 1066 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1066 1067 )
1067 1068
1068 1069 groups = []
1069 1070 for group_key, ref_list, group_name, match in sources:
1070 1071 group_refs = []
1071 1072 for ref_name, ref_id in ref_list:
1072 1073 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1073 1074 group_refs.append((ref_key, ref_name))
1074 1075
1075 1076 if not selected:
1076 1077 if set([commit_id, match]) & set([ref_id, ref_name]):
1077 1078 selected = ref_key
1078 1079
1079 1080 if group_refs:
1080 1081 groups.append((group_refs, group_name))
1081 1082
1082 1083 if not selected:
1083 1084 ref = commit_id or branch or bookmark
1084 1085 if ref:
1085 1086 raise CommitDoesNotExistError(
1086 1087 'No commit refs could be found matching: %s' % ref)
1087 1088 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1088 1089 selected = 'branch:%s:%s' % (
1089 1090 repo.DEFAULT_BRANCH_NAME,
1090 1091 repo.branches[repo.DEFAULT_BRANCH_NAME]
1091 1092 )
1092 1093 elif repo.commit_ids:
1093 1094 rev = repo.commit_ids[0]
1094 1095 selected = 'rev:%s:%s' % (rev, rev)
1095 1096 else:
1096 1097 raise EmptyRepositoryError()
1097 1098 return groups, selected
1098 1099
1099 1100 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1100 1101 pull_request = self.__get_pull_request(pull_request)
1101 1102 return self._get_diff_from_pr_or_version(pull_request, context=context)
1102 1103
1103 1104 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1104 1105 source_repo = pr_or_version.source_repo
1105 1106
1106 1107 # we swap org/other ref since we run a simple diff on one repo
1107 1108 target_ref_id = pr_or_version.target_ref_parts.commit_id
1108 1109 source_ref_id = pr_or_version.source_ref_parts.commit_id
1109 1110 target_commit = source_repo.get_commit(
1110 1111 commit_id=safe_str(target_ref_id))
1111 1112 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1112 1113 vcs_repo = source_repo.scm_instance()
1113 1114
1114 1115 # TODO: johbo: In the context of an update, we cannot reach
1115 1116 # the old commit anymore with our normal mechanisms. It needs
1116 1117 # some sort of special support in the vcs layer to avoid this
1117 1118 # workaround.
1118 1119 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1119 1120 vcs_repo.alias == 'git'):
1120 1121 source_commit.raw_id = safe_str(source_ref_id)
1121 1122
1122 1123 log.debug('calculating diff between '
1123 1124 'source_ref:%s and target_ref:%s for repo `%s`',
1124 1125 target_ref_id, source_ref_id,
1125 1126 safe_unicode(vcs_repo.path))
1126 1127
1127 1128 vcs_diff = vcs_repo.get_diff(
1128 1129 commit1=target_commit, commit2=source_commit, context=context)
1129 1130 return vcs_diff
1130 1131
1131 1132 def _is_merge_enabled(self, pull_request):
1132 1133 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1133 1134 settings = settings_model.get_general_settings()
1134 1135 return settings.get('rhodecode_pr_merge_enabled', False)
1135 1136
1136 1137 def _use_rebase_for_merging(self, pull_request):
1137 1138 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1138 1139 settings = settings_model.get_general_settings()
1139 1140 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1140 1141
1141 1142 def _log_action(self, action, user, pull_request):
1142 1143 action_logger(
1143 1144 user,
1144 1145 '{action}:{pr_id}'.format(
1145 1146 action=action, pr_id=pull_request.pull_request_id),
1146 1147 pull_request.target_repo)
1147 1148
1148 1149
1149 1150 ChangeTuple = namedtuple('ChangeTuple',
1150 1151 ['added', 'common', 'removed'])
1151 1152
1152 1153 FileChangeTuple = namedtuple('FileChangeTuple',
1153 1154 ['added', 'modified', 'removed'])
@@ -1,78 +1,78 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests.events.conftest import EventCatcher
24 24
25 25 from rhodecode.model.pull_request import PullRequestModel
26 26 from rhodecode.events import (
27 27 PullRequestCreateEvent,
28 28 PullRequestUpdateEvent,
29 29 PullRequestReviewEvent,
30 30 PullRequestMergeEvent,
31 31 PullRequestCloseEvent,
32 32 )
33 33
34 34 # TODO: dan: make the serialization tests complete json comparisons
35 35 @pytest.mark.backends("git", "hg")
36 36 @pytest.mark.parametrize('EventClass', [
37 37 PullRequestCreateEvent,
38 38 PullRequestUpdateEvent,
39 39 PullRequestReviewEvent,
40 40 PullRequestMergeEvent,
41 41 PullRequestCloseEvent,
42 42 ])
43 43 def test_pullrequest_events_serialized(pr_util, EventClass):
44 44 pr = pr_util.create_pull_request()
45 45 event = EventClass(pr)
46 46 data = event.as_dict()
47 47 assert data['name'] == EventClass.name
48 48 assert data['repo']['repo_name'] == pr.target_repo.repo_name
49 49 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
50
50 assert data['pullrequest']['url']
51 51
52 52 @pytest.mark.backends("git", "hg")
53 53 def test_create_pull_request_events(pr_util):
54 54 with EventCatcher() as event_catcher:
55 55 pr_util.create_pull_request()
56 56
57 57 assert PullRequestCreateEvent in event_catcher.events_types
58 58
59 59
60 60 @pytest.mark.backends("git", "hg")
61 61 def test_close_pull_request_events(pr_util, user_admin):
62 62 pr = pr_util.create_pull_request()
63 63
64 64 with EventCatcher() as event_catcher:
65 65 PullRequestModel().close_pull_request(pr, user_admin)
66 66
67 67 assert PullRequestCloseEvent in event_catcher.events_types
68 68
69 69
70 70 @pytest.mark.backends("git", "hg")
71 71 def test_close_pull_request_with_comment_events(pr_util, user_admin):
72 72 pr = pr_util.create_pull_request()
73 73
74 74 with EventCatcher() as event_catcher:
75 75 PullRequestModel().close_pull_request_with_comment(
76 76 pr, user_admin, pr.target_repo)
77 77
78 78 assert PullRequestCloseEvent in event_catcher.events_types
@@ -1,113 +1,116 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests.events.conftest import EventCatcher
24 24
25 25 from rhodecode.lib import hooks_base, utils2
26 26 from rhodecode.model.repo import RepoModel
27 27 from rhodecode.events.repo import (
28 28 RepoPrePullEvent, RepoPullEvent,
29 29 RepoPrePushEvent, RepoPushEvent,
30 30 RepoPreCreateEvent, RepoCreatedEvent,
31 31 RepoPreDeleteEvent, RepoDeletedEvent,
32 32 )
33 33
34 34
35 35 @pytest.fixture
36 36 def scm_extras(user_regular, repo_stub):
37 37 extras = utils2.AttributeDict({
38 38 'ip': '127.0.0.1',
39 39 'username': user_regular.username,
40 40 'action': '',
41 41 'repository': repo_stub.repo_name,
42 42 'scm': repo_stub.scm_instance().alias,
43 43 'config': '',
44 44 'server_url': 'http://example.com',
45 45 'make_lock': None,
46 46 'locked_by': [None],
47 47 'commit_ids': ['a' * 40] * 3,
48 48 })
49 49 return extras
50 50
51 51
52 52 # TODO: dan: make the serialization tests complete json comparisons
53 53 @pytest.mark.parametrize('EventClass', [
54 54 RepoPreCreateEvent, RepoCreatedEvent,
55 55 RepoPreDeleteEvent, RepoDeletedEvent,
56 56 ])
57 57 def test_repo_events_serialized(repo_stub, EventClass):
58 58 event = EventClass(repo_stub)
59 59 data = event.as_dict()
60 60 assert data['name'] == EventClass.name
61 61 assert data['repo']['repo_name'] == repo_stub.repo_name
62 assert data['repo']['url']
62 63
63 64
64 65 @pytest.mark.parametrize('EventClass', [
65 66 RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent
66 67 ])
67 68 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
68 69 event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras)
69 70 data = event.as_dict()
70 71 assert data['name'] == EventClass.name
71 72 assert data['repo']['repo_name'] == repo_stub.repo_name
73 assert data['repo']['url']
72 74
73 75
74 76
75 77 @pytest.mark.parametrize('EventClass', [RepoPushEvent])
76 def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass):
78 def test_vcs_repo_push_event_serialize(repo_stub, scm_extras, EventClass):
77 79 event = EventClass(repo_name=repo_stub.repo_name,
78 80 pushed_commit_ids=scm_extras['commit_ids'],
79 81 extras=scm_extras)
80 82 data = event.as_dict()
81 83 assert data['name'] == EventClass.name
82 84 assert data['repo']['repo_name'] == repo_stub.repo_name
85 assert data['repo']['url']
83 86
84 87
85 88 def test_create_delete_repo_fires_events(backend):
86 89 with EventCatcher() as event_catcher:
87 90 repo = backend.create_repo()
88 91 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreatedEvent]
89 92
90 93 with EventCatcher() as event_catcher:
91 94 RepoModel().delete(repo)
92 95 assert event_catcher.events_types == [RepoPreDeleteEvent, RepoDeletedEvent]
93 96
94 97
95 98 def test_pull_fires_events(scm_extras):
96 99 with EventCatcher() as event_catcher:
97 100 hooks_base.pre_push(scm_extras)
98 101 assert event_catcher.events_types == [RepoPrePushEvent]
99 102
100 103 with EventCatcher() as event_catcher:
101 104 hooks_base.post_push(scm_extras)
102 105 assert event_catcher.events_types == [RepoPushEvent]
103 106
104 107
105 108 def test_push_fires_events(scm_extras):
106 109 with EventCatcher() as event_catcher:
107 110 hooks_base.pre_pull(scm_extras)
108 111 assert event_catcher.events_types == [RepoPrePullEvent]
109 112
110 113 with EventCatcher() as event_catcher:
111 114 hooks_base.post_pull(scm_extras)
112 115 assert event_catcher.events_types == [RepoPullEvent]
113 116
General Comments 0
You need to be logged in to leave comments. Login now