##// END OF EJS Templates
db: use a wrapper on pull requests _last_merge_status to ensure this is always INT....
marcink -
r1968:ea1add97 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1551 +1,1551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35 from sqlalchemy import or_
35 from sqlalchemy import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository)
55 PullRequestVersion, ChangesetComment, Repository)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = namedtuple('UpdateResponse', [
68 UpdateResponse = namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = 3
77 DIFF_CONTEXT = 3
78
78
79 MERGE_STATUS_MESSAGES = {
79 MERGE_STATUS_MESSAGES = {
80 MergeFailureReason.NONE: lazy_ugettext(
80 MergeFailureReason.NONE: lazy_ugettext(
81 'This pull request can be automatically merged.'),
81 'This pull request can be automatically merged.'),
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 'This pull request cannot be merged because of an unhandled'
83 'This pull request cannot be merged because of an unhandled'
84 ' exception.'),
84 ' exception.'),
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 'This pull request cannot be merged because of merge conflicts.'),
86 'This pull request cannot be merged because of merge conflicts.'),
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 'This pull request could not be merged because push to target'
88 'This pull request could not be merged because push to target'
89 ' failed.'),
89 ' failed.'),
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 'This pull request cannot be merged because the target is not a'
91 'This pull request cannot be merged because the target is not a'
92 ' head.'),
92 ' head.'),
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 'This pull request cannot be merged because the source contains'
94 'This pull request cannot be merged because the source contains'
95 ' more branches than the target.'),
95 ' more branches than the target.'),
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 'This pull request cannot be merged because the target has'
97 'This pull request cannot be merged because the target has'
98 ' multiple heads.'),
98 ' multiple heads.'),
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 'This pull request cannot be merged because the target repository'
100 'This pull request cannot be merged because the target repository'
101 ' is locked.'),
101 ' is locked.'),
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 'This pull request cannot be merged because the target or the '
103 'This pull request cannot be merged because the target or the '
104 'source reference is missing.'),
104 'source reference is missing.'),
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 'This pull request cannot be merged because the target '
106 'This pull request cannot be merged because the target '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 'This pull request cannot be merged because the source '
109 'This pull request cannot be merged because the source '
110 'reference is missing.'),
110 'reference is missing.'),
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 'This pull request cannot be merged because of conflicts related '
112 'This pull request cannot be merged because of conflicts related '
113 'to sub repositories.'),
113 'to sub repositories.'),
114 }
114 }
115
115
116 UPDATE_STATUS_MESSAGES = {
116 UPDATE_STATUS_MESSAGES = {
117 UpdateFailureReason.NONE: lazy_ugettext(
117 UpdateFailureReason.NONE: lazy_ugettext(
118 'Pull request update successful.'),
118 'Pull request update successful.'),
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 'Pull request update failed because of an unknown error.'),
120 'Pull request update failed because of an unknown error.'),
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 'No update needed because the source and target have not changed.'),
122 'No update needed because the source and target have not changed.'),
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 'Pull request cannot be updated because the reference type is '
124 'Pull request cannot be updated because the reference type is '
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 'This pull request cannot be updated because the target '
127 'This pull request cannot be updated because the target '
128 'reference is missing.'),
128 'reference is missing.'),
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 'This pull request cannot be updated because the source '
130 'This pull request cannot be updated because the source '
131 'reference is missing.'),
131 'reference is missing.'),
132 }
132 }
133
133
134 def __get_pull_request(self, pull_request):
134 def __get_pull_request(self, pull_request):
135 return self._get_instance((
135 return self._get_instance((
136 PullRequest, PullRequestVersion), pull_request)
136 PullRequest, PullRequestVersion), pull_request)
137
137
138 def _check_perms(self, perms, pull_request, user, api=False):
138 def _check_perms(self, perms, pull_request, user, api=False):
139 if not api:
139 if not api:
140 return h.HasRepoPermissionAny(*perms)(
140 return h.HasRepoPermissionAny(*perms)(
141 user=user, repo_name=pull_request.target_repo.repo_name)
141 user=user, repo_name=pull_request.target_repo.repo_name)
142 else:
142 else:
143 return h.HasRepoPermissionAnyApi(*perms)(
143 return h.HasRepoPermissionAnyApi(*perms)(
144 user=user, repo_name=pull_request.target_repo.repo_name)
144 user=user, repo_name=pull_request.target_repo.repo_name)
145
145
146 def check_user_read(self, pull_request, user, api=False):
146 def check_user_read(self, pull_request, user, api=False):
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 return self._check_perms(_perms, pull_request, user, api)
148 return self._check_perms(_perms, pull_request, user, api)
149
149
150 def check_user_merge(self, pull_request, user, api=False):
150 def check_user_merge(self, pull_request, user, api=False):
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 return self._check_perms(_perms, pull_request, user, api)
152 return self._check_perms(_perms, pull_request, user, api)
153
153
154 def check_user_update(self, pull_request, user, api=False):
154 def check_user_update(self, pull_request, user, api=False):
155 owner = user.user_id == pull_request.user_id
155 owner = user.user_id == pull_request.user_id
156 return self.check_user_merge(pull_request, user, api) or owner
156 return self.check_user_merge(pull_request, user, api) or owner
157
157
158 def check_user_delete(self, pull_request, user):
158 def check_user_delete(self, pull_request, user):
159 owner = user.user_id == pull_request.user_id
159 owner = user.user_id == pull_request.user_id
160 _perms = ('repository.admin',)
160 _perms = ('repository.admin',)
161 return self._check_perms(_perms, pull_request, user) or owner
161 return self._check_perms(_perms, pull_request, user) or owner
162
162
163 def check_user_change_status(self, pull_request, user, api=False):
163 def check_user_change_status(self, pull_request, user, api=False):
164 reviewer = user.user_id in [x.user_id for x in
164 reviewer = user.user_id in [x.user_id for x in
165 pull_request.reviewers]
165 pull_request.reviewers]
166 return self.check_user_update(pull_request, user, api) or reviewer
166 return self.check_user_update(pull_request, user, api) or reviewer
167
167
168 def get(self, pull_request):
168 def get(self, pull_request):
169 return self.__get_pull_request(pull_request)
169 return self.__get_pull_request(pull_request)
170
170
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 opened_by=None, order_by=None,
172 opened_by=None, order_by=None,
173 order_dir='desc'):
173 order_dir='desc'):
174 repo = None
174 repo = None
175 if repo_name:
175 if repo_name:
176 repo = self._get_repo(repo_name)
176 repo = self._get_repo(repo_name)
177
177
178 q = PullRequest.query()
178 q = PullRequest.query()
179
179
180 # source or target
180 # source or target
181 if repo and source:
181 if repo and source:
182 q = q.filter(PullRequest.source_repo == repo)
182 q = q.filter(PullRequest.source_repo == repo)
183 elif repo:
183 elif repo:
184 q = q.filter(PullRequest.target_repo == repo)
184 q = q.filter(PullRequest.target_repo == repo)
185
185
186 # closed,opened
186 # closed,opened
187 if statuses:
187 if statuses:
188 q = q.filter(PullRequest.status.in_(statuses))
188 q = q.filter(PullRequest.status.in_(statuses))
189
189
190 # opened by filter
190 # opened by filter
191 if opened_by:
191 if opened_by:
192 q = q.filter(PullRequest.user_id.in_(opened_by))
192 q = q.filter(PullRequest.user_id.in_(opened_by))
193
193
194 if order_by:
194 if order_by:
195 order_map = {
195 order_map = {
196 'name_raw': PullRequest.pull_request_id,
196 'name_raw': PullRequest.pull_request_id,
197 'title': PullRequest.title,
197 'title': PullRequest.title,
198 'updated_on_raw': PullRequest.updated_on,
198 'updated_on_raw': PullRequest.updated_on,
199 'target_repo': PullRequest.target_repo_id
199 'target_repo': PullRequest.target_repo_id
200 }
200 }
201 if order_dir == 'asc':
201 if order_dir == 'asc':
202 q = q.order_by(order_map[order_by].asc())
202 q = q.order_by(order_map[order_by].asc())
203 else:
203 else:
204 q = q.order_by(order_map[order_by].desc())
204 q = q.order_by(order_map[order_by].desc())
205
205
206 return q
206 return q
207
207
208 def count_all(self, repo_name, source=False, statuses=None,
208 def count_all(self, repo_name, source=False, statuses=None,
209 opened_by=None):
209 opened_by=None):
210 """
210 """
211 Count the number of pull requests for a specific repository.
211 Count the number of pull requests for a specific repository.
212
212
213 :param repo_name: target or source repo
213 :param repo_name: target or source repo
214 :param source: boolean flag to specify if repo_name refers to source
214 :param source: boolean flag to specify if repo_name refers to source
215 :param statuses: list of pull request statuses
215 :param statuses: list of pull request statuses
216 :param opened_by: author user of the pull request
216 :param opened_by: author user of the pull request
217 :returns: int number of pull requests
217 :returns: int number of pull requests
218 """
218 """
219 q = self._prepare_get_all_query(
219 q = self._prepare_get_all_query(
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221
221
222 return q.count()
222 return q.count()
223
223
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 offset=0, length=None, order_by=None, order_dir='desc'):
225 offset=0, length=None, order_by=None, order_dir='desc'):
226 """
226 """
227 Get all pull requests for a specific repository.
227 Get all pull requests for a specific repository.
228
228
229 :param repo_name: target or source repo
229 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
230 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
231 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
232 :param opened_by: author user of the pull request
233 :param offset: pagination offset
233 :param offset: pagination offset
234 :param length: length of returned list
234 :param length: length of returned list
235 :param order_by: order of the returned list
235 :param order_by: order of the returned list
236 :param order_dir: 'asc' or 'desc' ordering direction
236 :param order_dir: 'asc' or 'desc' ordering direction
237 :returns: list of pull requests
237 :returns: list of pull requests
238 """
238 """
239 q = self._prepare_get_all_query(
239 q = self._prepare_get_all_query(
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 order_by=order_by, order_dir=order_dir)
241 order_by=order_by, order_dir=order_dir)
242
242
243 if length:
243 if length:
244 pull_requests = q.limit(length).offset(offset).all()
244 pull_requests = q.limit(length).offset(offset).all()
245 else:
245 else:
246 pull_requests = q.all()
246 pull_requests = q.all()
247
247
248 return pull_requests
248 return pull_requests
249
249
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 opened_by=None):
251 opened_by=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review.
254 awaiting review.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :returns: int number of pull requests
260 :returns: int number of pull requests
261 """
261 """
262 pull_requests = self.get_awaiting_review(
262 pull_requests = self.get_awaiting_review(
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264
264
265 return len(pull_requests)
265 return len(pull_requests)
266
266
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 opened_by=None, offset=0, length=None,
268 opened_by=None, offset=0, length=None,
269 order_by=None, order_dir='desc'):
269 order_by=None, order_dir='desc'):
270 """
270 """
271 Get all pull requests for a specific repository that are awaiting
271 Get all pull requests for a specific repository that are awaiting
272 review.
272 review.
273
273
274 :param repo_name: target or source repo
274 :param repo_name: target or source repo
275 :param source: boolean flag to specify if repo_name refers to source
275 :param source: boolean flag to specify if repo_name refers to source
276 :param statuses: list of pull request statuses
276 :param statuses: list of pull request statuses
277 :param opened_by: author user of the pull request
277 :param opened_by: author user of the pull request
278 :param offset: pagination offset
278 :param offset: pagination offset
279 :param length: length of returned list
279 :param length: length of returned list
280 :param order_by: order of the returned list
280 :param order_by: order of the returned list
281 :param order_dir: 'asc' or 'desc' ordering direction
281 :param order_dir: 'asc' or 'desc' ordering direction
282 :returns: list of pull requests
282 :returns: list of pull requests
283 """
283 """
284 pull_requests = self.get_all(
284 pull_requests = self.get_all(
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 order_by=order_by, order_dir=order_dir)
286 order_by=order_by, order_dir=order_dir)
287
287
288 _filtered_pull_requests = []
288 _filtered_pull_requests = []
289 for pr in pull_requests:
289 for pr in pull_requests:
290 status = pr.calculated_review_status()
290 status = pr.calculated_review_status()
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 _filtered_pull_requests.append(pr)
293 _filtered_pull_requests.append(pr)
294 if length:
294 if length:
295 return _filtered_pull_requests[offset:offset+length]
295 return _filtered_pull_requests[offset:offset+length]
296 else:
296 else:
297 return _filtered_pull_requests
297 return _filtered_pull_requests
298
298
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 opened_by=None, user_id=None):
300 opened_by=None, user_id=None):
301 """
301 """
302 Count the number of pull requests for a specific repository that are
302 Count the number of pull requests for a specific repository that are
303 awaiting review from a specific user.
303 awaiting review from a specific user.
304
304
305 :param repo_name: target or source repo
305 :param repo_name: target or source repo
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 user_id=user_id)
314 user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param source: boolean flag to specify if repo_name refers to source
326 :param source: boolean flag to specify if repo_name refers to source
327 :param statuses: list of pull request statuses
327 :param statuses: list of pull request statuses
328 :param opened_by: author user of the pull request
328 :param opened_by: author user of the pull request
329 :param user_id: reviewer user of the pull request
329 :param user_id: reviewer user of the pull request
330 :param offset: pagination offset
330 :param offset: pagination offset
331 :param length: length of returned list
331 :param length: length of returned list
332 :param order_by: order of the returned list
332 :param order_by: order of the returned list
333 :param order_dir: 'asc' or 'desc' ordering direction
333 :param order_dir: 'asc' or 'desc' ordering direction
334 :returns: list of pull requests
334 :returns: list of pull requests
335 """
335 """
336 pull_requests = self.get_all(
336 pull_requests = self.get_all(
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 order_by=order_by, order_dir=order_dir)
338 order_by=order_by, order_dir=order_dir)
339
339
340 _my = PullRequestModel().get_not_reviewed(user_id)
340 _my = PullRequestModel().get_not_reviewed(user_id)
341 my_participation = []
341 my_participation = []
342 for pr in pull_requests:
342 for pr in pull_requests:
343 if pr in _my:
343 if pr in _my:
344 my_participation.append(pr)
344 my_participation.append(pr)
345 _filtered_pull_requests = my_participation
345 _filtered_pull_requests = my_participation
346 if length:
346 if length:
347 return _filtered_pull_requests[offset:offset+length]
347 return _filtered_pull_requests[offset:offset+length]
348 else:
348 else:
349 return _filtered_pull_requests
349 return _filtered_pull_requests
350
350
351 def get_not_reviewed(self, user_id):
351 def get_not_reviewed(self, user_id):
352 return [
352 return [
353 x.pull_request for x in PullRequestReviewers.query().filter(
353 x.pull_request for x in PullRequestReviewers.query().filter(
354 PullRequestReviewers.user_id == user_id).all()
354 PullRequestReviewers.user_id == user_id).all()
355 ]
355 ]
356
356
357 def _prepare_participating_query(self, user_id=None, statuses=None,
357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 order_by=None, order_dir='desc'):
358 order_by=None, order_dir='desc'):
359 q = PullRequest.query()
359 q = PullRequest.query()
360 if user_id:
360 if user_id:
361 reviewers_subquery = Session().query(
361 reviewers_subquery = Session().query(
362 PullRequestReviewers.pull_request_id).filter(
362 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.user_id == user_id).subquery()
363 PullRequestReviewers.user_id == user_id).subquery()
364 user_filter= or_(
364 user_filter= or_(
365 PullRequest.user_id == user_id,
365 PullRequest.user_id == user_id,
366 PullRequest.pull_request_id.in_(reviewers_subquery)
366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 )
367 )
368 q = PullRequest.query().filter(user_filter)
368 q = PullRequest.query().filter(user_filter)
369
369
370 # closed,opened
370 # closed,opened
371 if statuses:
371 if statuses:
372 q = q.filter(PullRequest.status.in_(statuses))
372 q = q.filter(PullRequest.status.in_(statuses))
373
373
374 if order_by:
374 if order_by:
375 order_map = {
375 order_map = {
376 'name_raw': PullRequest.pull_request_id,
376 'name_raw': PullRequest.pull_request_id,
377 'title': PullRequest.title,
377 'title': PullRequest.title,
378 'updated_on_raw': PullRequest.updated_on,
378 'updated_on_raw': PullRequest.updated_on,
379 'target_repo': PullRequest.target_repo_id
379 'target_repo': PullRequest.target_repo_id
380 }
380 }
381 if order_dir == 'asc':
381 if order_dir == 'asc':
382 q = q.order_by(order_map[order_by].asc())
382 q = q.order_by(order_map[order_by].asc())
383 else:
383 else:
384 q = q.order_by(order_map[order_by].desc())
384 q = q.order_by(order_map[order_by].desc())
385
385
386 return q
386 return q
387
387
388 def count_im_participating_in(self, user_id=None, statuses=None):
388 def count_im_participating_in(self, user_id=None, statuses=None):
389 q = self._prepare_participating_query(user_id, statuses=statuses)
389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 return q.count()
390 return q.count()
391
391
392 def get_im_participating_in(
392 def get_im_participating_in(
393 self, user_id=None, statuses=None, offset=0,
393 self, user_id=None, statuses=None, offset=0,
394 length=None, order_by=None, order_dir='desc'):
394 length=None, order_by=None, order_dir='desc'):
395 """
395 """
396 Get all Pull requests that i'm participating in, or i have opened
396 Get all Pull requests that i'm participating in, or i have opened
397 """
397 """
398
398
399 q = self._prepare_participating_query(
399 q = self._prepare_participating_query(
400 user_id, statuses=statuses, order_by=order_by,
400 user_id, statuses=statuses, order_by=order_by,
401 order_dir=order_dir)
401 order_dir=order_dir)
402
402
403 if length:
403 if length:
404 pull_requests = q.limit(length).offset(offset).all()
404 pull_requests = q.limit(length).offset(offset).all()
405 else:
405 else:
406 pull_requests = q.all()
406 pull_requests = q.all()
407
407
408 return pull_requests
408 return pull_requests
409
409
410 def get_versions(self, pull_request):
410 def get_versions(self, pull_request):
411 """
411 """
412 returns version of pull request sorted by ID descending
412 returns version of pull request sorted by ID descending
413 """
413 """
414 return PullRequestVersion.query()\
414 return PullRequestVersion.query()\
415 .filter(PullRequestVersion.pull_request == pull_request)\
415 .filter(PullRequestVersion.pull_request == pull_request)\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 .all()
417 .all()
418
418
419 def create(self, created_by, source_repo, source_ref, target_repo,
419 def create(self, created_by, source_repo, source_ref, target_repo,
420 target_ref, revisions, reviewers, title, description=None,
420 target_ref, revisions, reviewers, title, description=None,
421 reviewer_data=None):
421 reviewer_data=None):
422
422
423 created_by_user = self._get_user(created_by)
423 created_by_user = self._get_user(created_by)
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.author = created_by_user
435 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
436 pull_request.reviewer_data = reviewer_data
437
437
438 Session().add(pull_request)
438 Session().add(pull_request)
439 Session().flush()
439 Session().flush()
440
440
441 reviewer_ids = set()
441 reviewer_ids = set()
442 # members / reviewers
442 # members / reviewers
443 for reviewer_object in reviewers:
443 for reviewer_object in reviewers:
444 user_id, reasons, mandatory = reviewer_object
444 user_id, reasons, mandatory = reviewer_object
445 user = self._get_user(user_id)
445 user = self._get_user(user_id)
446
446
447 # skip duplicates
447 # skip duplicates
448 if user.user_id in reviewer_ids:
448 if user.user_id in reviewer_ids:
449 continue
449 continue
450
450
451 reviewer_ids.add(user.user_id)
451 reviewer_ids.add(user.user_id)
452
452
453 reviewer = PullRequestReviewers()
453 reviewer = PullRequestReviewers()
454 reviewer.user = user
454 reviewer.user = user
455 reviewer.pull_request = pull_request
455 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
456 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
457 reviewer.mandatory = mandatory
458 Session().add(reviewer)
458 Session().add(reviewer)
459
459
460 # Set approval status to "Under Review" for all commits which are
460 # Set approval status to "Under Review" for all commits which are
461 # part of this pull request.
461 # part of this pull request.
462 ChangesetStatusModel().set_status(
462 ChangesetStatusModel().set_status(
463 repo=target_repo,
463 repo=target_repo,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 user=created_by_user,
465 user=created_by_user,
466 pull_request=pull_request
466 pull_request=pull_request
467 )
467 )
468
468
469 self.notify_reviewers(pull_request, reviewer_ids)
469 self.notify_reviewers(pull_request, reviewer_ids)
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
476 created_by_user, pull_request)
477
477
478 return pull_request
478 return pull_request
479
479
480 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 pull_request = self.__get_pull_request(pull_request)
481 pull_request = self.__get_pull_request(pull_request)
482 target_scm = pull_request.target_repo.scm_instance()
482 target_scm = pull_request.target_repo.scm_instance()
483 if action == 'create':
483 if action == 'create':
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 elif action == 'merge':
485 elif action == 'merge':
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 elif action == 'close':
487 elif action == 'close':
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 elif action == 'review_status_change':
489 elif action == 'review_status_change':
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 elif action == 'update':
491 elif action == 'update':
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 else:
493 else:
494 return
494 return
495
495
496 trigger_hook(
496 trigger_hook(
497 username=user.username,
497 username=user.username,
498 repo_name=pull_request.target_repo.repo_name,
498 repo_name=pull_request.target_repo.repo_name,
499 repo_alias=target_scm.alias,
499 repo_alias=target_scm.alias,
500 pull_request=pull_request)
500 pull_request=pull_request)
501
501
502 def _get_commit_ids(self, pull_request):
502 def _get_commit_ids(self, pull_request):
503 """
503 """
504 Return the commit ids of the merged pull request.
504 Return the commit ids of the merged pull request.
505
505
506 This method is not dealing correctly yet with the lack of autoupdates
506 This method is not dealing correctly yet with the lack of autoupdates
507 nor with the implicit target updates.
507 nor with the implicit target updates.
508 For example: if a commit in the source repo is already in the target it
508 For example: if a commit in the source repo is already in the target it
509 will be reported anyways.
509 will be reported anyways.
510 """
510 """
511 merge_rev = pull_request.merge_rev
511 merge_rev = pull_request.merge_rev
512 if merge_rev is None:
512 if merge_rev is None:
513 raise ValueError('This pull request was not merged yet')
513 raise ValueError('This pull request was not merged yet')
514
514
515 commit_ids = list(pull_request.revisions)
515 commit_ids = list(pull_request.revisions)
516 if merge_rev not in commit_ids:
516 if merge_rev not in commit_ids:
517 commit_ids.append(merge_rev)
517 commit_ids.append(merge_rev)
518
518
519 return commit_ids
519 return commit_ids
520
520
521 def merge(self, pull_request, user, extras):
521 def merge(self, pull_request, user, extras):
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 if merge_state.executed:
524 if merge_state.executed:
525 log.debug(
525 log.debug(
526 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
527 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
528
528
529 self._log_audit_action(
529 self._log_audit_action(
530 'repo.pull_request.merge',
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
532 user, pull_request)
533
533
534 else:
534 else:
535 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
536 return merge_state
536 return merge_state
537
537
538 def _merge_pull_request(self, pull_request, user, extras):
538 def _merge_pull_request(self, pull_request, user, extras):
539 target_vcs = pull_request.target_repo.scm_instance()
539 target_vcs = pull_request.target_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
540 source_vcs = pull_request.source_repo.scm_instance()
541 target_ref = self._refresh_reference(
541 target_ref = self._refresh_reference(
542 pull_request.target_ref_parts, target_vcs)
542 pull_request.target_ref_parts, target_vcs)
543
543
544 message = _(
544 message = _(
545 'Merge pull request #%(pr_id)s from '
545 'Merge pull request #%(pr_id)s from '
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 'pr_id': pull_request.pull_request_id,
547 'pr_id': pull_request.pull_request_id,
548 'source_repo': source_vcs.name,
548 'source_repo': source_vcs.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
549 'source_ref_name': pull_request.source_ref_parts.name,
550 'pr_title': pull_request.title
550 'pr_title': pull_request.title
551 }
551 }
552
552
553 workspace_id = self._workspace_id(pull_request)
553 workspace_id = self._workspace_id(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
554 use_rebase = self._use_rebase_for_merging(pull_request)
555
555
556 callback_daemon, extras = prepare_callback_daemon(
556 callback_daemon, extras = prepare_callback_daemon(
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559
559
560 with callback_daemon:
560 with callback_daemon:
561 # TODO: johbo: Implement a clean way to run a config_override
561 # TODO: johbo: Implement a clean way to run a config_override
562 # for a single call.
562 # for a single call.
563 target_vcs.config.set(
563 target_vcs.config.set(
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 merge_state = target_vcs.merge(
565 merge_state = target_vcs.merge(
566 target_ref, source_vcs, pull_request.source_ref_parts,
566 target_ref, source_vcs, pull_request.source_ref_parts,
567 workspace_id, user_name=user.username,
567 workspace_id, user_name=user.username,
568 user_email=user.email, message=message, use_rebase=use_rebase)
568 user_email=user.email, message=message, use_rebase=use_rebase)
569 return merge_state
569 return merge_state
570
570
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 pull_request.updated_on = datetime.datetime.now()
573 pull_request.updated_on = datetime.datetime.now()
574
574
575 CommentsModel().create(
575 CommentsModel().create(
576 text=unicode(_('Pull request merged and closed')),
576 text=unicode(_('Pull request merged and closed')),
577 repo=pull_request.target_repo.repo_id,
577 repo=pull_request.target_repo.repo_id,
578 user=user.user_id,
578 user=user.user_id,
579 pull_request=pull_request.pull_request_id,
579 pull_request=pull_request.pull_request_id,
580 f_path=None,
580 f_path=None,
581 line_no=None,
581 line_no=None,
582 closing_pr=True
582 closing_pr=True
583 )
583 )
584
584
585 Session().add(pull_request)
585 Session().add(pull_request)
586 Session().flush()
586 Session().flush()
587 # TODO: paris: replace invalidation with less radical solution
587 # TODO: paris: replace invalidation with less radical solution
588 ScmModel().mark_for_invalidation(
588 ScmModel().mark_for_invalidation(
589 pull_request.target_repo.repo_name)
589 pull_request.target_repo.repo_name)
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591
591
592 def has_valid_update_type(self, pull_request):
592 def has_valid_update_type(self, pull_request):
593 source_ref_type = pull_request.source_ref_parts.type
593 source_ref_type = pull_request.source_ref_parts.type
594 return source_ref_type in ['book', 'branch', 'tag']
594 return source_ref_type in ['book', 'branch', 'tag']
595
595
596 def update_commits(self, pull_request):
596 def update_commits(self, pull_request):
597 """
597 """
598 Get the updated list of commits for the pull request
598 Get the updated list of commits for the pull request
599 and return the new pull request version and the list
599 and return the new pull request version and the list
600 of commits processed by this update action
600 of commits processed by this update action
601 """
601 """
602 pull_request = self.__get_pull_request(pull_request)
602 pull_request = self.__get_pull_request(pull_request)
603 source_ref_type = pull_request.source_ref_parts.type
603 source_ref_type = pull_request.source_ref_parts.type
604 source_ref_name = pull_request.source_ref_parts.name
604 source_ref_name = pull_request.source_ref_parts.name
605 source_ref_id = pull_request.source_ref_parts.commit_id
605 source_ref_id = pull_request.source_ref_parts.commit_id
606
606
607 target_ref_type = pull_request.target_ref_parts.type
607 target_ref_type = pull_request.target_ref_parts.type
608 target_ref_name = pull_request.target_ref_parts.name
608 target_ref_name = pull_request.target_ref_parts.name
609 target_ref_id = pull_request.target_ref_parts.commit_id
609 target_ref_id = pull_request.target_ref_parts.commit_id
610
610
611 if not self.has_valid_update_type(pull_request):
611 if not self.has_valid_update_type(pull_request):
612 log.debug(
612 log.debug(
613 "Skipping update of pull request %s due to ref type: %s",
613 "Skipping update of pull request %s due to ref type: %s",
614 pull_request, source_ref_type)
614 pull_request, source_ref_type)
615 return UpdateResponse(
615 return UpdateResponse(
616 executed=False,
616 executed=False,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 old=pull_request, new=None, changes=None,
618 old=pull_request, new=None, changes=None,
619 source_changed=False, target_changed=False)
619 source_changed=False, target_changed=False)
620
620
621 # source repo
621 # source repo
622 source_repo = pull_request.source_repo.scm_instance()
622 source_repo = pull_request.source_repo.scm_instance()
623 try:
623 try:
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 except CommitDoesNotExistError:
625 except CommitDoesNotExistError:
626 return UpdateResponse(
626 return UpdateResponse(
627 executed=False,
627 executed=False,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 old=pull_request, new=None, changes=None,
629 old=pull_request, new=None, changes=None,
630 source_changed=False, target_changed=False)
630 source_changed=False, target_changed=False)
631
631
632 source_changed = source_ref_id != source_commit.raw_id
632 source_changed = source_ref_id != source_commit.raw_id
633
633
634 # target repo
634 # target repo
635 target_repo = pull_request.target_repo.scm_instance()
635 target_repo = pull_request.target_repo.scm_instance()
636 try:
636 try:
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 except CommitDoesNotExistError:
638 except CommitDoesNotExistError:
639 return UpdateResponse(
639 return UpdateResponse(
640 executed=False,
640 executed=False,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 old=pull_request, new=None, changes=None,
642 old=pull_request, new=None, changes=None,
643 source_changed=False, target_changed=False)
643 source_changed=False, target_changed=False)
644 target_changed = target_ref_id != target_commit.raw_id
644 target_changed = target_ref_id != target_commit.raw_id
645
645
646 if not (source_changed or target_changed):
646 if not (source_changed or target_changed):
647 log.debug("Nothing changed in pull request %s", pull_request)
647 log.debug("Nothing changed in pull request %s", pull_request)
648 return UpdateResponse(
648 return UpdateResponse(
649 executed=False,
649 executed=False,
650 reason=UpdateFailureReason.NO_CHANGE,
650 reason=UpdateFailureReason.NO_CHANGE,
651 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
652 source_changed=target_changed, target_changed=source_changed)
652 source_changed=target_changed, target_changed=source_changed)
653
653
654 change_in_found = 'target repo' if target_changed else 'source repo'
654 change_in_found = 'target repo' if target_changed else 'source repo'
655 log.debug('Updating pull request because of change in %s detected',
655 log.debug('Updating pull request because of change in %s detected',
656 change_in_found)
656 change_in_found)
657
657
658 # Finally there is a need for an update, in case of source change
658 # Finally there is a need for an update, in case of source change
659 # we create a new version, else just an update
659 # we create a new version, else just an update
660 if source_changed:
660 if source_changed:
661 pull_request_version = self._create_version_from_snapshot(pull_request)
661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 self._link_comments_to_version(pull_request_version)
662 self._link_comments_to_version(pull_request_version)
663 else:
663 else:
664 try:
664 try:
665 ver = pull_request.versions[-1]
665 ver = pull_request.versions[-1]
666 except IndexError:
666 except IndexError:
667 ver = None
667 ver = None
668
668
669 pull_request.pull_request_version_id = \
669 pull_request.pull_request_version_id = \
670 ver.pull_request_version_id if ver else None
670 ver.pull_request_version_id if ver else None
671 pull_request_version = pull_request
671 pull_request_version = pull_request
672
672
673 try:
673 try:
674 if target_ref_type in ('tag', 'branch', 'book'):
674 if target_ref_type in ('tag', 'branch', 'book'):
675 target_commit = target_repo.get_commit(target_ref_name)
675 target_commit = target_repo.get_commit(target_ref_name)
676 else:
676 else:
677 target_commit = target_repo.get_commit(target_ref_id)
677 target_commit = target_repo.get_commit(target_ref_id)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 return UpdateResponse(
679 return UpdateResponse(
680 executed=False,
680 executed=False,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 old=pull_request, new=None, changes=None,
682 old=pull_request, new=None, changes=None,
683 source_changed=source_changed, target_changed=target_changed)
683 source_changed=source_changed, target_changed=target_changed)
684
684
685 # re-compute commit ids
685 # re-compute commit ids
686 old_commit_ids = pull_request.revisions
686 old_commit_ids = pull_request.revisions
687 pre_load = ["author", "branch", "date", "message"]
687 pre_load = ["author", "branch", "date", "message"]
688 commit_ranges = target_repo.compare(
688 commit_ranges = target_repo.compare(
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 pre_load=pre_load)
690 pre_load=pre_load)
691
691
692 ancestor = target_repo.get_common_ancestor(
692 ancestor = target_repo.get_common_ancestor(
693 target_commit.raw_id, source_commit.raw_id, source_repo)
693 target_commit.raw_id, source_commit.raw_id, source_repo)
694
694
695 pull_request.source_ref = '%s:%s:%s' % (
695 pull_request.source_ref = '%s:%s:%s' % (
696 source_ref_type, source_ref_name, source_commit.raw_id)
696 source_ref_type, source_ref_name, source_commit.raw_id)
697 pull_request.target_ref = '%s:%s:%s' % (
697 pull_request.target_ref = '%s:%s:%s' % (
698 target_ref_type, target_ref_name, ancestor)
698 target_ref_type, target_ref_name, ancestor)
699
699
700 pull_request.revisions = [
700 pull_request.revisions = [
701 commit.raw_id for commit in reversed(commit_ranges)]
701 commit.raw_id for commit in reversed(commit_ranges)]
702 pull_request.updated_on = datetime.datetime.now()
702 pull_request.updated_on = datetime.datetime.now()
703 Session().add(pull_request)
703 Session().add(pull_request)
704 new_commit_ids = pull_request.revisions
704 new_commit_ids = pull_request.revisions
705
705
706 old_diff_data, new_diff_data = self._generate_update_diffs(
706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 pull_request, pull_request_version)
707 pull_request, pull_request_version)
708
708
709 # calculate commit and file changes
709 # calculate commit and file changes
710 changes = self._calculate_commit_id_changes(
710 changes = self._calculate_commit_id_changes(
711 old_commit_ids, new_commit_ids)
711 old_commit_ids, new_commit_ids)
712 file_changes = self._calculate_file_changes(
712 file_changes = self._calculate_file_changes(
713 old_diff_data, new_diff_data)
713 old_diff_data, new_diff_data)
714
714
715 # set comments as outdated if DIFFS changed
715 # set comments as outdated if DIFFS changed
716 CommentsModel().outdate_comments(
716 CommentsModel().outdate_comments(
717 pull_request, old_diff_data=old_diff_data,
717 pull_request, old_diff_data=old_diff_data,
718 new_diff_data=new_diff_data)
718 new_diff_data=new_diff_data)
719
719
720 commit_changes = (changes.added or changes.removed)
720 commit_changes = (changes.added or changes.removed)
721 file_node_changes = (
721 file_node_changes = (
722 file_changes.added or file_changes.modified or file_changes.removed)
722 file_changes.added or file_changes.modified or file_changes.removed)
723 pr_has_changes = commit_changes or file_node_changes
723 pr_has_changes = commit_changes or file_node_changes
724
724
725 # Add an automatic comment to the pull request, in case
725 # Add an automatic comment to the pull request, in case
726 # anything has changed
726 # anything has changed
727 if pr_has_changes:
727 if pr_has_changes:
728 update_comment = CommentsModel().create(
728 update_comment = CommentsModel().create(
729 text=self._render_update_message(changes, file_changes),
729 text=self._render_update_message(changes, file_changes),
730 repo=pull_request.target_repo,
730 repo=pull_request.target_repo,
731 user=pull_request.author,
731 user=pull_request.author,
732 pull_request=pull_request,
732 pull_request=pull_request,
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734
734
735 # Update status to "Under Review" for added commits
735 # Update status to "Under Review" for added commits
736 for commit_id in changes.added:
736 for commit_id in changes.added:
737 ChangesetStatusModel().set_status(
737 ChangesetStatusModel().set_status(
738 repo=pull_request.source_repo,
738 repo=pull_request.source_repo,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 comment=update_comment,
740 comment=update_comment,
741 user=pull_request.author,
741 user=pull_request.author,
742 pull_request=pull_request,
742 pull_request=pull_request,
743 revision=commit_id)
743 revision=commit_id)
744
744
745 log.debug(
745 log.debug(
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 'removed_ids: %s', pull_request.pull_request_id,
747 'removed_ids: %s', pull_request.pull_request_id,
748 changes.added, changes.common, changes.removed)
748 changes.added, changes.common, changes.removed)
749 log.debug(
749 log.debug(
750 'Updated pull request with the following file changes: %s',
750 'Updated pull request with the following file changes: %s',
751 file_changes)
751 file_changes)
752
752
753 log.info(
753 log.info(
754 "Updated pull request %s from commit %s to commit %s, "
754 "Updated pull request %s from commit %s to commit %s, "
755 "stored new version %s of this pull request.",
755 "stored new version %s of this pull request.",
756 pull_request.pull_request_id, source_ref_id,
756 pull_request.pull_request_id, source_ref_id,
757 pull_request.source_ref_parts.commit_id,
757 pull_request.source_ref_parts.commit_id,
758 pull_request_version.pull_request_version_id)
758 pull_request_version.pull_request_version_id)
759 Session().commit()
759 Session().commit()
760 self._trigger_pull_request_hook(
760 self._trigger_pull_request_hook(
761 pull_request, pull_request.author, 'update')
761 pull_request, pull_request.author, 'update')
762
762
763 return UpdateResponse(
763 return UpdateResponse(
764 executed=True, reason=UpdateFailureReason.NONE,
764 executed=True, reason=UpdateFailureReason.NONE,
765 old=pull_request, new=pull_request_version, changes=changes,
765 old=pull_request, new=pull_request_version, changes=changes,
766 source_changed=source_changed, target_changed=target_changed)
766 source_changed=source_changed, target_changed=target_changed)
767
767
768 def _create_version_from_snapshot(self, pull_request):
768 def _create_version_from_snapshot(self, pull_request):
769 version = PullRequestVersion()
769 version = PullRequestVersion()
770 version.title = pull_request.title
770 version.title = pull_request.title
771 version.description = pull_request.description
771 version.description = pull_request.description
772 version.status = pull_request.status
772 version.status = pull_request.status
773 version.created_on = datetime.datetime.now()
773 version.created_on = datetime.datetime.now()
774 version.updated_on = pull_request.updated_on
774 version.updated_on = pull_request.updated_on
775 version.user_id = pull_request.user_id
775 version.user_id = pull_request.user_id
776 version.source_repo = pull_request.source_repo
776 version.source_repo = pull_request.source_repo
777 version.source_ref = pull_request.source_ref
777 version.source_ref = pull_request.source_ref
778 version.target_repo = pull_request.target_repo
778 version.target_repo = pull_request.target_repo
779 version.target_ref = pull_request.target_ref
779 version.target_ref = pull_request.target_ref
780
780
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 version._last_merge_status = pull_request._last_merge_status
783 version.last_merge_status = pull_request.last_merge_status
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 version.merge_rev = pull_request.merge_rev
785 version.merge_rev = pull_request.merge_rev
786 version.reviewer_data = pull_request.reviewer_data
786 version.reviewer_data = pull_request.reviewer_data
787
787
788 version.revisions = pull_request.revisions
788 version.revisions = pull_request.revisions
789 version.pull_request = pull_request
789 version.pull_request = pull_request
790 Session().add(version)
790 Session().add(version)
791 Session().flush()
791 Session().flush()
792
792
793 return version
793 return version
794
794
795 def _generate_update_diffs(self, pull_request, pull_request_version):
795 def _generate_update_diffs(self, pull_request, pull_request_version):
796
796
797 diff_context = (
797 diff_context = (
798 self.DIFF_CONTEXT +
798 self.DIFF_CONTEXT +
799 CommentsModel.needed_extra_diff_context())
799 CommentsModel.needed_extra_diff_context())
800
800
801 source_repo = pull_request_version.source_repo
801 source_repo = pull_request_version.source_repo
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 old_diff = self._get_diff_from_pr_or_version(
804 old_diff = self._get_diff_from_pr_or_version(
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806
806
807 source_repo = pull_request.source_repo
807 source_repo = pull_request.source_repo
808 source_ref_id = pull_request.source_ref_parts.commit_id
808 source_ref_id = pull_request.source_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
809 target_ref_id = pull_request.target_ref_parts.commit_id
810
810
811 new_diff = self._get_diff_from_pr_or_version(
811 new_diff = self._get_diff_from_pr_or_version(
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813
813
814 old_diff_data = diffs.DiffProcessor(old_diff)
814 old_diff_data = diffs.DiffProcessor(old_diff)
815 old_diff_data.prepare()
815 old_diff_data.prepare()
816 new_diff_data = diffs.DiffProcessor(new_diff)
816 new_diff_data = diffs.DiffProcessor(new_diff)
817 new_diff_data.prepare()
817 new_diff_data.prepare()
818
818
819 return old_diff_data, new_diff_data
819 return old_diff_data, new_diff_data
820
820
821 def _link_comments_to_version(self, pull_request_version):
821 def _link_comments_to_version(self, pull_request_version):
822 """
822 """
823 Link all unlinked comments of this pull request to the given version.
823 Link all unlinked comments of this pull request to the given version.
824
824
825 :param pull_request_version: The `PullRequestVersion` to which
825 :param pull_request_version: The `PullRequestVersion` to which
826 the comments shall be linked.
826 the comments shall be linked.
827
827
828 """
828 """
829 pull_request = pull_request_version.pull_request
829 pull_request = pull_request_version.pull_request
830 comments = ChangesetComment.query()\
830 comments = ChangesetComment.query()\
831 .filter(
831 .filter(
832 # TODO: johbo: Should we query for the repo at all here?
832 # TODO: johbo: Should we query for the repo at all here?
833 # Pending decision on how comments of PRs are to be related
833 # Pending decision on how comments of PRs are to be related
834 # to either the source repo, the target repo or no repo at all.
834 # to either the source repo, the target repo or no repo at all.
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 ChangesetComment.pull_request == pull_request,
836 ChangesetComment.pull_request == pull_request,
837 ChangesetComment.pull_request_version == None)\
837 ChangesetComment.pull_request_version == None)\
838 .order_by(ChangesetComment.comment_id.asc())
838 .order_by(ChangesetComment.comment_id.asc())
839
839
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 # operation.
841 # operation.
842 for comment in comments:
842 for comment in comments:
843 comment.pull_request_version_id = (
843 comment.pull_request_version_id = (
844 pull_request_version.pull_request_version_id)
844 pull_request_version.pull_request_version_id)
845 Session().add(comment)
845 Session().add(comment)
846
846
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 added = [x for x in new_ids if x not in old_ids]
848 added = [x for x in new_ids if x not in old_ids]
849 common = [x for x in new_ids if x in old_ids]
849 common = [x for x in new_ids if x in old_ids]
850 removed = [x for x in old_ids if x not in new_ids]
850 removed = [x for x in old_ids if x not in new_ids]
851 total = new_ids
851 total = new_ids
852 return ChangeTuple(added, common, removed, total)
852 return ChangeTuple(added, common, removed, total)
853
853
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855
855
856 old_files = OrderedDict()
856 old_files = OrderedDict()
857 for diff_data in old_diff_data.parsed_diff:
857 for diff_data in old_diff_data.parsed_diff:
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859
859
860 added_files = []
860 added_files = []
861 modified_files = []
861 modified_files = []
862 removed_files = []
862 removed_files = []
863 for diff_data in new_diff_data.parsed_diff:
863 for diff_data in new_diff_data.parsed_diff:
864 new_filename = diff_data['filename']
864 new_filename = diff_data['filename']
865 new_hash = md5_safe(diff_data['raw_diff'])
865 new_hash = md5_safe(diff_data['raw_diff'])
866
866
867 old_hash = old_files.get(new_filename)
867 old_hash = old_files.get(new_filename)
868 if not old_hash:
868 if not old_hash:
869 # file is not present in old diff, means it's added
869 # file is not present in old diff, means it's added
870 added_files.append(new_filename)
870 added_files.append(new_filename)
871 else:
871 else:
872 if new_hash != old_hash:
872 if new_hash != old_hash:
873 modified_files.append(new_filename)
873 modified_files.append(new_filename)
874 # now remove a file from old, since we have seen it already
874 # now remove a file from old, since we have seen it already
875 del old_files[new_filename]
875 del old_files[new_filename]
876
876
877 # removed files is when there are present in old, but not in NEW,
877 # removed files is when there are present in old, but not in NEW,
878 # since we remove old files that are present in new diff, left-overs
878 # since we remove old files that are present in new diff, left-overs
879 # if any should be the removed files
879 # if any should be the removed files
880 removed_files.extend(old_files.keys())
880 removed_files.extend(old_files.keys())
881
881
882 return FileChangeTuple(added_files, modified_files, removed_files)
882 return FileChangeTuple(added_files, modified_files, removed_files)
883
883
884 def _render_update_message(self, changes, file_changes):
884 def _render_update_message(self, changes, file_changes):
885 """
885 """
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 so it's always looking the same disregarding on which default
887 so it's always looking the same disregarding on which default
888 renderer system is using.
888 renderer system is using.
889
889
890 :param changes: changes named tuple
890 :param changes: changes named tuple
891 :param file_changes: file changes named tuple
891 :param file_changes: file changes named tuple
892
892
893 """
893 """
894 new_status = ChangesetStatus.get_status_lbl(
894 new_status = ChangesetStatus.get_status_lbl(
895 ChangesetStatus.STATUS_UNDER_REVIEW)
895 ChangesetStatus.STATUS_UNDER_REVIEW)
896
896
897 changed_files = (
897 changed_files = (
898 file_changes.added + file_changes.modified + file_changes.removed)
898 file_changes.added + file_changes.modified + file_changes.removed)
899
899
900 params = {
900 params = {
901 'under_review_label': new_status,
901 'under_review_label': new_status,
902 'added_commits': changes.added,
902 'added_commits': changes.added,
903 'removed_commits': changes.removed,
903 'removed_commits': changes.removed,
904 'changed_files': changed_files,
904 'changed_files': changed_files,
905 'added_files': file_changes.added,
905 'added_files': file_changes.added,
906 'modified_files': file_changes.modified,
906 'modified_files': file_changes.modified,
907 'removed_files': file_changes.removed,
907 'removed_files': file_changes.removed,
908 }
908 }
909 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
910 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
911
911
912 def edit(self, pull_request, title, description, user):
912 def edit(self, pull_request, title, description, user):
913 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
914 old_data = pull_request.get_api_data(with_merge_state=False)
915 if pull_request.is_closed():
915 if pull_request.is_closed():
916 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
917 if title:
917 if title:
918 pull_request.title = title
918 pull_request.title = title
919 pull_request.description = description
919 pull_request.description = description
920 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
921 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
924 user, pull_request)
925
925
926 def update_reviewers(self, pull_request, reviewer_data, user):
926 def update_reviewers(self, pull_request, reviewer_data, user):
927 """
927 """
928 Update the reviewers in the pull request
928 Update the reviewers in the pull request
929
929
930 :param pull_request: the pr to update
930 :param pull_request: the pr to update
931 :param reviewer_data: list of tuples
931 :param reviewer_data: list of tuples
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 """
933 """
934
934
935 reviewers = {}
935 reviewers = {}
936 for user_id, reasons, mandatory in reviewer_data:
936 for user_id, reasons, mandatory in reviewer_data:
937 if isinstance(user_id, (int, basestring)):
937 if isinstance(user_id, (int, basestring)):
938 user_id = self._get_user(user_id).user_id
938 user_id = self._get_user(user_id).user_id
939 reviewers[user_id] = {
939 reviewers[user_id] = {
940 'reasons': reasons, 'mandatory': mandatory}
940 'reasons': reasons, 'mandatory': mandatory}
941
941
942 reviewers_ids = set(reviewers.keys())
942 reviewers_ids = set(reviewers.keys())
943 pull_request = self.__get_pull_request(pull_request)
943 pull_request = self.__get_pull_request(pull_request)
944 current_reviewers = PullRequestReviewers.query()\
944 current_reviewers = PullRequestReviewers.query()\
945 .filter(PullRequestReviewers.pull_request ==
945 .filter(PullRequestReviewers.pull_request ==
946 pull_request).all()
946 pull_request).all()
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948
948
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951
951
952 log.debug("Adding %s reviewers", ids_to_add)
952 log.debug("Adding %s reviewers", ids_to_add)
953 log.debug("Removing %s reviewers", ids_to_remove)
953 log.debug("Removing %s reviewers", ids_to_remove)
954 changed = False
954 changed = False
955 for uid in ids_to_add:
955 for uid in ids_to_add:
956 changed = True
956 changed = True
957 _usr = self._get_user(uid)
957 _usr = self._get_user(uid)
958 reviewer = PullRequestReviewers()
958 reviewer = PullRequestReviewers()
959 reviewer.user = _usr
959 reviewer.user = _usr
960 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
961 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
962 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
963 # reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
964 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
967 user, pull_request)
968
968
969 for uid in ids_to_remove:
969 for uid in ids_to_remove:
970 changed = True
970 changed = True
971 reviewers = PullRequestReviewers.query()\
971 reviewers = PullRequestReviewers.query()\
972 .filter(PullRequestReviewers.user_id == uid,
972 .filter(PullRequestReviewers.user_id == uid,
973 PullRequestReviewers.pull_request == pull_request)\
973 PullRequestReviewers.pull_request == pull_request)\
974 .all()
974 .all()
975 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
976 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
977 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
978 old_data = obj.get_dict()
979 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
982 {'old_data': old_data}, user, pull_request)
983
983
984 if changed:
984 if changed:
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
986 Session().add(pull_request)
986 Session().add(pull_request)
987
987
988 self.notify_reviewers(pull_request, ids_to_add)
988 self.notify_reviewers(pull_request, ids_to_add)
989 return ids_to_add, ids_to_remove
989 return ids_to_add, ids_to_remove
990
990
991 def get_url(self, pull_request, request=None, permalink=False):
991 def get_url(self, pull_request, request=None, permalink=False):
992 if not request:
992 if not request:
993 request = get_current_request()
993 request = get_current_request()
994
994
995 if permalink:
995 if permalink:
996 return request.route_url(
996 return request.route_url(
997 'pull_requests_global',
997 'pull_requests_global',
998 pull_request_id=pull_request.pull_request_id,)
998 pull_request_id=pull_request.pull_request_id,)
999 else:
999 else:
1000 return request.route_url('pullrequest_show',
1000 return request.route_url('pullrequest_show',
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 pull_request_id=pull_request.pull_request_id,)
1002 pull_request_id=pull_request.pull_request_id,)
1003
1003
1004 def get_shadow_clone_url(self, pull_request):
1004 def get_shadow_clone_url(self, pull_request):
1005 """
1005 """
1006 Returns qualified url pointing to the shadow repository. If this pull
1006 Returns qualified url pointing to the shadow repository. If this pull
1007 request is closed there is no shadow repository and ``None`` will be
1007 request is closed there is no shadow repository and ``None`` will be
1008 returned.
1008 returned.
1009 """
1009 """
1010 if pull_request.is_closed():
1010 if pull_request.is_closed():
1011 return None
1011 return None
1012 else:
1012 else:
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1013 pr_url = urllib.unquote(self.get_url(pull_request))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015
1015
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1016 def notify_reviewers(self, pull_request, reviewers_ids):
1017 # notification to reviewers
1017 # notification to reviewers
1018 if not reviewers_ids:
1018 if not reviewers_ids:
1019 return
1019 return
1020
1020
1021 pull_request_obj = pull_request
1021 pull_request_obj = pull_request
1022 # get the current participants of this pull request
1022 # get the current participants of this pull request
1023 recipients = reviewers_ids
1023 recipients = reviewers_ids
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025
1025
1026 pr_source_repo = pull_request_obj.source_repo
1026 pr_source_repo = pull_request_obj.source_repo
1027 pr_target_repo = pull_request_obj.target_repo
1027 pr_target_repo = pull_request_obj.target_repo
1028
1028
1029 pr_url = h.route_url('pullrequest_show',
1029 pr_url = h.route_url('pullrequest_show',
1030 repo_name=pr_target_repo.repo_name,
1030 repo_name=pr_target_repo.repo_name,
1031 pull_request_id=pull_request_obj.pull_request_id,)
1031 pull_request_id=pull_request_obj.pull_request_id,)
1032
1032
1033 # set some variables for email notification
1033 # set some variables for email notification
1034 pr_target_repo_url = h.route_url(
1034 pr_target_repo_url = h.route_url(
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1036
1036
1037 pr_source_repo_url = h.route_url(
1037 pr_source_repo_url = h.route_url(
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1039
1039
1040 # pull request specifics
1040 # pull request specifics
1041 pull_request_commits = [
1041 pull_request_commits = [
1042 (x.raw_id, x.message)
1042 (x.raw_id, x.message)
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1044
1044
1045 kwargs = {
1045 kwargs = {
1046 'user': pull_request.author,
1046 'user': pull_request.author,
1047 'pull_request': pull_request_obj,
1047 'pull_request': pull_request_obj,
1048 'pull_request_commits': pull_request_commits,
1048 'pull_request_commits': pull_request_commits,
1049
1049
1050 'pull_request_target_repo': pr_target_repo,
1050 'pull_request_target_repo': pr_target_repo,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1051 'pull_request_target_repo_url': pr_target_repo_url,
1052
1052
1053 'pull_request_source_repo': pr_source_repo,
1053 'pull_request_source_repo': pr_source_repo,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1054 'pull_request_source_repo_url': pr_source_repo_url,
1055
1055
1056 'pull_request_url': pr_url,
1056 'pull_request_url': pr_url,
1057 }
1057 }
1058
1058
1059 # pre-generate the subject for notification itself
1059 # pre-generate the subject for notification itself
1060 (subject,
1060 (subject,
1061 _h, _e, # we don't care about those
1061 _h, _e, # we don't care about those
1062 body_plaintext) = EmailNotificationModel().render_email(
1062 body_plaintext) = EmailNotificationModel().render_email(
1063 notification_type, **kwargs)
1063 notification_type, **kwargs)
1064
1064
1065 # create notification objects, and emails
1065 # create notification objects, and emails
1066 NotificationModel().create(
1066 NotificationModel().create(
1067 created_by=pull_request.author,
1067 created_by=pull_request.author,
1068 notification_subject=subject,
1068 notification_subject=subject,
1069 notification_body=body_plaintext,
1069 notification_body=body_plaintext,
1070 notification_type=notification_type,
1070 notification_type=notification_type,
1071 recipients=recipients,
1071 recipients=recipients,
1072 email_kwargs=kwargs,
1072 email_kwargs=kwargs,
1073 )
1073 )
1074
1074
1075 def delete(self, pull_request, user):
1075 def delete(self, pull_request, user):
1076 pull_request = self.__get_pull_request(pull_request)
1076 pull_request = self.__get_pull_request(pull_request)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1077 old_data = pull_request.get_api_data(with_merge_state=False)
1078 self._cleanup_merge_workspace(pull_request)
1078 self._cleanup_merge_workspace(pull_request)
1079 self._log_audit_action(
1079 self._log_audit_action(
1080 'repo.pull_request.delete', {'old_data': old_data},
1080 'repo.pull_request.delete', {'old_data': old_data},
1081 user, pull_request)
1081 user, pull_request)
1082 Session().delete(pull_request)
1082 Session().delete(pull_request)
1083
1083
1084 def close_pull_request(self, pull_request, user):
1084 def close_pull_request(self, pull_request, user):
1085 pull_request = self.__get_pull_request(pull_request)
1085 pull_request = self.__get_pull_request(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1086 self._cleanup_merge_workspace(pull_request)
1087 pull_request.status = PullRequest.STATUS_CLOSED
1087 pull_request.status = PullRequest.STATUS_CLOSED
1088 pull_request.updated_on = datetime.datetime.now()
1088 pull_request.updated_on = datetime.datetime.now()
1089 Session().add(pull_request)
1089 Session().add(pull_request)
1090 self._trigger_pull_request_hook(
1090 self._trigger_pull_request_hook(
1091 pull_request, pull_request.author, 'close')
1091 pull_request, pull_request.author, 'close')
1092 self._log_audit_action(
1092 self._log_audit_action(
1093 'repo.pull_request.close', {}, user, pull_request)
1093 'repo.pull_request.close', {}, user, pull_request)
1094
1094
1095 def close_pull_request_with_comment(
1095 def close_pull_request_with_comment(
1096 self, pull_request, user, repo, message=None):
1096 self, pull_request, user, repo, message=None):
1097
1097
1098 pull_request_review_status = pull_request.calculated_review_status()
1098 pull_request_review_status = pull_request.calculated_review_status()
1099
1099
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1101 # approved only if we have voting consent
1101 # approved only if we have voting consent
1102 status = ChangesetStatus.STATUS_APPROVED
1102 status = ChangesetStatus.STATUS_APPROVED
1103 else:
1103 else:
1104 status = ChangesetStatus.STATUS_REJECTED
1104 status = ChangesetStatus.STATUS_REJECTED
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1106
1106
1107 default_message = (
1107 default_message = (
1108 _('Closing with status change {transition_icon} {status}.')
1108 _('Closing with status change {transition_icon} {status}.')
1109 ).format(transition_icon='>', status=status_lbl)
1109 ).format(transition_icon='>', status=status_lbl)
1110 text = message or default_message
1110 text = message or default_message
1111
1111
1112 # create a comment, and link it to new status
1112 # create a comment, and link it to new status
1113 comment = CommentsModel().create(
1113 comment = CommentsModel().create(
1114 text=text,
1114 text=text,
1115 repo=repo.repo_id,
1115 repo=repo.repo_id,
1116 user=user.user_id,
1116 user=user.user_id,
1117 pull_request=pull_request.pull_request_id,
1117 pull_request=pull_request.pull_request_id,
1118 status_change=status_lbl,
1118 status_change=status_lbl,
1119 status_change_type=status,
1119 status_change_type=status,
1120 closing_pr=True
1120 closing_pr=True
1121 )
1121 )
1122
1122
1123 # calculate old status before we change it
1123 # calculate old status before we change it
1124 old_calculated_status = pull_request.calculated_review_status()
1124 old_calculated_status = pull_request.calculated_review_status()
1125 ChangesetStatusModel().set_status(
1125 ChangesetStatusModel().set_status(
1126 repo.repo_id,
1126 repo.repo_id,
1127 status,
1127 status,
1128 user.user_id,
1128 user.user_id,
1129 comment=comment,
1129 comment=comment,
1130 pull_request=pull_request.pull_request_id
1130 pull_request=pull_request.pull_request_id
1131 )
1131 )
1132
1132
1133 Session().flush()
1133 Session().flush()
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1135 # we now calculate the status of pull request again, and based on that
1135 # we now calculate the status of pull request again, and based on that
1136 # calculation trigger status change. This might happen in cases
1136 # calculation trigger status change. This might happen in cases
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1138 # change the status, while if he's a reviewer this might change it.
1138 # change the status, while if he's a reviewer this might change it.
1139 calculated_status = pull_request.calculated_review_status()
1139 calculated_status = pull_request.calculated_review_status()
1140 if old_calculated_status != calculated_status:
1140 if old_calculated_status != calculated_status:
1141 self._trigger_pull_request_hook(
1141 self._trigger_pull_request_hook(
1142 pull_request, user, 'review_status_change')
1142 pull_request, user, 'review_status_change')
1143
1143
1144 # finally close the PR
1144 # finally close the PR
1145 PullRequestModel().close_pull_request(
1145 PullRequestModel().close_pull_request(
1146 pull_request.pull_request_id, user)
1146 pull_request.pull_request_id, user)
1147
1147
1148 return comment, status
1148 return comment, status
1149
1149
1150 def merge_status(self, pull_request):
1150 def merge_status(self, pull_request):
1151 if not self._is_merge_enabled(pull_request):
1151 if not self._is_merge_enabled(pull_request):
1152 return False, _('Server-side pull request merging is disabled.')
1152 return False, _('Server-side pull request merging is disabled.')
1153 if pull_request.is_closed():
1153 if pull_request.is_closed():
1154 return False, _('This pull request is closed.')
1154 return False, _('This pull request is closed.')
1155 merge_possible, msg = self._check_repo_requirements(
1155 merge_possible, msg = self._check_repo_requirements(
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1156 target=pull_request.target_repo, source=pull_request.source_repo)
1157 if not merge_possible:
1157 if not merge_possible:
1158 return merge_possible, msg
1158 return merge_possible, msg
1159
1159
1160 try:
1160 try:
1161 resp = self._try_merge(pull_request)
1161 resp = self._try_merge(pull_request)
1162 log.debug("Merge response: %s", resp)
1162 log.debug("Merge response: %s", resp)
1163 status = resp.possible, self.merge_status_message(
1163 status = resp.possible, self.merge_status_message(
1164 resp.failure_reason)
1164 resp.failure_reason)
1165 except NotImplementedError:
1165 except NotImplementedError:
1166 status = False, _('Pull request merging is not supported.')
1166 status = False, _('Pull request merging is not supported.')
1167
1167
1168 return status
1168 return status
1169
1169
1170 def _check_repo_requirements(self, target, source):
1170 def _check_repo_requirements(self, target, source):
1171 """
1171 """
1172 Check if `target` and `source` have compatible requirements.
1172 Check if `target` and `source` have compatible requirements.
1173
1173
1174 Currently this is just checking for largefiles.
1174 Currently this is just checking for largefiles.
1175 """
1175 """
1176 target_has_largefiles = self._has_largefiles(target)
1176 target_has_largefiles = self._has_largefiles(target)
1177 source_has_largefiles = self._has_largefiles(source)
1177 source_has_largefiles = self._has_largefiles(source)
1178 merge_possible = True
1178 merge_possible = True
1179 message = u''
1179 message = u''
1180
1180
1181 if target_has_largefiles != source_has_largefiles:
1181 if target_has_largefiles != source_has_largefiles:
1182 merge_possible = False
1182 merge_possible = False
1183 if source_has_largefiles:
1183 if source_has_largefiles:
1184 message = _(
1184 message = _(
1185 'Target repository large files support is disabled.')
1185 'Target repository large files support is disabled.')
1186 else:
1186 else:
1187 message = _(
1187 message = _(
1188 'Source repository large files support is disabled.')
1188 'Source repository large files support is disabled.')
1189
1189
1190 return merge_possible, message
1190 return merge_possible, message
1191
1191
1192 def _has_largefiles(self, repo):
1192 def _has_largefiles(self, repo):
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1194 'extensions', 'largefiles')
1194 'extensions', 'largefiles')
1195 return largefiles_ui and largefiles_ui[0].active
1195 return largefiles_ui and largefiles_ui[0].active
1196
1196
1197 def _try_merge(self, pull_request):
1197 def _try_merge(self, pull_request):
1198 """
1198 """
1199 Try to merge the pull request and return the merge status.
1199 Try to merge the pull request and return the merge status.
1200 """
1200 """
1201 log.debug(
1201 log.debug(
1202 "Trying out if the pull request %s can be merged.",
1202 "Trying out if the pull request %s can be merged.",
1203 pull_request.pull_request_id)
1203 pull_request.pull_request_id)
1204 target_vcs = pull_request.target_repo.scm_instance()
1204 target_vcs = pull_request.target_repo.scm_instance()
1205
1205
1206 # Refresh the target reference.
1206 # Refresh the target reference.
1207 try:
1207 try:
1208 target_ref = self._refresh_reference(
1208 target_ref = self._refresh_reference(
1209 pull_request.target_ref_parts, target_vcs)
1209 pull_request.target_ref_parts, target_vcs)
1210 except CommitDoesNotExistError:
1210 except CommitDoesNotExistError:
1211 merge_state = MergeResponse(
1211 merge_state = MergeResponse(
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1213 return merge_state
1213 return merge_state
1214
1214
1215 target_locked = pull_request.target_repo.locked
1215 target_locked = pull_request.target_repo.locked
1216 if target_locked and target_locked[0]:
1216 if target_locked and target_locked[0]:
1217 log.debug("The target repository is locked.")
1217 log.debug("The target repository is locked.")
1218 merge_state = MergeResponse(
1218 merge_state = MergeResponse(
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1221 log.debug("Refreshing the merge status of the repository.")
1221 log.debug("Refreshing the merge status of the repository.")
1222 merge_state = self._refresh_merge_state(
1222 merge_state = self._refresh_merge_state(
1223 pull_request, target_vcs, target_ref)
1223 pull_request, target_vcs, target_ref)
1224 else:
1224 else:
1225 possible = pull_request.\
1225 possible = pull_request.\
1226 _last_merge_status == MergeFailureReason.NONE
1226 last_merge_status == MergeFailureReason.NONE
1227 merge_state = MergeResponse(
1227 merge_state = MergeResponse(
1228 possible, False, None, pull_request._last_merge_status)
1228 possible, False, None, pull_request.last_merge_status)
1229
1229
1230 return merge_state
1230 return merge_state
1231
1231
1232 def _refresh_reference(self, reference, vcs_repository):
1232 def _refresh_reference(self, reference, vcs_repository):
1233 if reference.type in ('branch', 'book'):
1233 if reference.type in ('branch', 'book'):
1234 name_or_id = reference.name
1234 name_or_id = reference.name
1235 else:
1235 else:
1236 name_or_id = reference.commit_id
1236 name_or_id = reference.commit_id
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1238 refreshed_reference = Reference(
1238 refreshed_reference = Reference(
1239 reference.type, reference.name, refreshed_commit.raw_id)
1239 reference.type, reference.name, refreshed_commit.raw_id)
1240 return refreshed_reference
1240 return refreshed_reference
1241
1241
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1243 return not(
1243 return not(
1244 pull_request.revisions and
1244 pull_request.revisions and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1247
1247
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1249 workspace_id = self._workspace_id(pull_request)
1249 workspace_id = self._workspace_id(pull_request)
1250 source_vcs = pull_request.source_repo.scm_instance()
1250 source_vcs = pull_request.source_repo.scm_instance()
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1251 use_rebase = self._use_rebase_for_merging(pull_request)
1252 merge_state = target_vcs.merge(
1252 merge_state = target_vcs.merge(
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1253 target_reference, source_vcs, pull_request.source_ref_parts,
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1255
1255
1256 # Do not store the response if there was an unknown error.
1256 # Do not store the response if there was an unknown error.
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1258 pull_request._last_merge_source_rev = \
1258 pull_request._last_merge_source_rev = \
1259 pull_request.source_ref_parts.commit_id
1259 pull_request.source_ref_parts.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1260 pull_request._last_merge_target_rev = target_reference.commit_id
1261 pull_request._last_merge_status = merge_state.failure_reason
1261 pull_request.last_merge_status = merge_state.failure_reason
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1263 Session().add(pull_request)
1263 Session().add(pull_request)
1264 Session().commit()
1264 Session().commit()
1265
1265
1266 return merge_state
1266 return merge_state
1267
1267
1268 def _workspace_id(self, pull_request):
1268 def _workspace_id(self, pull_request):
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270 return workspace_id
1270 return workspace_id
1271
1271
1272 def merge_status_message(self, status_code):
1272 def merge_status_message(self, status_code):
1273 """
1273 """
1274 Return a human friendly error message for the given merge status code.
1274 Return a human friendly error message for the given merge status code.
1275 """
1275 """
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1276 return self.MERGE_STATUS_MESSAGES[status_code]
1277
1277
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1279 bookmark=None):
1279 bookmark=None):
1280 all_refs, selected_ref = \
1280 all_refs, selected_ref = \
1281 self._get_repo_pullrequest_sources(
1281 self._get_repo_pullrequest_sources(
1282 repo.scm_instance(), commit_id=commit_id,
1282 repo.scm_instance(), commit_id=commit_id,
1283 branch=branch, bookmark=bookmark)
1283 branch=branch, bookmark=bookmark)
1284
1284
1285 refs_select2 = []
1285 refs_select2 = []
1286 for element in all_refs:
1286 for element in all_refs:
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1288 refs_select2.append({'text': element[1], 'children': children})
1288 refs_select2.append({'text': element[1], 'children': children})
1289
1289
1290 return {
1290 return {
1291 'user': {
1291 'user': {
1292 'user_id': repo.user.user_id,
1292 'user_id': repo.user.user_id,
1293 'username': repo.user.username,
1293 'username': repo.user.username,
1294 'firstname': repo.user.first_name,
1294 'firstname': repo.user.first_name,
1295 'lastname': repo.user.last_name,
1295 'lastname': repo.user.last_name,
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1297 },
1297 },
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1299 'refs': {
1299 'refs': {
1300 'all_refs': all_refs,
1300 'all_refs': all_refs,
1301 'selected_ref': selected_ref,
1301 'selected_ref': selected_ref,
1302 'select2_refs': refs_select2
1302 'select2_refs': refs_select2
1303 }
1303 }
1304 }
1304 }
1305
1305
1306 def generate_pullrequest_title(self, source, source_ref, target):
1306 def generate_pullrequest_title(self, source, source_ref, target):
1307 return u'{source}#{at_ref} to {target}'.format(
1307 return u'{source}#{at_ref} to {target}'.format(
1308 source=source,
1308 source=source,
1309 at_ref=source_ref,
1309 at_ref=source_ref,
1310 target=target,
1310 target=target,
1311 )
1311 )
1312
1312
1313 def _cleanup_merge_workspace(self, pull_request):
1313 def _cleanup_merge_workspace(self, pull_request):
1314 # Merging related cleanup
1314 # Merging related cleanup
1315 target_scm = pull_request.target_repo.scm_instance()
1315 target_scm = pull_request.target_repo.scm_instance()
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1317
1317
1318 try:
1318 try:
1319 target_scm.cleanup_merge_workspace(workspace_id)
1319 target_scm.cleanup_merge_workspace(workspace_id)
1320 except NotImplementedError:
1320 except NotImplementedError:
1321 pass
1321 pass
1322
1322
1323 def _get_repo_pullrequest_sources(
1323 def _get_repo_pullrequest_sources(
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1324 self, repo, commit_id=None, branch=None, bookmark=None):
1325 """
1325 """
1326 Return a structure with repo's interesting commits, suitable for
1326 Return a structure with repo's interesting commits, suitable for
1327 the selectors in pullrequest controller
1327 the selectors in pullrequest controller
1328
1328
1329 :param commit_id: a commit that must be in the list somehow
1329 :param commit_id: a commit that must be in the list somehow
1330 and selected by default
1330 and selected by default
1331 :param branch: a branch that must be in the list and selected
1331 :param branch: a branch that must be in the list and selected
1332 by default - even if closed
1332 by default - even if closed
1333 :param bookmark: a bookmark that must be in the list and selected
1333 :param bookmark: a bookmark that must be in the list and selected
1334 """
1334 """
1335
1335
1336 commit_id = safe_str(commit_id) if commit_id else None
1336 commit_id = safe_str(commit_id) if commit_id else None
1337 branch = safe_str(branch) if branch else None
1337 branch = safe_str(branch) if branch else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1338 bookmark = safe_str(bookmark) if bookmark else None
1339
1339
1340 selected = None
1340 selected = None
1341
1341
1342 # order matters: first source that has commit_id in it will be selected
1342 # order matters: first source that has commit_id in it will be selected
1343 sources = []
1343 sources = []
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1346
1346
1347 if commit_id:
1347 if commit_id:
1348 ref_commit = (h.short_id(commit_id), commit_id)
1348 ref_commit = (h.short_id(commit_id), commit_id)
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1350
1350
1351 sources.append(
1351 sources.append(
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1353 )
1353 )
1354
1354
1355 groups = []
1355 groups = []
1356 for group_key, ref_list, group_name, match in sources:
1356 for group_key, ref_list, group_name, match in sources:
1357 group_refs = []
1357 group_refs = []
1358 for ref_name, ref_id in ref_list:
1358 for ref_name, ref_id in ref_list:
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1360 group_refs.append((ref_key, ref_name))
1360 group_refs.append((ref_key, ref_name))
1361
1361
1362 if not selected:
1362 if not selected:
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1364 selected = ref_key
1364 selected = ref_key
1365
1365
1366 if group_refs:
1366 if group_refs:
1367 groups.append((group_refs, group_name))
1367 groups.append((group_refs, group_name))
1368
1368
1369 if not selected:
1369 if not selected:
1370 ref = commit_id or branch or bookmark
1370 ref = commit_id or branch or bookmark
1371 if ref:
1371 if ref:
1372 raise CommitDoesNotExistError(
1372 raise CommitDoesNotExistError(
1373 'No commit refs could be found matching: %s' % ref)
1373 'No commit refs could be found matching: %s' % ref)
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1375 selected = 'branch:%s:%s' % (
1375 selected = 'branch:%s:%s' % (
1376 repo.DEFAULT_BRANCH_NAME,
1376 repo.DEFAULT_BRANCH_NAME,
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1378 )
1378 )
1379 elif repo.commit_ids:
1379 elif repo.commit_ids:
1380 rev = repo.commit_ids[0]
1380 rev = repo.commit_ids[0]
1381 selected = 'rev:%s:%s' % (rev, rev)
1381 selected = 'rev:%s:%s' % (rev, rev)
1382 else:
1382 else:
1383 raise EmptyRepositoryError()
1383 raise EmptyRepositoryError()
1384 return groups, selected
1384 return groups, selected
1385
1385
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1387 return self._get_diff_from_pr_or_version(
1387 return self._get_diff_from_pr_or_version(
1388 source_repo, source_ref_id, target_ref_id, context=context)
1388 source_repo, source_ref_id, target_ref_id, context=context)
1389
1389
1390 def _get_diff_from_pr_or_version(
1390 def _get_diff_from_pr_or_version(
1391 self, source_repo, source_ref_id, target_ref_id, context):
1391 self, source_repo, source_ref_id, target_ref_id, context):
1392 target_commit = source_repo.get_commit(
1392 target_commit = source_repo.get_commit(
1393 commit_id=safe_str(target_ref_id))
1393 commit_id=safe_str(target_ref_id))
1394 source_commit = source_repo.get_commit(
1394 source_commit = source_repo.get_commit(
1395 commit_id=safe_str(source_ref_id))
1395 commit_id=safe_str(source_ref_id))
1396 if isinstance(source_repo, Repository):
1396 if isinstance(source_repo, Repository):
1397 vcs_repo = source_repo.scm_instance()
1397 vcs_repo = source_repo.scm_instance()
1398 else:
1398 else:
1399 vcs_repo = source_repo
1399 vcs_repo = source_repo
1400
1400
1401 # TODO: johbo: In the context of an update, we cannot reach
1401 # TODO: johbo: In the context of an update, we cannot reach
1402 # the old commit anymore with our normal mechanisms. It needs
1402 # the old commit anymore with our normal mechanisms. It needs
1403 # some sort of special support in the vcs layer to avoid this
1403 # some sort of special support in the vcs layer to avoid this
1404 # workaround.
1404 # workaround.
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1406 vcs_repo.alias == 'git'):
1406 vcs_repo.alias == 'git'):
1407 source_commit.raw_id = safe_str(source_ref_id)
1407 source_commit.raw_id = safe_str(source_ref_id)
1408
1408
1409 log.debug('calculating diff between '
1409 log.debug('calculating diff between '
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1410 'source_ref:%s and target_ref:%s for repo `%s`',
1411 target_ref_id, source_ref_id,
1411 target_ref_id, source_ref_id,
1412 safe_unicode(vcs_repo.path))
1412 safe_unicode(vcs_repo.path))
1413
1413
1414 vcs_diff = vcs_repo.get_diff(
1414 vcs_diff = vcs_repo.get_diff(
1415 commit1=target_commit, commit2=source_commit, context=context)
1415 commit1=target_commit, commit2=source_commit, context=context)
1416 return vcs_diff
1416 return vcs_diff
1417
1417
1418 def _is_merge_enabled(self, pull_request):
1418 def _is_merge_enabled(self, pull_request):
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1420 settings = settings_model.get_general_settings()
1420 settings = settings_model.get_general_settings()
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1421 return settings.get('rhodecode_pr_merge_enabled', False)
1422
1422
1423 def _use_rebase_for_merging(self, pull_request):
1423 def _use_rebase_for_merging(self, pull_request):
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1425 settings = settings_model.get_general_settings()
1425 settings = settings_model.get_general_settings()
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1427
1427
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1428 def _log_audit_action(self, action, action_data, user, pull_request):
1429 audit_logger.store(
1429 audit_logger.store(
1430 action=action,
1430 action=action,
1431 action_data=action_data,
1431 action_data=action_data,
1432 user=user,
1432 user=user,
1433 repo=pull_request.target_repo)
1433 repo=pull_request.target_repo)
1434
1434
1435 def get_reviewer_functions(self):
1435 def get_reviewer_functions(self):
1436 """
1436 """
1437 Fetches functions for validation and fetching default reviewers.
1437 Fetches functions for validation and fetching default reviewers.
1438 If available we use the EE package, else we fallback to CE
1438 If available we use the EE package, else we fallback to CE
1439 package functions
1439 package functions
1440 """
1440 """
1441 try:
1441 try:
1442 from rc_reviewers.utils import get_default_reviewers_data
1442 from rc_reviewers.utils import get_default_reviewers_data
1443 from rc_reviewers.utils import validate_default_reviewers
1443 from rc_reviewers.utils import validate_default_reviewers
1444 except ImportError:
1444 except ImportError:
1445 from rhodecode.apps.repository.utils import \
1445 from rhodecode.apps.repository.utils import \
1446 get_default_reviewers_data
1446 get_default_reviewers_data
1447 from rhodecode.apps.repository.utils import \
1447 from rhodecode.apps.repository.utils import \
1448 validate_default_reviewers
1448 validate_default_reviewers
1449
1449
1450 return get_default_reviewers_data, validate_default_reviewers
1450 return get_default_reviewers_data, validate_default_reviewers
1451
1451
1452
1452
1453 class MergeCheck(object):
1453 class MergeCheck(object):
1454 """
1454 """
1455 Perform Merge Checks and returns a check object which stores information
1455 Perform Merge Checks and returns a check object which stores information
1456 about merge errors, and merge conditions
1456 about merge errors, and merge conditions
1457 """
1457 """
1458 TODO_CHECK = 'todo'
1458 TODO_CHECK = 'todo'
1459 PERM_CHECK = 'perm'
1459 PERM_CHECK = 'perm'
1460 REVIEW_CHECK = 'review'
1460 REVIEW_CHECK = 'review'
1461 MERGE_CHECK = 'merge'
1461 MERGE_CHECK = 'merge'
1462
1462
1463 def __init__(self):
1463 def __init__(self):
1464 self.review_status = None
1464 self.review_status = None
1465 self.merge_possible = None
1465 self.merge_possible = None
1466 self.merge_msg = ''
1466 self.merge_msg = ''
1467 self.failed = None
1467 self.failed = None
1468 self.errors = []
1468 self.errors = []
1469 self.error_details = OrderedDict()
1469 self.error_details = OrderedDict()
1470
1470
1471 def push_error(self, error_type, message, error_key, details):
1471 def push_error(self, error_type, message, error_key, details):
1472 self.failed = True
1472 self.failed = True
1473 self.errors.append([error_type, message])
1473 self.errors.append([error_type, message])
1474 self.error_details[error_key] = dict(
1474 self.error_details[error_key] = dict(
1475 details=details,
1475 details=details,
1476 error_type=error_type,
1476 error_type=error_type,
1477 message=message
1477 message=message
1478 )
1478 )
1479
1479
1480 @classmethod
1480 @classmethod
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1482 # if migrated to pyramid...
1482 # if migrated to pyramid...
1483 # _ = lambda: translator or _ # use passed in translator if any
1483 # _ = lambda: translator or _ # use passed in translator if any
1484
1484
1485 merge_check = cls()
1485 merge_check = cls()
1486
1486
1487 # permissions to merge
1487 # permissions to merge
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1489 pull_request, user)
1489 pull_request, user)
1490 if not user_allowed_to_merge:
1490 if not user_allowed_to_merge:
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1492
1492
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1495 if fail_early:
1495 if fail_early:
1496 return merge_check
1496 return merge_check
1497
1497
1498 # review status, must be always present
1498 # review status, must be always present
1499 review_status = pull_request.calculated_review_status()
1499 review_status = pull_request.calculated_review_status()
1500 merge_check.review_status = review_status
1500 merge_check.review_status = review_status
1501
1501
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1503 if not status_approved:
1503 if not status_approved:
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1505
1505
1506 msg = _('Pull request reviewer approval is pending.')
1506 msg = _('Pull request reviewer approval is pending.')
1507
1507
1508 merge_check.push_error(
1508 merge_check.push_error(
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1510
1510
1511 if fail_early:
1511 if fail_early:
1512 return merge_check
1512 return merge_check
1513
1513
1514 # left over TODOs
1514 # left over TODOs
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1516 if todos:
1516 if todos:
1517 log.debug("MergeCheck: cannot merge, {} "
1517 log.debug("MergeCheck: cannot merge, {} "
1518 "unresolved todos left.".format(len(todos)))
1518 "unresolved todos left.".format(len(todos)))
1519
1519
1520 if len(todos) == 1:
1520 if len(todos) == 1:
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1522 len(todos))
1522 len(todos))
1523 else:
1523 else:
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1525 len(todos))
1525 len(todos))
1526
1526
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1528
1528
1529 if fail_early:
1529 if fail_early:
1530 return merge_check
1530 return merge_check
1531
1531
1532 # merge possible
1532 # merge possible
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1534 merge_check.merge_possible = merge_status
1534 merge_check.merge_possible = merge_status
1535 merge_check.merge_msg = msg
1535 merge_check.merge_msg = msg
1536 if not merge_status:
1536 if not merge_status:
1537 log.debug(
1537 log.debug(
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1538 "MergeCheck: cannot merge, pull request merge not possible.")
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1540
1540
1541 if fail_early:
1541 if fail_early:
1542 return merge_check
1542 return merge_check
1543
1543
1544 return merge_check
1544 return merge_check
1545
1545
1546
1546
1547 ChangeTuple = namedtuple('ChangeTuple',
1547 ChangeTuple = namedtuple('ChangeTuple',
1548 ['added', 'common', 'removed', 'total'])
1548 ['added', 'common', 'removed', 'total'])
1549
1549
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1550 FileChangeTuple = namedtuple('FileChangeTuple',
1551 ['added', 'modified', 'removed'])
1551 ['added', 'modified', 'removed'])
@@ -1,859 +1,859 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23 import textwrap
23 import textwrap
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.lib.utils2 import safe_unicode
26 from rhodecode.lib.utils2 import safe_unicode
27 from rhodecode.lib.vcs.backends import get_backend
27 from rhodecode.lib.vcs.backends import get_backend
28 from rhodecode.lib.vcs.backends.base import (
28 from rhodecode.lib.vcs.backends.base import (
29 MergeResponse, MergeFailureReason, Reference)
29 MergeResponse, MergeFailureReason, Reference)
30 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import RepositoryError
31 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.vcs.nodes import FileNode
32 from rhodecode.model.comment import CommentsModel
32 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.db import PullRequest, Session
33 from rhodecode.model.db import PullRequest, Session
34 from rhodecode.model.pull_request import PullRequestModel
34 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.user import UserModel
35 from rhodecode.model.user import UserModel
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
37
37
38
38
39 pytestmark = [
39 pytestmark = [
40 pytest.mark.backends("git", "hg"),
40 pytest.mark.backends("git", "hg"),
41 ]
41 ]
42
42
43
43
44 @pytest.mark.usefixtures('config_stub')
44 @pytest.mark.usefixtures('config_stub')
45 class TestPullRequestModel(object):
45 class TestPullRequestModel(object):
46
46
47 @pytest.fixture
47 @pytest.fixture
48 def pull_request(self, request, backend, pr_util):
48 def pull_request(self, request, backend, pr_util):
49 """
49 """
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
55 False, False, None, MergeFailureReason.UNKNOWN))
56 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
57 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
58
58
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
59 self.workspace_remove_mock = self.workspace_remove_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
60 self.merge_mock = self.merge_patcher.start()
61 self.comment_patcher = mock.patch(
61 self.comment_patcher = mock.patch(
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
62 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
63 self.comment_patcher.start()
63 self.comment_patcher.start()
64 self.notification_patcher = mock.patch(
64 self.notification_patcher = mock.patch(
65 'rhodecode.model.notification.NotificationModel.create')
65 'rhodecode.model.notification.NotificationModel.create')
66 self.notification_patcher.start()
66 self.notification_patcher.start()
67 self.helper_patcher = mock.patch(
67 self.helper_patcher = mock.patch(
68 'rhodecode.lib.helpers.url')
68 'rhodecode.lib.helpers.url')
69 self.helper_patcher.start()
69 self.helper_patcher.start()
70
70
71 self.hook_patcher = mock.patch.object(PullRequestModel,
71 self.hook_patcher = mock.patch.object(PullRequestModel,
72 '_trigger_pull_request_hook')
72 '_trigger_pull_request_hook')
73 self.hook_mock = self.hook_patcher.start()
73 self.hook_mock = self.hook_patcher.start()
74
74
75 self.invalidation_patcher = mock.patch(
75 self.invalidation_patcher = mock.patch(
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
76 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
77 self.invalidation_mock = self.invalidation_patcher.start()
77 self.invalidation_mock = self.invalidation_patcher.start()
78
78
79 self.pull_request = pr_util.create_pull_request(
79 self.pull_request = pr_util.create_pull_request(
80 mergeable=True, name_suffix=u'ąć')
80 mergeable=True, name_suffix=u'ąć')
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
81 self.source_commit = self.pull_request.source_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
82 self.target_commit = self.pull_request.target_ref_parts.commit_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
83 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
84
84
85 @request.addfinalizer
85 @request.addfinalizer
86 def cleanup_pull_request():
86 def cleanup_pull_request():
87 calls = [mock.call(
87 calls = [mock.call(
88 self.pull_request, self.pull_request.author, 'create')]
88 self.pull_request, self.pull_request.author, 'create')]
89 self.hook_mock.assert_has_calls(calls)
89 self.hook_mock.assert_has_calls(calls)
90
90
91 self.workspace_remove_patcher.stop()
91 self.workspace_remove_patcher.stop()
92 self.merge_patcher.stop()
92 self.merge_patcher.stop()
93 self.comment_patcher.stop()
93 self.comment_patcher.stop()
94 self.notification_patcher.stop()
94 self.notification_patcher.stop()
95 self.helper_patcher.stop()
95 self.helper_patcher.stop()
96 self.hook_patcher.stop()
96 self.hook_patcher.stop()
97 self.invalidation_patcher.stop()
97 self.invalidation_patcher.stop()
98
98
99 return self.pull_request
99 return self.pull_request
100
100
101 def test_get_all(self, pull_request):
101 def test_get_all(self, pull_request):
102 prs = PullRequestModel().get_all(pull_request.target_repo)
102 prs = PullRequestModel().get_all(pull_request.target_repo)
103 assert isinstance(prs, list)
103 assert isinstance(prs, list)
104 assert len(prs) == 1
104 assert len(prs) == 1
105
105
106 def test_count_all(self, pull_request):
106 def test_count_all(self, pull_request):
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
107 pr_count = PullRequestModel().count_all(pull_request.target_repo)
108 assert pr_count == 1
108 assert pr_count == 1
109
109
110 def test_get_awaiting_review(self, pull_request):
110 def test_get_awaiting_review(self, pull_request):
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
111 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
112 assert isinstance(prs, list)
112 assert isinstance(prs, list)
113 assert len(prs) == 1
113 assert len(prs) == 1
114
114
115 def test_count_awaiting_review(self, pull_request):
115 def test_count_awaiting_review(self, pull_request):
116 pr_count = PullRequestModel().count_awaiting_review(
116 pr_count = PullRequestModel().count_awaiting_review(
117 pull_request.target_repo)
117 pull_request.target_repo)
118 assert pr_count == 1
118 assert pr_count == 1
119
119
120 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
121 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
122 pull_request, [(pull_request.author, ['author'], False)],
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
123 pull_request.author)
124 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
125 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
126 assert isinstance(prs, list)
126 assert isinstance(prs, list)
127 assert len(prs) == 1
127 assert len(prs) == 1
128
128
129 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
130 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
131 pull_request, [(pull_request.author, ['author'], False)],
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
132 pull_request.author)
133 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
134 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
135 assert pr_count == 1
135 assert pr_count == 1
136
136
137 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
138 PullRequestModel().delete(pull_request, pull_request.author)
138 PullRequestModel().delete(pull_request, pull_request.author)
139
139
140 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
141 self.workspace_id)
141 self.workspace_id)
142
142
143 def test_close_calls_cleanup_and_hook(self, pull_request):
143 def test_close_calls_cleanup_and_hook(self, pull_request):
144 PullRequestModel().close_pull_request(
144 PullRequestModel().close_pull_request(
145 pull_request, pull_request.author)
145 pull_request, pull_request.author)
146
146
147 self.workspace_remove_mock.assert_called_once_with(
147 self.workspace_remove_mock.assert_called_once_with(
148 self.workspace_id)
148 self.workspace_id)
149 self.hook_mock.assert_called_with(
149 self.hook_mock.assert_called_with(
150 self.pull_request, self.pull_request.author, 'close')
150 self.pull_request, self.pull_request.author, 'close')
151
151
152 def test_merge_status(self, pull_request):
152 def test_merge_status(self, pull_request):
153 self.merge_mock.return_value = MergeResponse(
153 self.merge_mock.return_value = MergeResponse(
154 True, False, None, MergeFailureReason.NONE)
154 True, False, None, MergeFailureReason.NONE)
155
155
156 assert pull_request._last_merge_source_rev is None
156 assert pull_request._last_merge_source_rev is None
157 assert pull_request._last_merge_target_rev is None
157 assert pull_request._last_merge_target_rev is None
158 assert pull_request._last_merge_status is None
158 assert pull_request.last_merge_status is None
159
159
160 status, msg = PullRequestModel().merge_status(pull_request)
160 status, msg = PullRequestModel().merge_status(pull_request)
161 assert status is True
161 assert status is True
162 assert msg.eval() == 'This pull request can be automatically merged.'
162 assert msg.eval() == 'This pull request can be automatically merged.'
163 self.merge_mock.assert_called_once_with(
163 self.merge_mock.assert_called_once_with(
164 pull_request.target_ref_parts,
164 pull_request.target_ref_parts,
165 pull_request.source_repo.scm_instance(),
165 pull_request.source_repo.scm_instance(),
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
166 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
167 use_rebase=False)
167 use_rebase=False)
168
168
169 assert pull_request._last_merge_source_rev == self.source_commit
169 assert pull_request._last_merge_source_rev == self.source_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
170 assert pull_request._last_merge_target_rev == self.target_commit
171 assert pull_request._last_merge_status is MergeFailureReason.NONE
171 assert pull_request.last_merge_status is MergeFailureReason.NONE
172
172
173 self.merge_mock.reset_mock()
173 self.merge_mock.reset_mock()
174 status, msg = PullRequestModel().merge_status(pull_request)
174 status, msg = PullRequestModel().merge_status(pull_request)
175 assert status is True
175 assert status is True
176 assert msg.eval() == 'This pull request can be automatically merged.'
176 assert msg.eval() == 'This pull request can be automatically merged.'
177 assert self.merge_mock.called is False
177 assert self.merge_mock.called is False
178
178
179 def test_merge_status_known_failure(self, pull_request):
179 def test_merge_status_known_failure(self, pull_request):
180 self.merge_mock.return_value = MergeResponse(
180 self.merge_mock.return_value = MergeResponse(
181 False, False, None, MergeFailureReason.MERGE_FAILED)
181 False, False, None, MergeFailureReason.MERGE_FAILED)
182
182
183 assert pull_request._last_merge_source_rev is None
183 assert pull_request._last_merge_source_rev is None
184 assert pull_request._last_merge_target_rev is None
184 assert pull_request._last_merge_target_rev is None
185 assert pull_request._last_merge_status is None
185 assert pull_request.last_merge_status is None
186
186
187 status, msg = PullRequestModel().merge_status(pull_request)
187 status, msg = PullRequestModel().merge_status(pull_request)
188 assert status is False
188 assert status is False
189 assert (
189 assert (
190 msg.eval() ==
190 msg.eval() ==
191 'This pull request cannot be merged because of merge conflicts.')
191 'This pull request cannot be merged because of merge conflicts.')
192 self.merge_mock.assert_called_once_with(
192 self.merge_mock.assert_called_once_with(
193 pull_request.target_ref_parts,
193 pull_request.target_ref_parts,
194 pull_request.source_repo.scm_instance(),
194 pull_request.source_repo.scm_instance(),
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
195 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
196 use_rebase=False)
196 use_rebase=False)
197
197
198 assert pull_request._last_merge_source_rev == self.source_commit
198 assert pull_request._last_merge_source_rev == self.source_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
199 assert pull_request._last_merge_target_rev == self.target_commit
200 assert (
200 assert (
201 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
201 pull_request.last_merge_status is MergeFailureReason.MERGE_FAILED)
202
202
203 self.merge_mock.reset_mock()
203 self.merge_mock.reset_mock()
204 status, msg = PullRequestModel().merge_status(pull_request)
204 status, msg = PullRequestModel().merge_status(pull_request)
205 assert status is False
205 assert status is False
206 assert (
206 assert (
207 msg.eval() ==
207 msg.eval() ==
208 'This pull request cannot be merged because of merge conflicts.')
208 'This pull request cannot be merged because of merge conflicts.')
209 assert self.merge_mock.called is False
209 assert self.merge_mock.called is False
210
210
211 def test_merge_status_unknown_failure(self, pull_request):
211 def test_merge_status_unknown_failure(self, pull_request):
212 self.merge_mock.return_value = MergeResponse(
212 self.merge_mock.return_value = MergeResponse(
213 False, False, None, MergeFailureReason.UNKNOWN)
213 False, False, None, MergeFailureReason.UNKNOWN)
214
214
215 assert pull_request._last_merge_source_rev is None
215 assert pull_request._last_merge_source_rev is None
216 assert pull_request._last_merge_target_rev is None
216 assert pull_request._last_merge_target_rev is None
217 assert pull_request._last_merge_status is None
217 assert pull_request.last_merge_status is None
218
218
219 status, msg = PullRequestModel().merge_status(pull_request)
219 status, msg = PullRequestModel().merge_status(pull_request)
220 assert status is False
220 assert status is False
221 assert msg.eval() == (
221 assert msg.eval() == (
222 'This pull request cannot be merged because of an unhandled'
222 'This pull request cannot be merged because of an unhandled'
223 ' exception.')
223 ' exception.')
224 self.merge_mock.assert_called_once_with(
224 self.merge_mock.assert_called_once_with(
225 pull_request.target_ref_parts,
225 pull_request.target_ref_parts,
226 pull_request.source_repo.scm_instance(),
226 pull_request.source_repo.scm_instance(),
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
227 pull_request.source_ref_parts, self.workspace_id, dry_run=True,
228 use_rebase=False)
228 use_rebase=False)
229
229
230 assert pull_request._last_merge_source_rev is None
230 assert pull_request._last_merge_source_rev is None
231 assert pull_request._last_merge_target_rev is None
231 assert pull_request._last_merge_target_rev is None
232 assert pull_request._last_merge_status is None
232 assert pull_request.last_merge_status is None
233
233
234 self.merge_mock.reset_mock()
234 self.merge_mock.reset_mock()
235 status, msg = PullRequestModel().merge_status(pull_request)
235 status, msg = PullRequestModel().merge_status(pull_request)
236 assert status is False
236 assert status is False
237 assert msg.eval() == (
237 assert msg.eval() == (
238 'This pull request cannot be merged because of an unhandled'
238 'This pull request cannot be merged because of an unhandled'
239 ' exception.')
239 ' exception.')
240 assert self.merge_mock.called is True
240 assert self.merge_mock.called is True
241
241
242 def test_merge_status_when_target_is_locked(self, pull_request):
242 def test_merge_status_when_target_is_locked(self, pull_request):
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
243 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
244 status, msg = PullRequestModel().merge_status(pull_request)
244 status, msg = PullRequestModel().merge_status(pull_request)
245 assert status is False
245 assert status is False
246 assert msg.eval() == (
246 assert msg.eval() == (
247 'This pull request cannot be merged because the target repository'
247 'This pull request cannot be merged because the target repository'
248 ' is locked.')
248 ' is locked.')
249
249
250 def test_merge_status_requirements_check_target(self, pull_request):
250 def test_merge_status_requirements_check_target(self, pull_request):
251
251
252 def has_largefiles(self, repo):
252 def has_largefiles(self, repo):
253 return repo == pull_request.source_repo
253 return repo == pull_request.source_repo
254
254
255 patcher = mock.patch.object(
255 patcher = mock.patch.object(
256 PullRequestModel, '_has_largefiles', has_largefiles)
256 PullRequestModel, '_has_largefiles', has_largefiles)
257 with patcher:
257 with patcher:
258 status, msg = PullRequestModel().merge_status(pull_request)
258 status, msg = PullRequestModel().merge_status(pull_request)
259
259
260 assert status is False
260 assert status is False
261 assert msg == 'Target repository large files support is disabled.'
261 assert msg == 'Target repository large files support is disabled.'
262
262
263 def test_merge_status_requirements_check_source(self, pull_request):
263 def test_merge_status_requirements_check_source(self, pull_request):
264
264
265 def has_largefiles(self, repo):
265 def has_largefiles(self, repo):
266 return repo == pull_request.target_repo
266 return repo == pull_request.target_repo
267
267
268 patcher = mock.patch.object(
268 patcher = mock.patch.object(
269 PullRequestModel, '_has_largefiles', has_largefiles)
269 PullRequestModel, '_has_largefiles', has_largefiles)
270 with patcher:
270 with patcher:
271 status, msg = PullRequestModel().merge_status(pull_request)
271 status, msg = PullRequestModel().merge_status(pull_request)
272
272
273 assert status is False
273 assert status is False
274 assert msg == 'Source repository large files support is disabled.'
274 assert msg == 'Source repository large files support is disabled.'
275
275
276 def test_merge(self, pull_request, merge_extras):
276 def test_merge(self, pull_request, merge_extras):
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
277 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
278 merge_ref = Reference(
278 merge_ref = Reference(
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
279 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
280 self.merge_mock.return_value = MergeResponse(
280 self.merge_mock.return_value = MergeResponse(
281 True, True, merge_ref, MergeFailureReason.NONE)
281 True, True, merge_ref, MergeFailureReason.NONE)
282
282
283 merge_extras['repository'] = pull_request.target_repo.repo_name
283 merge_extras['repository'] = pull_request.target_repo.repo_name
284 PullRequestModel().merge(
284 PullRequestModel().merge(
285 pull_request, pull_request.author, extras=merge_extras)
285 pull_request, pull_request.author, extras=merge_extras)
286
286
287 message = (
287 message = (
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
288 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
289 u'\n\n {pr_title}'.format(
289 u'\n\n {pr_title}'.format(
290 pr_id=pull_request.pull_request_id,
290 pr_id=pull_request.pull_request_id,
291 source_repo=safe_unicode(
291 source_repo=safe_unicode(
292 pull_request.source_repo.scm_instance().name),
292 pull_request.source_repo.scm_instance().name),
293 source_ref_name=pull_request.source_ref_parts.name,
293 source_ref_name=pull_request.source_ref_parts.name,
294 pr_title=safe_unicode(pull_request.title)
294 pr_title=safe_unicode(pull_request.title)
295 )
295 )
296 )
296 )
297 self.merge_mock.assert_called_once_with(
297 self.merge_mock.assert_called_once_with(
298 pull_request.target_ref_parts,
298 pull_request.target_ref_parts,
299 pull_request.source_repo.scm_instance(),
299 pull_request.source_repo.scm_instance(),
300 pull_request.source_ref_parts, self.workspace_id,
300 pull_request.source_ref_parts, self.workspace_id,
301 user_name=user.username, user_email=user.email, message=message,
301 user_name=user.username, user_email=user.email, message=message,
302 use_rebase=False
302 use_rebase=False
303 )
303 )
304 self.invalidation_mock.assert_called_once_with(
304 self.invalidation_mock.assert_called_once_with(
305 pull_request.target_repo.repo_name)
305 pull_request.target_repo.repo_name)
306
306
307 self.hook_mock.assert_called_with(
307 self.hook_mock.assert_called_with(
308 self.pull_request, self.pull_request.author, 'merge')
308 self.pull_request, self.pull_request.author, 'merge')
309
309
310 pull_request = PullRequest.get(pull_request.pull_request_id)
310 pull_request = PullRequest.get(pull_request.pull_request_id)
311 assert (
311 assert (
312 pull_request.merge_rev ==
312 pull_request.merge_rev ==
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
313 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
314
314
315 def test_merge_failed(self, pull_request, merge_extras):
315 def test_merge_failed(self, pull_request, merge_extras):
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
316 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
317 merge_ref = Reference(
317 merge_ref = Reference(
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
318 'type', 'name', '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
319 self.merge_mock.return_value = MergeResponse(
319 self.merge_mock.return_value = MergeResponse(
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
320 False, False, merge_ref, MergeFailureReason.MERGE_FAILED)
321
321
322 merge_extras['repository'] = pull_request.target_repo.repo_name
322 merge_extras['repository'] = pull_request.target_repo.repo_name
323 PullRequestModel().merge(
323 PullRequestModel().merge(
324 pull_request, pull_request.author, extras=merge_extras)
324 pull_request, pull_request.author, extras=merge_extras)
325
325
326 message = (
326 message = (
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
327 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
328 u'\n\n {pr_title}'.format(
328 u'\n\n {pr_title}'.format(
329 pr_id=pull_request.pull_request_id,
329 pr_id=pull_request.pull_request_id,
330 source_repo=safe_unicode(
330 source_repo=safe_unicode(
331 pull_request.source_repo.scm_instance().name),
331 pull_request.source_repo.scm_instance().name),
332 source_ref_name=pull_request.source_ref_parts.name,
332 source_ref_name=pull_request.source_ref_parts.name,
333 pr_title=safe_unicode(pull_request.title)
333 pr_title=safe_unicode(pull_request.title)
334 )
334 )
335 )
335 )
336 self.merge_mock.assert_called_once_with(
336 self.merge_mock.assert_called_once_with(
337 pull_request.target_ref_parts,
337 pull_request.target_ref_parts,
338 pull_request.source_repo.scm_instance(),
338 pull_request.source_repo.scm_instance(),
339 pull_request.source_ref_parts, self.workspace_id,
339 pull_request.source_ref_parts, self.workspace_id,
340 user_name=user.username, user_email=user.email, message=message,
340 user_name=user.username, user_email=user.email, message=message,
341 use_rebase=False
341 use_rebase=False
342 )
342 )
343
343
344 pull_request = PullRequest.get(pull_request.pull_request_id)
344 pull_request = PullRequest.get(pull_request.pull_request_id)
345 assert self.invalidation_mock.called is False
345 assert self.invalidation_mock.called is False
346 assert pull_request.merge_rev is None
346 assert pull_request.merge_rev is None
347
347
348 def test_get_commit_ids(self, pull_request):
348 def test_get_commit_ids(self, pull_request):
349 # The PR has been not merget yet, so expect an exception
349 # The PR has been not merget yet, so expect an exception
350 with pytest.raises(ValueError):
350 with pytest.raises(ValueError):
351 PullRequestModel()._get_commit_ids(pull_request)
351 PullRequestModel()._get_commit_ids(pull_request)
352
352
353 # Merge revision is in the revisions list
353 # Merge revision is in the revisions list
354 pull_request.merge_rev = pull_request.revisions[0]
354 pull_request.merge_rev = pull_request.revisions[0]
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
355 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
356 assert commit_ids == pull_request.revisions
356 assert commit_ids == pull_request.revisions
357
357
358 # Merge revision is not in the revisions list
358 # Merge revision is not in the revisions list
359 pull_request.merge_rev = 'f000' * 10
359 pull_request.merge_rev = 'f000' * 10
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
360 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
361 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
362
362
363 def test_get_diff_from_pr_version(self, pull_request):
363 def test_get_diff_from_pr_version(self, pull_request):
364 source_repo = pull_request.source_repo
364 source_repo = pull_request.source_repo
365 source_ref_id = pull_request.source_ref_parts.commit_id
365 source_ref_id = pull_request.source_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
366 target_ref_id = pull_request.target_ref_parts.commit_id
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
367 diff = PullRequestModel()._get_diff_from_pr_or_version(
368 source_repo, source_ref_id, target_ref_id, context=6)
368 source_repo, source_ref_id, target_ref_id, context=6)
369 assert 'file_1' in diff.raw
369 assert 'file_1' in diff.raw
370
370
371 def test_generate_title_returns_unicode(self):
371 def test_generate_title_returns_unicode(self):
372 title = PullRequestModel().generate_pullrequest_title(
372 title = PullRequestModel().generate_pullrequest_title(
373 source='source-dummy',
373 source='source-dummy',
374 source_ref='source-ref-dummy',
374 source_ref='source-ref-dummy',
375 target='target-dummy',
375 target='target-dummy',
376 )
376 )
377 assert type(title) == unicode
377 assert type(title) == unicode
378
378
379
379
380 @pytest.mark.usefixtures('config_stub')
380 @pytest.mark.usefixtures('config_stub')
381 class TestIntegrationMerge(object):
381 class TestIntegrationMerge(object):
382 @pytest.mark.parametrize('extra_config', (
382 @pytest.mark.parametrize('extra_config', (
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
383 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
384 ))
384 ))
385 def test_merge_triggers_push_hooks(
385 def test_merge_triggers_push_hooks(
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
386 self, pr_util, user_admin, capture_rcextensions, merge_extras,
387 extra_config):
387 extra_config):
388 pull_request = pr_util.create_pull_request(
388 pull_request = pr_util.create_pull_request(
389 approved=True, mergeable=True)
389 approved=True, mergeable=True)
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
390 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
391 merge_extras['repository'] = pull_request.target_repo.repo_name
391 merge_extras['repository'] = pull_request.target_repo.repo_name
392 Session().commit()
392 Session().commit()
393
393
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
394 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
395 merge_state = PullRequestModel().merge(
395 merge_state = PullRequestModel().merge(
396 pull_request, user_admin, extras=merge_extras)
396 pull_request, user_admin, extras=merge_extras)
397
397
398 assert merge_state.executed
398 assert merge_state.executed
399 assert 'pre_push' in capture_rcextensions
399 assert 'pre_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
400 assert 'post_push' in capture_rcextensions
401
401
402 def test_merge_can_be_rejected_by_pre_push_hook(
402 def test_merge_can_be_rejected_by_pre_push_hook(
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
403 self, pr_util, user_admin, capture_rcextensions, merge_extras):
404 pull_request = pr_util.create_pull_request(
404 pull_request = pr_util.create_pull_request(
405 approved=True, mergeable=True)
405 approved=True, mergeable=True)
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
406 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
407 merge_extras['repository'] = pull_request.target_repo.repo_name
407 merge_extras['repository'] = pull_request.target_repo.repo_name
408 Session().commit()
408 Session().commit()
409
409
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
410 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
411 pre_pull.side_effect = RepositoryError("Disallow push!")
411 pre_pull.side_effect = RepositoryError("Disallow push!")
412 merge_status = PullRequestModel().merge(
412 merge_status = PullRequestModel().merge(
413 pull_request, user_admin, extras=merge_extras)
413 pull_request, user_admin, extras=merge_extras)
414
414
415 assert not merge_status.executed
415 assert not merge_status.executed
416 assert 'pre_push' not in capture_rcextensions
416 assert 'pre_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
417 assert 'post_push' not in capture_rcextensions
418
418
419 def test_merge_fails_if_target_is_locked(
419 def test_merge_fails_if_target_is_locked(
420 self, pr_util, user_regular, merge_extras):
420 self, pr_util, user_regular, merge_extras):
421 pull_request = pr_util.create_pull_request(
421 pull_request = pr_util.create_pull_request(
422 approved=True, mergeable=True)
422 approved=True, mergeable=True)
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
423 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
424 pull_request.target_repo.locked = locked_by
424 pull_request.target_repo.locked = locked_by
425 # TODO: johbo: Check if this can work based on the database, currently
425 # TODO: johbo: Check if this can work based on the database, currently
426 # all data is pre-computed, that's why just updating the DB is not
426 # all data is pre-computed, that's why just updating the DB is not
427 # enough.
427 # enough.
428 merge_extras['locked_by'] = locked_by
428 merge_extras['locked_by'] = locked_by
429 merge_extras['repository'] = pull_request.target_repo.repo_name
429 merge_extras['repository'] = pull_request.target_repo.repo_name
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
430 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
431 Session().commit()
431 Session().commit()
432 merge_status = PullRequestModel().merge(
432 merge_status = PullRequestModel().merge(
433 pull_request, user_regular, extras=merge_extras)
433 pull_request, user_regular, extras=merge_extras)
434 assert not merge_status.executed
434 assert not merge_status.executed
435
435
436
436
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
437 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
438 (False, 1, 0),
438 (False, 1, 0),
439 (True, 0, 1),
439 (True, 0, 1),
440 ])
440 ])
441 def test_outdated_comments(
441 def test_outdated_comments(
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
442 pr_util, use_outdated, inlines_count, outdated_count, config_stub):
443 pull_request = pr_util.create_pull_request()
443 pull_request = pr_util.create_pull_request()
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
444 pr_util.create_inline_comment(file_path='not_in_updated_diff')
445
445
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
446 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
447 pr_util.add_one_commit()
447 pr_util.add_one_commit()
448 assert_inline_comments(
448 assert_inline_comments(
449 pull_request, visible=inlines_count, outdated=outdated_count)
449 pull_request, visible=inlines_count, outdated=outdated_count)
450 outdated_comment_mock.assert_called_with(pull_request)
450 outdated_comment_mock.assert_called_with(pull_request)
451
451
452
452
453 @pytest.fixture
453 @pytest.fixture
454 def merge_extras(user_regular):
454 def merge_extras(user_regular):
455 """
455 """
456 Context for the vcs operation when running a merge.
456 Context for the vcs operation when running a merge.
457 """
457 """
458 extras = {
458 extras = {
459 'ip': '127.0.0.1',
459 'ip': '127.0.0.1',
460 'username': user_regular.username,
460 'username': user_regular.username,
461 'action': 'push',
461 'action': 'push',
462 'repository': 'fake_target_repo_name',
462 'repository': 'fake_target_repo_name',
463 'scm': 'git',
463 'scm': 'git',
464 'config': 'fake_config_ini_path',
464 'config': 'fake_config_ini_path',
465 'make_lock': None,
465 'make_lock': None,
466 'locked_by': [None, None, None],
466 'locked_by': [None, None, None],
467 'server_url': 'http://test.example.com:5000',
467 'server_url': 'http://test.example.com:5000',
468 'hooks': ['push', 'pull'],
468 'hooks': ['push', 'pull'],
469 'is_shadow_repo': False,
469 'is_shadow_repo': False,
470 }
470 }
471 return extras
471 return extras
472
472
473
473
474 @pytest.mark.usefixtures('config_stub')
474 @pytest.mark.usefixtures('config_stub')
475 class TestUpdateCommentHandling(object):
475 class TestUpdateCommentHandling(object):
476
476
477 @pytest.fixture(autouse=True, scope='class')
477 @pytest.fixture(autouse=True, scope='class')
478 def enable_outdated_comments(self, request, pylonsapp):
478 def enable_outdated_comments(self, request, pylonsapp):
479 config_patch = mock.patch.dict(
479 config_patch = mock.patch.dict(
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
480 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
481 config_patch.start()
481 config_patch.start()
482
482
483 @request.addfinalizer
483 @request.addfinalizer
484 def cleanup():
484 def cleanup():
485 config_patch.stop()
485 config_patch.stop()
486
486
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
487 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
488 commits = [
488 commits = [
489 {'message': 'a'},
489 {'message': 'a'},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
490 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
491 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
492 ]
492 ]
493 pull_request = pr_util.create_pull_request(
493 pull_request = pr_util.create_pull_request(
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
494 commits=commits, target_head='a', source_head='b', revisions=['b'])
495 pr_util.create_inline_comment(file_path='file_b')
495 pr_util.create_inline_comment(file_path='file_b')
496 pr_util.add_one_commit(head='c')
496 pr_util.add_one_commit(head='c')
497
497
498 assert_inline_comments(pull_request, visible=1, outdated=0)
498 assert_inline_comments(pull_request, visible=1, outdated=0)
499
499
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
500 def test_comment_stays_unflagged_on_change_above(self, pr_util):
501 original_content = ''.join(
501 original_content = ''.join(
502 ['line {}\n'.format(x) for x in range(1, 11)])
502 ['line {}\n'.format(x) for x in range(1, 11)])
503 updated_content = 'new_line_at_top\n' + original_content
503 updated_content = 'new_line_at_top\n' + original_content
504 commits = [
504 commits = [
505 {'message': 'a'},
505 {'message': 'a'},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
506 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
507 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
508 ]
508 ]
509 pull_request = pr_util.create_pull_request(
509 pull_request = pr_util.create_pull_request(
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
510 commits=commits, target_head='a', source_head='b', revisions=['b'])
511
511
512 with outdated_comments_patcher():
512 with outdated_comments_patcher():
513 comment = pr_util.create_inline_comment(
513 comment = pr_util.create_inline_comment(
514 line_no=u'n8', file_path='file_b')
514 line_no=u'n8', file_path='file_b')
515 pr_util.add_one_commit(head='c')
515 pr_util.add_one_commit(head='c')
516
516
517 assert_inline_comments(pull_request, visible=1, outdated=0)
517 assert_inline_comments(pull_request, visible=1, outdated=0)
518 assert comment.line_no == u'n9'
518 assert comment.line_no == u'n9'
519
519
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
520 def test_comment_stays_unflagged_on_change_below(self, pr_util):
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
521 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
522 updated_content = original_content + 'new_line_at_end\n'
522 updated_content = original_content + 'new_line_at_end\n'
523 commits = [
523 commits = [
524 {'message': 'a'},
524 {'message': 'a'},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
525 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
526 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
527 ]
527 ]
528 pull_request = pr_util.create_pull_request(
528 pull_request = pr_util.create_pull_request(
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
529 commits=commits, target_head='a', source_head='b', revisions=['b'])
530 pr_util.create_inline_comment(file_path='file_b')
530 pr_util.create_inline_comment(file_path='file_b')
531 pr_util.add_one_commit(head='c')
531 pr_util.add_one_commit(head='c')
532
532
533 assert_inline_comments(pull_request, visible=1, outdated=0)
533 assert_inline_comments(pull_request, visible=1, outdated=0)
534
534
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
535 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
536 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
537 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
538 change_lines = list(base_lines)
538 change_lines = list(base_lines)
539 change_lines.insert(6, 'line 6a added\n')
539 change_lines.insert(6, 'line 6a added\n')
540
540
541 # Changes on the last line of sight
541 # Changes on the last line of sight
542 update_lines = list(change_lines)
542 update_lines = list(change_lines)
543 update_lines[0] = 'line 1 changed\n'
543 update_lines[0] = 'line 1 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
544 update_lines[-1] = 'line 12 changed\n'
545
545
546 def file_b(lines):
546 def file_b(lines):
547 return FileNode('file_b', ''.join(lines))
547 return FileNode('file_b', ''.join(lines))
548
548
549 commits = [
549 commits = [
550 {'message': 'a', 'added': [file_b(base_lines)]},
550 {'message': 'a', 'added': [file_b(base_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
551 {'message': 'b', 'changed': [file_b(change_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
552 {'message': 'c', 'changed': [file_b(update_lines)]},
553 ]
553 ]
554
554
555 pull_request = pr_util.create_pull_request(
555 pull_request = pr_util.create_pull_request(
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
556 commits=commits, target_head='a', source_head='b', revisions=['b'])
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
557 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
558
558
559 with outdated_comments_patcher():
559 with outdated_comments_patcher():
560 pr_util.add_one_commit(head='c')
560 pr_util.add_one_commit(head='c')
561 assert_inline_comments(pull_request, visible=0, outdated=1)
561 assert_inline_comments(pull_request, visible=0, outdated=1)
562
562
563 @pytest.mark.parametrize("change, content", [
563 @pytest.mark.parametrize("change, content", [
564 ('changed', 'changed\n'),
564 ('changed', 'changed\n'),
565 ('removed', ''),
565 ('removed', ''),
566 ], ids=['changed', 'removed'])
566 ], ids=['changed', 'removed'])
567 def test_comment_flagged_on_change(self, pr_util, change, content):
567 def test_comment_flagged_on_change(self, pr_util, change, content):
568 commits = [
568 commits = [
569 {'message': 'a'},
569 {'message': 'a'},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
570 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
571 {'message': 'c', change: [FileNode('file_b', content)]},
572 ]
572 ]
573 pull_request = pr_util.create_pull_request(
573 pull_request = pr_util.create_pull_request(
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
574 commits=commits, target_head='a', source_head='b', revisions=['b'])
575 pr_util.create_inline_comment(file_path='file_b')
575 pr_util.create_inline_comment(file_path='file_b')
576
576
577 with outdated_comments_patcher():
577 with outdated_comments_patcher():
578 pr_util.add_one_commit(head='c')
578 pr_util.add_one_commit(head='c')
579 assert_inline_comments(pull_request, visible=0, outdated=1)
579 assert_inline_comments(pull_request, visible=0, outdated=1)
580
580
581
581
582 @pytest.mark.usefixtures('config_stub')
582 @pytest.mark.usefixtures('config_stub')
583 class TestUpdateChangedFiles(object):
583 class TestUpdateChangedFiles(object):
584
584
585 def test_no_changes_on_unchanged_diff(self, pr_util):
585 def test_no_changes_on_unchanged_diff(self, pr_util):
586 commits = [
586 commits = [
587 {'message': 'a'},
587 {'message': 'a'},
588 {'message': 'b',
588 {'message': 'b',
589 'added': [FileNode('file_b', 'test_content b\n')]},
589 'added': [FileNode('file_b', 'test_content b\n')]},
590 {'message': 'c',
590 {'message': 'c',
591 'added': [FileNode('file_c', 'test_content c\n')]},
591 'added': [FileNode('file_c', 'test_content c\n')]},
592 ]
592 ]
593 # open a PR from a to b, adding file_b
593 # open a PR from a to b, adding file_b
594 pull_request = pr_util.create_pull_request(
594 pull_request = pr_util.create_pull_request(
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
595 commits=commits, target_head='a', source_head='b', revisions=['b'],
596 name_suffix='per-file-review')
596 name_suffix='per-file-review')
597
597
598 # modify PR adding new file file_c
598 # modify PR adding new file file_c
599 pr_util.add_one_commit(head='c')
599 pr_util.add_one_commit(head='c')
600
600
601 assert_pr_file_changes(
601 assert_pr_file_changes(
602 pull_request,
602 pull_request,
603 added=['file_c'],
603 added=['file_c'],
604 modified=[],
604 modified=[],
605 removed=[])
605 removed=[])
606
606
607 def test_modify_and_undo_modification_diff(self, pr_util):
607 def test_modify_and_undo_modification_diff(self, pr_util):
608 commits = [
608 commits = [
609 {'message': 'a'},
609 {'message': 'a'},
610 {'message': 'b',
610 {'message': 'b',
611 'added': [FileNode('file_b', 'test_content b\n')]},
611 'added': [FileNode('file_b', 'test_content b\n')]},
612 {'message': 'c',
612 {'message': 'c',
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
613 'changed': [FileNode('file_b', 'test_content b modified\n')]},
614 {'message': 'd',
614 {'message': 'd',
615 'changed': [FileNode('file_b', 'test_content b\n')]},
615 'changed': [FileNode('file_b', 'test_content b\n')]},
616 ]
616 ]
617 # open a PR from a to b, adding file_b
617 # open a PR from a to b, adding file_b
618 pull_request = pr_util.create_pull_request(
618 pull_request = pr_util.create_pull_request(
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
619 commits=commits, target_head='a', source_head='b', revisions=['b'],
620 name_suffix='per-file-review')
620 name_suffix='per-file-review')
621
621
622 # modify PR modifying file file_b
622 # modify PR modifying file file_b
623 pr_util.add_one_commit(head='c')
623 pr_util.add_one_commit(head='c')
624
624
625 assert_pr_file_changes(
625 assert_pr_file_changes(
626 pull_request,
626 pull_request,
627 added=[],
627 added=[],
628 modified=['file_b'],
628 modified=['file_b'],
629 removed=[])
629 removed=[])
630
630
631 # move the head again to d, which rollbacks change,
631 # move the head again to d, which rollbacks change,
632 # meaning we should indicate no changes
632 # meaning we should indicate no changes
633 pr_util.add_one_commit(head='d')
633 pr_util.add_one_commit(head='d')
634
634
635 assert_pr_file_changes(
635 assert_pr_file_changes(
636 pull_request,
636 pull_request,
637 added=[],
637 added=[],
638 modified=[],
638 modified=[],
639 removed=[])
639 removed=[])
640
640
641 def test_updated_all_files_in_pr(self, pr_util):
641 def test_updated_all_files_in_pr(self, pr_util):
642 commits = [
642 commits = [
643 {'message': 'a'},
643 {'message': 'a'},
644 {'message': 'b', 'added': [
644 {'message': 'b', 'added': [
645 FileNode('file_a', 'test_content a\n'),
645 FileNode('file_a', 'test_content a\n'),
646 FileNode('file_b', 'test_content b\n'),
646 FileNode('file_b', 'test_content b\n'),
647 FileNode('file_c', 'test_content c\n')]},
647 FileNode('file_c', 'test_content c\n')]},
648 {'message': 'c', 'changed': [
648 {'message': 'c', 'changed': [
649 FileNode('file_a', 'test_content a changed\n'),
649 FileNode('file_a', 'test_content a changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
650 FileNode('file_b', 'test_content b changed\n'),
651 FileNode('file_c', 'test_content c changed\n')]},
651 FileNode('file_c', 'test_content c changed\n')]},
652 ]
652 ]
653 # open a PR from a to b, changing 3 files
653 # open a PR from a to b, changing 3 files
654 pull_request = pr_util.create_pull_request(
654 pull_request = pr_util.create_pull_request(
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
655 commits=commits, target_head='a', source_head='b', revisions=['b'],
656 name_suffix='per-file-review')
656 name_suffix='per-file-review')
657
657
658 pr_util.add_one_commit(head='c')
658 pr_util.add_one_commit(head='c')
659
659
660 assert_pr_file_changes(
660 assert_pr_file_changes(
661 pull_request,
661 pull_request,
662 added=[],
662 added=[],
663 modified=['file_a', 'file_b', 'file_c'],
663 modified=['file_a', 'file_b', 'file_c'],
664 removed=[])
664 removed=[])
665
665
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
666 def test_updated_and_removed_all_files_in_pr(self, pr_util):
667 commits = [
667 commits = [
668 {'message': 'a'},
668 {'message': 'a'},
669 {'message': 'b', 'added': [
669 {'message': 'b', 'added': [
670 FileNode('file_a', 'test_content a\n'),
670 FileNode('file_a', 'test_content a\n'),
671 FileNode('file_b', 'test_content b\n'),
671 FileNode('file_b', 'test_content b\n'),
672 FileNode('file_c', 'test_content c\n')]},
672 FileNode('file_c', 'test_content c\n')]},
673 {'message': 'c', 'removed': [
673 {'message': 'c', 'removed': [
674 FileNode('file_a', 'test_content a changed\n'),
674 FileNode('file_a', 'test_content a changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
675 FileNode('file_b', 'test_content b changed\n'),
676 FileNode('file_c', 'test_content c changed\n')]},
676 FileNode('file_c', 'test_content c changed\n')]},
677 ]
677 ]
678 # open a PR from a to b, removing 3 files
678 # open a PR from a to b, removing 3 files
679 pull_request = pr_util.create_pull_request(
679 pull_request = pr_util.create_pull_request(
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
680 commits=commits, target_head='a', source_head='b', revisions=['b'],
681 name_suffix='per-file-review')
681 name_suffix='per-file-review')
682
682
683 pr_util.add_one_commit(head='c')
683 pr_util.add_one_commit(head='c')
684
684
685 assert_pr_file_changes(
685 assert_pr_file_changes(
686 pull_request,
686 pull_request,
687 added=[],
687 added=[],
688 modified=[],
688 modified=[],
689 removed=['file_a', 'file_b', 'file_c'])
689 removed=['file_a', 'file_b', 'file_c'])
690
690
691
691
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
692 def test_update_writes_snapshot_into_pull_request_version(pr_util, config_stub):
693 model = PullRequestModel()
693 model = PullRequestModel()
694 pull_request = pr_util.create_pull_request()
694 pull_request = pr_util.create_pull_request()
695 pr_util.update_source_repository()
695 pr_util.update_source_repository()
696
696
697 model.update_commits(pull_request)
697 model.update_commits(pull_request)
698
698
699 # Expect that it has a version entry now
699 # Expect that it has a version entry now
700 assert len(model.get_versions(pull_request)) == 1
700 assert len(model.get_versions(pull_request)) == 1
701
701
702
702
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
703 def test_update_skips_new_version_if_unchanged(pr_util, config_stub):
704 pull_request = pr_util.create_pull_request()
704 pull_request = pr_util.create_pull_request()
705 model = PullRequestModel()
705 model = PullRequestModel()
706 model.update_commits(pull_request)
706 model.update_commits(pull_request)
707
707
708 # Expect that it still has no versions
708 # Expect that it still has no versions
709 assert len(model.get_versions(pull_request)) == 0
709 assert len(model.get_versions(pull_request)) == 0
710
710
711
711
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
712 def test_update_assigns_comments_to_the_new_version(pr_util, config_stub):
713 model = PullRequestModel()
713 model = PullRequestModel()
714 pull_request = pr_util.create_pull_request()
714 pull_request = pr_util.create_pull_request()
715 comment = pr_util.create_comment()
715 comment = pr_util.create_comment()
716 pr_util.update_source_repository()
716 pr_util.update_source_repository()
717
717
718 model.update_commits(pull_request)
718 model.update_commits(pull_request)
719
719
720 # Expect that the comment is linked to the pr version now
720 # Expect that the comment is linked to the pr version now
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
721 assert comment.pull_request_version == model.get_versions(pull_request)[0]
722
722
723
723
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
724 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util, config_stub):
725 model = PullRequestModel()
725 model = PullRequestModel()
726 pull_request = pr_util.create_pull_request()
726 pull_request = pr_util.create_pull_request()
727 pr_util.update_source_repository()
727 pr_util.update_source_repository()
728 pr_util.update_source_repository()
728 pr_util.update_source_repository()
729
729
730 model.update_commits(pull_request)
730 model.update_commits(pull_request)
731
731
732 # Expect to find a new comment about the change
732 # Expect to find a new comment about the change
733 expected_message = textwrap.dedent(
733 expected_message = textwrap.dedent(
734 """\
734 """\
735 Pull request updated. Auto status change to |under_review|
735 Pull request updated. Auto status change to |under_review|
736
736
737 .. role:: added
737 .. role:: added
738 .. role:: removed
738 .. role:: removed
739 .. parsed-literal::
739 .. parsed-literal::
740
740
741 Changed commits:
741 Changed commits:
742 * :added:`1 added`
742 * :added:`1 added`
743 * :removed:`0 removed`
743 * :removed:`0 removed`
744
744
745 Changed files:
745 Changed files:
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
746 * `A file_2 <#a_c--92ed3b5f07b4>`_
747
747
748 .. |under_review| replace:: *"Under Review"*"""
748 .. |under_review| replace:: *"Under Review"*"""
749 )
749 )
750 pull_request_comments = sorted(
750 pull_request_comments = sorted(
751 pull_request.comments, key=lambda c: c.modified_at)
751 pull_request.comments, key=lambda c: c.modified_at)
752 update_comment = pull_request_comments[-1]
752 update_comment = pull_request_comments[-1]
753 assert update_comment.text == expected_message
753 assert update_comment.text == expected_message
754
754
755
755
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
756 def test_create_version_from_snapshot_updates_attributes(pr_util, config_stub):
757 pull_request = pr_util.create_pull_request()
757 pull_request = pr_util.create_pull_request()
758
758
759 # Avoiding default values
759 # Avoiding default values
760 pull_request.status = PullRequest.STATUS_CLOSED
760 pull_request.status = PullRequest.STATUS_CLOSED
761 pull_request._last_merge_source_rev = "0" * 40
761 pull_request._last_merge_source_rev = "0" * 40
762 pull_request._last_merge_target_rev = "1" * 40
762 pull_request._last_merge_target_rev = "1" * 40
763 pull_request._last_merge_status = 1
763 pull_request.last_merge_status = 1
764 pull_request.merge_rev = "2" * 40
764 pull_request.merge_rev = "2" * 40
765
765
766 # Remember automatic values
766 # Remember automatic values
767 created_on = pull_request.created_on
767 created_on = pull_request.created_on
768 updated_on = pull_request.updated_on
768 updated_on = pull_request.updated_on
769
769
770 # Create a new version of the pull request
770 # Create a new version of the pull request
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
771 version = PullRequestModel()._create_version_from_snapshot(pull_request)
772
772
773 # Check attributes
773 # Check attributes
774 assert version.title == pr_util.create_parameters['title']
774 assert version.title == pr_util.create_parameters['title']
775 assert version.description == pr_util.create_parameters['description']
775 assert version.description == pr_util.create_parameters['description']
776 assert version.status == PullRequest.STATUS_CLOSED
776 assert version.status == PullRequest.STATUS_CLOSED
777
777
778 # versions get updated created_on
778 # versions get updated created_on
779 assert version.created_on != created_on
779 assert version.created_on != created_on
780
780
781 assert version.updated_on == updated_on
781 assert version.updated_on == updated_on
782 assert version.user_id == pull_request.user_id
782 assert version.user_id == pull_request.user_id
783 assert version.revisions == pr_util.create_parameters['revisions']
783 assert version.revisions == pr_util.create_parameters['revisions']
784 assert version.source_repo == pr_util.source_repository
784 assert version.source_repo == pr_util.source_repository
785 assert version.source_ref == pr_util.create_parameters['source_ref']
785 assert version.source_ref == pr_util.create_parameters['source_ref']
786 assert version.target_repo == pr_util.target_repository
786 assert version.target_repo == pr_util.target_repository
787 assert version.target_ref == pr_util.create_parameters['target_ref']
787 assert version.target_ref == pr_util.create_parameters['target_ref']
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
788 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
789 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
790 assert version._last_merge_status == pull_request._last_merge_status
790 assert version.last_merge_status == pull_request.last_merge_status
791 assert version.merge_rev == pull_request.merge_rev
791 assert version.merge_rev == pull_request.merge_rev
792 assert version.pull_request == pull_request
792 assert version.pull_request == pull_request
793
793
794
794
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
795 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util, config_stub):
796 version1 = pr_util.create_version_of_pull_request()
796 version1 = pr_util.create_version_of_pull_request()
797 comment_linked = pr_util.create_comment(linked_to=version1)
797 comment_linked = pr_util.create_comment(linked_to=version1)
798 comment_unlinked = pr_util.create_comment()
798 comment_unlinked = pr_util.create_comment()
799 version2 = pr_util.create_version_of_pull_request()
799 version2 = pr_util.create_version_of_pull_request()
800
800
801 PullRequestModel()._link_comments_to_version(version2)
801 PullRequestModel()._link_comments_to_version(version2)
802
802
803 # Expect that only the new comment is linked to version2
803 # Expect that only the new comment is linked to version2
804 assert (
804 assert (
805 comment_unlinked.pull_request_version_id ==
805 comment_unlinked.pull_request_version_id ==
806 version2.pull_request_version_id)
806 version2.pull_request_version_id)
807 assert (
807 assert (
808 comment_linked.pull_request_version_id ==
808 comment_linked.pull_request_version_id ==
809 version1.pull_request_version_id)
809 version1.pull_request_version_id)
810 assert (
810 assert (
811 comment_unlinked.pull_request_version_id !=
811 comment_unlinked.pull_request_version_id !=
812 comment_linked.pull_request_version_id)
812 comment_linked.pull_request_version_id)
813
813
814
814
815 def test_calculate_commits():
815 def test_calculate_commits():
816 old_ids = [1, 2, 3]
816 old_ids = [1, 2, 3]
817 new_ids = [1, 3, 4, 5]
817 new_ids = [1, 3, 4, 5]
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
818 change = PullRequestModel()._calculate_commit_id_changes(old_ids, new_ids)
819 assert change.added == [4, 5]
819 assert change.added == [4, 5]
820 assert change.common == [1, 3]
820 assert change.common == [1, 3]
821 assert change.removed == [2]
821 assert change.removed == [2]
822 assert change.total == [1, 3, 4, 5]
822 assert change.total == [1, 3, 4, 5]
823
823
824
824
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
825 def assert_inline_comments(pull_request, visible=None, outdated=None):
826 if visible is not None:
826 if visible is not None:
827 inline_comments = CommentsModel().get_inline_comments(
827 inline_comments = CommentsModel().get_inline_comments(
828 pull_request.target_repo.repo_id, pull_request=pull_request)
828 pull_request.target_repo.repo_id, pull_request=pull_request)
829 inline_cnt = CommentsModel().get_inline_comments_count(
829 inline_cnt = CommentsModel().get_inline_comments_count(
830 inline_comments)
830 inline_comments)
831 assert inline_cnt == visible
831 assert inline_cnt == visible
832 if outdated is not None:
832 if outdated is not None:
833 outdated_comments = CommentsModel().get_outdated_comments(
833 outdated_comments = CommentsModel().get_outdated_comments(
834 pull_request.target_repo.repo_id, pull_request)
834 pull_request.target_repo.repo_id, pull_request)
835 assert len(outdated_comments) == outdated
835 assert len(outdated_comments) == outdated
836
836
837
837
838 def assert_pr_file_changes(
838 def assert_pr_file_changes(
839 pull_request, added=None, modified=None, removed=None):
839 pull_request, added=None, modified=None, removed=None):
840 pr_versions = PullRequestModel().get_versions(pull_request)
840 pr_versions = PullRequestModel().get_versions(pull_request)
841 # always use first version, ie original PR to calculate changes
841 # always use first version, ie original PR to calculate changes
842 pull_request_version = pr_versions[0]
842 pull_request_version = pr_versions[0]
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
843 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
844 pull_request, pull_request_version)
844 pull_request, pull_request_version)
845 file_changes = PullRequestModel()._calculate_file_changes(
845 file_changes = PullRequestModel()._calculate_file_changes(
846 old_diff_data, new_diff_data)
846 old_diff_data, new_diff_data)
847
847
848 assert added == file_changes.added, \
848 assert added == file_changes.added, \
849 'expected added:%s vs value:%s' % (added, file_changes.added)
849 'expected added:%s vs value:%s' % (added, file_changes.added)
850 assert modified == file_changes.modified, \
850 assert modified == file_changes.modified, \
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
851 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
852 assert removed == file_changes.removed, \
852 assert removed == file_changes.removed, \
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
853 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
854
854
855
855
856 def outdated_comments_patcher(use_outdated=True):
856 def outdated_comments_patcher(use_outdated=True):
857 return mock.patch.object(
857 return mock.patch.object(
858 CommentsModel, 'use_outdated_comments',
858 CommentsModel, 'use_outdated_comments',
859 return_value=use_outdated)
859 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now