##// END OF EJS Templates
pull-requests: handle exceptions in state change and improve logging.
marcink -
r3927:65220619 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,1742 +1,1744 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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
26
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 import collections
31 import collections
32
32
33 from pyramid import compat
33 from pyramid import compat
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
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 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
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 = collections.namedtuple('UpdateResponse', [
68 UpdateResponse = collections.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 = diffs.DEFAULT_CONTEXT
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78
78
79 UPDATE_STATUS_MESSAGES = {
79 UPDATE_STATUS_MESSAGES = {
80 UpdateFailureReason.NONE: lazy_ugettext(
80 UpdateFailureReason.NONE: lazy_ugettext(
81 'Pull request update successful.'),
81 'Pull request update successful.'),
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 'Pull request update failed because of an unknown error.'),
83 'Pull request update failed because of an unknown error.'),
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 'No update needed because the source and target have not changed.'),
85 'No update needed because the source and target have not changed.'),
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 'Pull request cannot be updated because the reference type is '
87 'Pull request cannot be updated because the reference type is '
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 'This pull request cannot be updated because the target '
90 'This pull request cannot be updated because the target '
91 'reference is missing.'),
91 'reference is missing.'),
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 'This pull request cannot be updated because the source '
93 'This pull request cannot be updated because the source '
94 'reference is missing.'),
94 'reference is missing.'),
95 }
95 }
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98
98
99 def __get_pull_request(self, pull_request):
99 def __get_pull_request(self, pull_request):
100 return self._get_instance((
100 return self._get_instance((
101 PullRequest, PullRequestVersion), pull_request)
101 PullRequest, PullRequestVersion), pull_request)
102
102
103 def _check_perms(self, perms, pull_request, user, api=False):
103 def _check_perms(self, perms, pull_request, user, api=False):
104 if not api:
104 if not api:
105 return h.HasRepoPermissionAny(*perms)(
105 return h.HasRepoPermissionAny(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107 else:
107 else:
108 return h.HasRepoPermissionAnyApi(*perms)(
108 return h.HasRepoPermissionAnyApi(*perms)(
109 user=user, repo_name=pull_request.target_repo.repo_name)
109 user=user, repo_name=pull_request.target_repo.repo_name)
110
110
111 def check_user_read(self, pull_request, user, api=False):
111 def check_user_read(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_merge(self, pull_request, user, api=False):
115 def check_user_merge(self, pull_request, user, api=False):
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 return self._check_perms(_perms, pull_request, user, api)
117 return self._check_perms(_perms, pull_request, user, api)
118
118
119 def check_user_update(self, pull_request, user, api=False):
119 def check_user_update(self, pull_request, user, api=False):
120 owner = user.user_id == pull_request.user_id
120 owner = user.user_id == pull_request.user_id
121 return self.check_user_merge(pull_request, user, api) or owner
121 return self.check_user_merge(pull_request, user, api) or owner
122
122
123 def check_user_delete(self, pull_request, user):
123 def check_user_delete(self, pull_request, user):
124 owner = user.user_id == pull_request.user_id
124 owner = user.user_id == pull_request.user_id
125 _perms = ('repository.admin',)
125 _perms = ('repository.admin',)
126 return self._check_perms(_perms, pull_request, user) or owner
126 return self._check_perms(_perms, pull_request, user) or owner
127
127
128 def check_user_change_status(self, pull_request, user, api=False):
128 def check_user_change_status(self, pull_request, user, api=False):
129 reviewer = user.user_id in [x.user_id for x in
129 reviewer = user.user_id in [x.user_id for x in
130 pull_request.reviewers]
130 pull_request.reviewers]
131 return self.check_user_update(pull_request, user, api) or reviewer
131 return self.check_user_update(pull_request, user, api) or reviewer
132
132
133 def check_user_comment(self, pull_request, user):
133 def check_user_comment(self, pull_request, user):
134 owner = user.user_id == pull_request.user_id
134 owner = user.user_id == pull_request.user_id
135 return self.check_user_read(pull_request, user) or owner
135 return self.check_user_read(pull_request, user) or owner
136
136
137 def get(self, pull_request):
137 def get(self, pull_request):
138 return self.__get_pull_request(pull_request)
138 return self.__get_pull_request(pull_request)
139
139
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 opened_by=None, order_by=None,
141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=False):
142 order_dir='desc', only_created=False):
143 repo = None
143 repo = None
144 if repo_name:
144 if repo_name:
145 repo = self._get_repo(repo_name)
145 repo = self._get_repo(repo_name)
146
146
147 q = PullRequest.query()
147 q = PullRequest.query()
148
148
149 # source or target
149 # source or target
150 if repo and source:
150 if repo and source:
151 q = q.filter(PullRequest.source_repo == repo)
151 q = q.filter(PullRequest.source_repo == repo)
152 elif repo:
152 elif repo:
153 q = q.filter(PullRequest.target_repo == repo)
153 q = q.filter(PullRequest.target_repo == repo)
154
154
155 # closed,opened
155 # closed,opened
156 if statuses:
156 if statuses:
157 q = q.filter(PullRequest.status.in_(statuses))
157 q = q.filter(PullRequest.status.in_(statuses))
158
158
159 # opened by filter
159 # opened by filter
160 if opened_by:
160 if opened_by:
161 q = q.filter(PullRequest.user_id.in_(opened_by))
161 q = q.filter(PullRequest.user_id.in_(opened_by))
162
162
163 # only get those that are in "created" state
163 # only get those that are in "created" state
164 if only_created:
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
166
167 if order_by:
167 if order_by:
168 order_map = {
168 order_map = {
169 'name_raw': PullRequest.pull_request_id,
169 'name_raw': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
171 'title': PullRequest.title,
171 'title': PullRequest.title,
172 'updated_on_raw': PullRequest.updated_on,
172 'updated_on_raw': PullRequest.updated_on,
173 'target_repo': PullRequest.target_repo_id
173 'target_repo': PullRequest.target_repo_id
174 }
174 }
175 if order_dir == 'asc':
175 if order_dir == 'asc':
176 q = q.order_by(order_map[order_by].asc())
176 q = q.order_by(order_map[order_by].asc())
177 else:
177 else:
178 q = q.order_by(order_map[order_by].desc())
178 q = q.order_by(order_map[order_by].desc())
179
179
180 return q
180 return q
181
181
182 def count_all(self, repo_name, source=False, statuses=None,
182 def count_all(self, repo_name, source=False, statuses=None,
183 opened_by=None):
183 opened_by=None):
184 """
184 """
185 Count the number of pull requests for a specific repository.
185 Count the number of pull requests for a specific repository.
186
186
187 :param repo_name: target or source repo
187 :param repo_name: target or source repo
188 :param source: boolean flag to specify if repo_name refers to source
188 :param source: boolean flag to specify if repo_name refers to source
189 :param statuses: list of pull request statuses
189 :param statuses: list of pull request statuses
190 :param opened_by: author user of the pull request
190 :param opened_by: author user of the pull request
191 :returns: int number of pull requests
191 :returns: int number of pull requests
192 """
192 """
193 q = self._prepare_get_all_query(
193 q = self._prepare_get_all_query(
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
195
195
196 return q.count()
196 return q.count()
197
197
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
199 offset=0, length=None, order_by=None, order_dir='desc'):
199 offset=0, length=None, order_by=None, order_dir='desc'):
200 """
200 """
201 Get all pull requests for a specific repository.
201 Get all pull requests for a specific repository.
202
202
203 :param repo_name: target or source repo
203 :param repo_name: target or source repo
204 :param source: boolean flag to specify if repo_name refers to source
204 :param source: boolean flag to specify if repo_name refers to source
205 :param statuses: list of pull request statuses
205 :param statuses: list of pull request statuses
206 :param opened_by: author user of the pull request
206 :param opened_by: author user of the pull request
207 :param offset: pagination offset
207 :param offset: pagination offset
208 :param length: length of returned list
208 :param length: length of returned list
209 :param order_by: order of the returned list
209 :param order_by: order of the returned list
210 :param order_dir: 'asc' or 'desc' ordering direction
210 :param order_dir: 'asc' or 'desc' ordering direction
211 :returns: list of pull requests
211 :returns: list of pull requests
212 """
212 """
213 q = self._prepare_get_all_query(
213 q = self._prepare_get_all_query(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
215 order_by=order_by, order_dir=order_dir)
215 order_by=order_by, order_dir=order_dir)
216
216
217 if length:
217 if length:
218 pull_requests = q.limit(length).offset(offset).all()
218 pull_requests = q.limit(length).offset(offset).all()
219 else:
219 else:
220 pull_requests = q.all()
220 pull_requests = q.all()
221
221
222 return pull_requests
222 return pull_requests
223
223
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
225 opened_by=None):
225 opened_by=None):
226 """
226 """
227 Count the number of pull requests for a specific repository that are
227 Count the number of pull requests for a specific repository that are
228 awaiting review.
228 awaiting review.
229
229
230 :param repo_name: target or source repo
230 :param repo_name: target or source repo
231 :param source: boolean flag to specify if repo_name refers to source
231 :param source: boolean flag to specify if repo_name refers to source
232 :param statuses: list of pull request statuses
232 :param statuses: list of pull request statuses
233 :param opened_by: author user of the pull request
233 :param opened_by: author user of the pull request
234 :returns: int number of pull requests
234 :returns: int number of pull requests
235 """
235 """
236 pull_requests = self.get_awaiting_review(
236 pull_requests = self.get_awaiting_review(
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
238
238
239 return len(pull_requests)
239 return len(pull_requests)
240
240
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
242 opened_by=None, offset=0, length=None,
242 opened_by=None, offset=0, length=None,
243 order_by=None, order_dir='desc'):
243 order_by=None, order_dir='desc'):
244 """
244 """
245 Get all pull requests for a specific repository that are awaiting
245 Get all pull requests for a specific repository that are awaiting
246 review.
246 review.
247
247
248 :param repo_name: target or source repo
248 :param repo_name: target or source repo
249 :param source: boolean flag to specify if repo_name refers to source
249 :param source: boolean flag to specify if repo_name refers to source
250 :param statuses: list of pull request statuses
250 :param statuses: list of pull request statuses
251 :param opened_by: author user of the pull request
251 :param opened_by: author user of the pull request
252 :param offset: pagination offset
252 :param offset: pagination offset
253 :param length: length of returned list
253 :param length: length of returned list
254 :param order_by: order of the returned list
254 :param order_by: order of the returned list
255 :param order_dir: 'asc' or 'desc' ordering direction
255 :param order_dir: 'asc' or 'desc' ordering direction
256 :returns: list of pull requests
256 :returns: list of pull requests
257 """
257 """
258 pull_requests = self.get_all(
258 pull_requests = self.get_all(
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
260 order_by=order_by, order_dir=order_dir)
260 order_by=order_by, order_dir=order_dir)
261
261
262 _filtered_pull_requests = []
262 _filtered_pull_requests = []
263 for pr in pull_requests:
263 for pr in pull_requests:
264 status = pr.calculated_review_status()
264 status = pr.calculated_review_status()
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
267 _filtered_pull_requests.append(pr)
267 _filtered_pull_requests.append(pr)
268 if length:
268 if length:
269 return _filtered_pull_requests[offset:offset+length]
269 return _filtered_pull_requests[offset:offset+length]
270 else:
270 else:
271 return _filtered_pull_requests
271 return _filtered_pull_requests
272
272
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
274 opened_by=None, user_id=None):
274 opened_by=None, user_id=None):
275 """
275 """
276 Count the number of pull requests for a specific repository that are
276 Count the number of pull requests for a specific repository that are
277 awaiting review from a specific user.
277 awaiting review from a specific user.
278
278
279 :param repo_name: target or source repo
279 :param repo_name: target or source repo
280 :param source: boolean flag to specify if repo_name refers to source
280 :param source: boolean flag to specify if repo_name refers to source
281 :param statuses: list of pull request statuses
281 :param statuses: list of pull request statuses
282 :param opened_by: author user of the pull request
282 :param opened_by: author user of the pull request
283 :param user_id: reviewer user of the pull request
283 :param user_id: reviewer user of the pull request
284 :returns: int number of pull requests
284 :returns: int number of pull requests
285 """
285 """
286 pull_requests = self.get_awaiting_my_review(
286 pull_requests = self.get_awaiting_my_review(
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 user_id=user_id)
288 user_id=user_id)
289
289
290 return len(pull_requests)
290 return len(pull_requests)
291
291
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
293 opened_by=None, user_id=None, offset=0,
293 opened_by=None, user_id=None, offset=0,
294 length=None, order_by=None, order_dir='desc'):
294 length=None, order_by=None, order_dir='desc'):
295 """
295 """
296 Get all pull requests for a specific repository that are awaiting
296 Get all pull requests for a specific repository that are awaiting
297 review from a specific user.
297 review from a specific user.
298
298
299 :param repo_name: target or source repo
299 :param repo_name: target or source repo
300 :param source: boolean flag to specify if repo_name refers to source
300 :param source: boolean flag to specify if repo_name refers to source
301 :param statuses: list of pull request statuses
301 :param statuses: list of pull request statuses
302 :param opened_by: author user of the pull request
302 :param opened_by: author user of the pull request
303 :param user_id: reviewer user of the pull request
303 :param user_id: reviewer user of the pull request
304 :param offset: pagination offset
304 :param offset: pagination offset
305 :param length: length of returned list
305 :param length: length of returned list
306 :param order_by: order of the returned list
306 :param order_by: order of the returned list
307 :param order_dir: 'asc' or 'desc' ordering direction
307 :param order_dir: 'asc' or 'desc' ordering direction
308 :returns: list of pull requests
308 :returns: list of pull requests
309 """
309 """
310 pull_requests = self.get_all(
310 pull_requests = self.get_all(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 order_by=order_by, order_dir=order_dir)
312 order_by=order_by, order_dir=order_dir)
313
313
314 _my = PullRequestModel().get_not_reviewed(user_id)
314 _my = PullRequestModel().get_not_reviewed(user_id)
315 my_participation = []
315 my_participation = []
316 for pr in pull_requests:
316 for pr in pull_requests:
317 if pr in _my:
317 if pr in _my:
318 my_participation.append(pr)
318 my_participation.append(pr)
319 _filtered_pull_requests = my_participation
319 _filtered_pull_requests = my_participation
320 if length:
320 if length:
321 return _filtered_pull_requests[offset:offset+length]
321 return _filtered_pull_requests[offset:offset+length]
322 else:
322 else:
323 return _filtered_pull_requests
323 return _filtered_pull_requests
324
324
325 def get_not_reviewed(self, user_id):
325 def get_not_reviewed(self, user_id):
326 return [
326 return [
327 x.pull_request for x in PullRequestReviewers.query().filter(
327 x.pull_request for x in PullRequestReviewers.query().filter(
328 PullRequestReviewers.user_id == user_id).all()
328 PullRequestReviewers.user_id == user_id).all()
329 ]
329 ]
330
330
331 def _prepare_participating_query(self, user_id=None, statuses=None,
331 def _prepare_participating_query(self, user_id=None, statuses=None,
332 order_by=None, order_dir='desc'):
332 order_by=None, order_dir='desc'):
333 q = PullRequest.query()
333 q = PullRequest.query()
334 if user_id:
334 if user_id:
335 reviewers_subquery = Session().query(
335 reviewers_subquery = Session().query(
336 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.pull_request_id).filter(
337 PullRequestReviewers.user_id == user_id).subquery()
337 PullRequestReviewers.user_id == user_id).subquery()
338 user_filter = or_(
338 user_filter = or_(
339 PullRequest.user_id == user_id,
339 PullRequest.user_id == user_id,
340 PullRequest.pull_request_id.in_(reviewers_subquery)
340 PullRequest.pull_request_id.in_(reviewers_subquery)
341 )
341 )
342 q = PullRequest.query().filter(user_filter)
342 q = PullRequest.query().filter(user_filter)
343
343
344 # closed,opened
344 # closed,opened
345 if statuses:
345 if statuses:
346 q = q.filter(PullRequest.status.in_(statuses))
346 q = q.filter(PullRequest.status.in_(statuses))
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'title': PullRequest.title,
351 'title': PullRequest.title,
352 'updated_on_raw': PullRequest.updated_on,
352 'updated_on_raw': PullRequest.updated_on,
353 'target_repo': PullRequest.target_repo_id
353 'target_repo': PullRequest.target_repo_id
354 }
354 }
355 if order_dir == 'asc':
355 if order_dir == 'asc':
356 q = q.order_by(order_map[order_by].asc())
356 q = q.order_by(order_map[order_by].asc())
357 else:
357 else:
358 q = q.order_by(order_map[order_by].desc())
358 q = q.order_by(order_map[order_by].desc())
359
359
360 return q
360 return q
361
361
362 def count_im_participating_in(self, user_id=None, statuses=None):
362 def count_im_participating_in(self, user_id=None, statuses=None):
363 q = self._prepare_participating_query(user_id, statuses=statuses)
363 q = self._prepare_participating_query(user_id, statuses=statuses)
364 return q.count()
364 return q.count()
365
365
366 def get_im_participating_in(
366 def get_im_participating_in(
367 self, user_id=None, statuses=None, offset=0,
367 self, user_id=None, statuses=None, offset=0,
368 length=None, order_by=None, order_dir='desc'):
368 length=None, order_by=None, order_dir='desc'):
369 """
369 """
370 Get all Pull requests that i'm participating in, or i have opened
370 Get all Pull requests that i'm participating in, or i have opened
371 """
371 """
372
372
373 q = self._prepare_participating_query(
373 q = self._prepare_participating_query(
374 user_id, statuses=statuses, order_by=order_by,
374 user_id, statuses=statuses, order_by=order_by,
375 order_dir=order_dir)
375 order_dir=order_dir)
376
376
377 if length:
377 if length:
378 pull_requests = q.limit(length).offset(offset).all()
378 pull_requests = q.limit(length).offset(offset).all()
379 else:
379 else:
380 pull_requests = q.all()
380 pull_requests = q.all()
381
381
382 return pull_requests
382 return pull_requests
383
383
384 def get_versions(self, pull_request):
384 def get_versions(self, pull_request):
385 """
385 """
386 returns version of pull request sorted by ID descending
386 returns version of pull request sorted by ID descending
387 """
387 """
388 return PullRequestVersion.query()\
388 return PullRequestVersion.query()\
389 .filter(PullRequestVersion.pull_request == pull_request)\
389 .filter(PullRequestVersion.pull_request == pull_request)\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
391 .all()
391 .all()
392
392
393 def get_pr_version(self, pull_request_id, version=None):
393 def get_pr_version(self, pull_request_id, version=None):
394 at_version = None
394 at_version = None
395
395
396 if version and version == 'latest':
396 if version and version == 'latest':
397 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_ver = PullRequest.get(pull_request_id)
398 pull_request_obj = pull_request_ver
398 pull_request_obj = pull_request_ver
399 _org_pull_request_obj = pull_request_obj
399 _org_pull_request_obj = pull_request_obj
400 at_version = 'latest'
400 at_version = 'latest'
401 elif version:
401 elif version:
402 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_ver = PullRequestVersion.get_or_404(version)
403 pull_request_obj = pull_request_ver
403 pull_request_obj = pull_request_ver
404 _org_pull_request_obj = pull_request_ver.pull_request
404 _org_pull_request_obj = pull_request_ver.pull_request
405 at_version = pull_request_ver.pull_request_version_id
405 at_version = pull_request_ver.pull_request_version_id
406 else:
406 else:
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
408 pull_request_id)
408 pull_request_id)
409
409
410 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_display_obj = PullRequest.get_pr_display_object(
411 pull_request_obj, _org_pull_request_obj)
411 pull_request_obj, _org_pull_request_obj)
412
412
413 return _org_pull_request_obj, pull_request_obj, \
413 return _org_pull_request_obj, pull_request_obj, \
414 pull_request_display_obj, at_version
414 pull_request_display_obj, at_version
415
415
416 def create(self, created_by, source_repo, source_ref, target_repo,
416 def create(self, created_by, source_repo, source_ref, target_repo,
417 target_ref, revisions, reviewers, title, description=None,
417 target_ref, revisions, reviewers, title, description=None,
418 description_renderer=None,
418 description_renderer=None,
419 reviewer_data=None, translator=None, auth_user=None):
419 reviewer_data=None, translator=None, auth_user=None):
420 translator = translator or get_current_request().translate
420 translator = translator or get_current_request().translate
421
421
422 created_by_user = self._get_user(created_by)
422 created_by_user = self._get_user(created_by)
423 auth_user = auth_user or created_by_user.AuthUser()
423 auth_user = auth_user or created_by_user.AuthUser()
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.description_renderer = description_renderer
435 pull_request.description_renderer = description_renderer
436 pull_request.author = created_by_user
436 pull_request.author = created_by_user
437 pull_request.reviewer_data = reviewer_data
437 pull_request.reviewer_data = reviewer_data
438 pull_request.pull_request_state = pull_request.STATE_CREATING
438 pull_request.pull_request_state = pull_request.STATE_CREATING
439 Session().add(pull_request)
439 Session().add(pull_request)
440 Session().flush()
440 Session().flush()
441
441
442 reviewer_ids = set()
442 reviewer_ids = set()
443 # members / reviewers
443 # members / reviewers
444 for reviewer_object in reviewers:
444 for reviewer_object in reviewers:
445 user_id, reasons, mandatory, rules = reviewer_object
445 user_id, reasons, mandatory, rules = reviewer_object
446 user = self._get_user(user_id)
446 user = self._get_user(user_id)
447
447
448 # skip duplicates
448 # skip duplicates
449 if user.user_id in reviewer_ids:
449 if user.user_id in reviewer_ids:
450 continue
450 continue
451
451
452 reviewer_ids.add(user.user_id)
452 reviewer_ids.add(user.user_id)
453
453
454 reviewer = PullRequestReviewers()
454 reviewer = PullRequestReviewers()
455 reviewer.user = user
455 reviewer.user = user
456 reviewer.pull_request = pull_request
456 reviewer.pull_request = pull_request
457 reviewer.reasons = reasons
457 reviewer.reasons = reasons
458 reviewer.mandatory = mandatory
458 reviewer.mandatory = mandatory
459
459
460 # NOTE(marcink): pick only first rule for now
460 # NOTE(marcink): pick only first rule for now
461 rule_id = list(rules)[0] if rules else None
461 rule_id = list(rules)[0] if rules else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
463 if rule:
463 if rule:
464 review_group = rule.user_group_vote_rule(user_id)
464 review_group = rule.user_group_vote_rule(user_id)
465 # we check if this particular reviewer is member of a voting group
465 # we check if this particular reviewer is member of a voting group
466 if review_group:
466 if review_group:
467 # NOTE(marcink):
467 # NOTE(marcink):
468 # can be that user is member of more but we pick the first same,
468 # can be that user is member of more but we pick the first same,
469 # same as default reviewers algo
469 # same as default reviewers algo
470 review_group = review_group[0]
470 review_group = review_group[0]
471
471
472 rule_data = {
472 rule_data = {
473 'rule_name':
473 'rule_name':
474 rule.review_rule_name,
474 rule.review_rule_name,
475 'rule_user_group_entry_id':
475 'rule_user_group_entry_id':
476 review_group.repo_review_rule_users_group_id,
476 review_group.repo_review_rule_users_group_id,
477 'rule_user_group_name':
477 'rule_user_group_name':
478 review_group.users_group.users_group_name,
478 review_group.users_group.users_group_name,
479 'rule_user_group_members':
479 'rule_user_group_members':
480 [x.user.username for x in review_group.users_group.members],
480 [x.user.username for x in review_group.users_group.members],
481 'rule_user_group_members_id':
481 'rule_user_group_members_id':
482 [x.user.user_id for x in review_group.users_group.members],
482 [x.user.user_id for x in review_group.users_group.members],
483 }
483 }
484 # e.g {'vote_rule': -1, 'mandatory': True}
484 # e.g {'vote_rule': -1, 'mandatory': True}
485 rule_data.update(review_group.rule_data())
485 rule_data.update(review_group.rule_data())
486
486
487 reviewer.rule_data = rule_data
487 reviewer.rule_data = rule_data
488
488
489 Session().add(reviewer)
489 Session().add(reviewer)
490 Session().flush()
490 Session().flush()
491
491
492 # Set approval status to "Under Review" for all commits which are
492 # Set approval status to "Under Review" for all commits which are
493 # part of this pull request.
493 # part of this pull request.
494 ChangesetStatusModel().set_status(
494 ChangesetStatusModel().set_status(
495 repo=target_repo,
495 repo=target_repo,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
497 user=created_by_user,
497 user=created_by_user,
498 pull_request=pull_request
498 pull_request=pull_request
499 )
499 )
500 # we commit early at this point. This has to do with a fact
500 # we commit early at this point. This has to do with a fact
501 # that before queries do some row-locking. And because of that
501 # that before queries do some row-locking. And because of that
502 # we need to commit and finish transaction before below validate call
502 # we need to commit and finish transaction before below validate call
503 # that for large repos could be long resulting in long row locks
503 # that for large repos could be long resulting in long row locks
504 Session().commit()
504 Session().commit()
505
505
506 # prepare workspace, and run initial merge simulation. Set state during that
506 # prepare workspace, and run initial merge simulation. Set state during that
507 # operation
507 # operation
508 pull_request = PullRequest.get(pull_request.pull_request_id)
508 pull_request = PullRequest.get(pull_request.pull_request_id)
509
509
510 # set as merging, for simulation, and if finished to created so we mark
510 # set as merging, for merge simulation, and if finished to created so we mark
511 # simulation is working fine
511 # simulation is working fine
512 with pull_request.set_state(PullRequest.STATE_MERGING,
512 with pull_request.set_state(PullRequest.STATE_MERGING,
513 final_state=PullRequest.STATE_CREATED):
513 final_state=PullRequest.STATE_CREATED) as state_obj:
514 MergeCheck.validate(
514 MergeCheck.validate(
515 pull_request, auth_user=auth_user, translator=translator)
515 pull_request, auth_user=auth_user, translator=translator)
516
516
517 self.notify_reviewers(pull_request, reviewer_ids)
517 self.notify_reviewers(pull_request, reviewer_ids)
518 self.trigger_pull_request_hook(
518 self.trigger_pull_request_hook(
519 pull_request, created_by_user, 'create')
519 pull_request, created_by_user, 'create')
520
520
521 creation_data = pull_request.get_api_data(with_merge_state=False)
521 creation_data = pull_request.get_api_data(with_merge_state=False)
522 self._log_audit_action(
522 self._log_audit_action(
523 'repo.pull_request.create', {'data': creation_data},
523 'repo.pull_request.create', {'data': creation_data},
524 auth_user, pull_request)
524 auth_user, pull_request)
525
525
526 return pull_request
526 return pull_request
527
527
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
529 pull_request = self.__get_pull_request(pull_request)
529 pull_request = self.__get_pull_request(pull_request)
530 target_scm = pull_request.target_repo.scm_instance()
530 target_scm = pull_request.target_repo.scm_instance()
531 if action == 'create':
531 if action == 'create':
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
533 elif action == 'merge':
533 elif action == 'merge':
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
535 elif action == 'close':
535 elif action == 'close':
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
537 elif action == 'review_status_change':
537 elif action == 'review_status_change':
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
539 elif action == 'update':
539 elif action == 'update':
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
541 elif action == 'comment':
541 elif action == 'comment':
542 # dummy hook ! for comment. We want this function to handle all cases
542 # dummy hook ! for comment. We want this function to handle all cases
543 def trigger_hook(*args, **kwargs):
543 def trigger_hook(*args, **kwargs):
544 pass
544 pass
545 comment = data['comment']
545 comment = data['comment']
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
547 else:
547 else:
548 return
548 return
549
549
550 trigger_hook(
550 trigger_hook(
551 username=user.username,
551 username=user.username,
552 repo_name=pull_request.target_repo.repo_name,
552 repo_name=pull_request.target_repo.repo_name,
553 repo_alias=target_scm.alias,
553 repo_alias=target_scm.alias,
554 pull_request=pull_request,
554 pull_request=pull_request,
555 data=data)
555 data=data)
556
556
557 def _get_commit_ids(self, pull_request):
557 def _get_commit_ids(self, pull_request):
558 """
558 """
559 Return the commit ids of the merged pull request.
559 Return the commit ids of the merged pull request.
560
560
561 This method is not dealing correctly yet with the lack of autoupdates
561 This method is not dealing correctly yet with the lack of autoupdates
562 nor with the implicit target updates.
562 nor with the implicit target updates.
563 For example: if a commit in the source repo is already in the target it
563 For example: if a commit in the source repo is already in the target it
564 will be reported anyways.
564 will be reported anyways.
565 """
565 """
566 merge_rev = pull_request.merge_rev
566 merge_rev = pull_request.merge_rev
567 if merge_rev is None:
567 if merge_rev is None:
568 raise ValueError('This pull request was not merged yet')
568 raise ValueError('This pull request was not merged yet')
569
569
570 commit_ids = list(pull_request.revisions)
570 commit_ids = list(pull_request.revisions)
571 if merge_rev not in commit_ids:
571 if merge_rev not in commit_ids:
572 commit_ids.append(merge_rev)
572 commit_ids.append(merge_rev)
573
573
574 return commit_ids
574 return commit_ids
575
575
576 def merge_repo(self, pull_request, user, extras):
576 def merge_repo(self, pull_request, user, extras):
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
578 extras['user_agent'] = 'internal-merge'
578 extras['user_agent'] = 'internal-merge'
579 merge_state = self._merge_pull_request(pull_request, user, extras)
579 merge_state = self._merge_pull_request(pull_request, user, extras)
580 if merge_state.executed:
580 if merge_state.executed:
581 log.debug("Merge was successful, updating the pull request comments.")
581 log.debug("Merge was successful, updating the pull request comments.")
582 self._comment_and_close_pr(pull_request, user, merge_state)
582 self._comment_and_close_pr(pull_request, user, merge_state)
583
583
584 self._log_audit_action(
584 self._log_audit_action(
585 'repo.pull_request.merge',
585 'repo.pull_request.merge',
586 {'merge_state': merge_state.__dict__},
586 {'merge_state': merge_state.__dict__},
587 user, pull_request)
587 user, pull_request)
588
588
589 else:
589 else:
590 log.warn("Merge failed, not updating the pull request.")
590 log.warn("Merge failed, not updating the pull request.")
591 return merge_state
591 return merge_state
592
592
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
594 target_vcs = pull_request.target_repo.scm_instance()
594 target_vcs = pull_request.target_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
596
596
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
598 pr_id=pull_request.pull_request_id,
598 pr_id=pull_request.pull_request_id,
599 pr_title=pull_request.title,
599 pr_title=pull_request.title,
600 source_repo=source_vcs.name,
600 source_repo=source_vcs.name,
601 source_ref_name=pull_request.source_ref_parts.name,
601 source_ref_name=pull_request.source_ref_parts.name,
602 target_repo=target_vcs.name,
602 target_repo=target_vcs.name,
603 target_ref_name=pull_request.target_ref_parts.name,
603 target_ref_name=pull_request.target_ref_parts.name,
604 )
604 )
605
605
606 workspace_id = self._workspace_id(pull_request)
606 workspace_id = self._workspace_id(pull_request)
607 repo_id = pull_request.target_repo.repo_id
607 repo_id = pull_request.target_repo.repo_id
608 use_rebase = self._use_rebase_for_merging(pull_request)
608 use_rebase = self._use_rebase_for_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
610
610
611 target_ref = self._refresh_reference(
611 target_ref = self._refresh_reference(
612 pull_request.target_ref_parts, target_vcs)
612 pull_request.target_ref_parts, target_vcs)
613
613
614 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 host=vcs_settings.HOOKS_HOST,
616 host=vcs_settings.HOOKS_HOST,
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
618
618
619 with callback_daemon:
619 with callback_daemon:
620 # TODO: johbo: Implement a clean way to run a config_override
620 # TODO: johbo: Implement a clean way to run a config_override
621 # for a single call.
621 # for a single call.
622 target_vcs.config.set(
622 target_vcs.config.set(
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
624
624
625 user_name = user.short_contact
625 user_name = user.short_contact
626 merge_state = target_vcs.merge(
626 merge_state = target_vcs.merge(
627 repo_id, workspace_id, target_ref, source_vcs,
627 repo_id, workspace_id, target_ref, source_vcs,
628 pull_request.source_ref_parts,
628 pull_request.source_ref_parts,
629 user_name=user_name, user_email=user.email,
629 user_name=user_name, user_email=user.email,
630 message=message, use_rebase=use_rebase,
630 message=message, use_rebase=use_rebase,
631 close_branch=close_branch)
631 close_branch=close_branch)
632 return merge_state
632 return merge_state
633
633
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
636 pull_request.updated_on = datetime.datetime.now()
636 pull_request.updated_on = datetime.datetime.now()
637 close_msg = close_msg or 'Pull request merged and closed'
637 close_msg = close_msg or 'Pull request merged and closed'
638
638
639 CommentsModel().create(
639 CommentsModel().create(
640 text=safe_unicode(close_msg),
640 text=safe_unicode(close_msg),
641 repo=pull_request.target_repo.repo_id,
641 repo=pull_request.target_repo.repo_id,
642 user=user.user_id,
642 user=user.user_id,
643 pull_request=pull_request.pull_request_id,
643 pull_request=pull_request.pull_request_id,
644 f_path=None,
644 f_path=None,
645 line_no=None,
645 line_no=None,
646 closing_pr=True
646 closing_pr=True
647 )
647 )
648
648
649 Session().add(pull_request)
649 Session().add(pull_request)
650 Session().flush()
650 Session().flush()
651 # TODO: paris: replace invalidation with less radical solution
651 # TODO: paris: replace invalidation with less radical solution
652 ScmModel().mark_for_invalidation(
652 ScmModel().mark_for_invalidation(
653 pull_request.target_repo.repo_name)
653 pull_request.target_repo.repo_name)
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
655
655
656 def has_valid_update_type(self, pull_request):
656 def has_valid_update_type(self, pull_request):
657 source_ref_type = pull_request.source_ref_parts.type
657 source_ref_type = pull_request.source_ref_parts.type
658 return source_ref_type in self.REF_TYPES
658 return source_ref_type in self.REF_TYPES
659
659
660 def update_commits(self, pull_request):
660 def update_commits(self, pull_request):
661 """
661 """
662 Get the updated list of commits for the pull request
662 Get the updated list of commits for the pull request
663 and return the new pull request version and the list
663 and return the new pull request version and the list
664 of commits processed by this update action
664 of commits processed by this update action
665 """
665 """
666 pull_request = self.__get_pull_request(pull_request)
666 pull_request = self.__get_pull_request(pull_request)
667 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_type = pull_request.source_ref_parts.type
668 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_name = pull_request.source_ref_parts.name
669 source_ref_id = pull_request.source_ref_parts.commit_id
669 source_ref_id = pull_request.source_ref_parts.commit_id
670
670
671 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_type = pull_request.target_ref_parts.type
672 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_name = pull_request.target_ref_parts.name
673 target_ref_id = pull_request.target_ref_parts.commit_id
673 target_ref_id = pull_request.target_ref_parts.commit_id
674
674
675 if not self.has_valid_update_type(pull_request):
675 if not self.has_valid_update_type(pull_request):
676 log.debug("Skipping update of pull request %s due to ref type: %s",
676 log.debug("Skipping update of pull request %s due to ref type: %s",
677 pull_request, source_ref_type)
677 pull_request, source_ref_type)
678 return UpdateResponse(
678 return UpdateResponse(
679 executed=False,
679 executed=False,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
681 old=pull_request, new=None, changes=None,
681 old=pull_request, new=None, changes=None,
682 source_changed=False, target_changed=False)
682 source_changed=False, target_changed=False)
683
683
684 # source repo
684 # source repo
685 source_repo = pull_request.source_repo.scm_instance()
685 source_repo = pull_request.source_repo.scm_instance()
686
686
687 try:
687 try:
688 source_commit = source_repo.get_commit(commit_id=source_ref_name)
688 source_commit = source_repo.get_commit(commit_id=source_ref_name)
689 except CommitDoesNotExistError:
689 except CommitDoesNotExistError:
690 return UpdateResponse(
690 return UpdateResponse(
691 executed=False,
691 executed=False,
692 reason=UpdateFailureReason.MISSING_SOURCE_REF,
692 reason=UpdateFailureReason.MISSING_SOURCE_REF,
693 old=pull_request, new=None, changes=None,
693 old=pull_request, new=None, changes=None,
694 source_changed=False, target_changed=False)
694 source_changed=False, target_changed=False)
695
695
696 source_changed = source_ref_id != source_commit.raw_id
696 source_changed = source_ref_id != source_commit.raw_id
697
697
698 # target repo
698 # target repo
699 target_repo = pull_request.target_repo.scm_instance()
699 target_repo = pull_request.target_repo.scm_instance()
700
700
701 try:
701 try:
702 target_commit = target_repo.get_commit(commit_id=target_ref_name)
702 target_commit = target_repo.get_commit(commit_id=target_ref_name)
703 except CommitDoesNotExistError:
703 except CommitDoesNotExistError:
704 return UpdateResponse(
704 return UpdateResponse(
705 executed=False,
705 executed=False,
706 reason=UpdateFailureReason.MISSING_TARGET_REF,
706 reason=UpdateFailureReason.MISSING_TARGET_REF,
707 old=pull_request, new=None, changes=None,
707 old=pull_request, new=None, changes=None,
708 source_changed=False, target_changed=False)
708 source_changed=False, target_changed=False)
709 target_changed = target_ref_id != target_commit.raw_id
709 target_changed = target_ref_id != target_commit.raw_id
710
710
711 if not (source_changed or target_changed):
711 if not (source_changed or target_changed):
712 log.debug("Nothing changed in pull request %s", pull_request)
712 log.debug("Nothing changed in pull request %s", pull_request)
713 return UpdateResponse(
713 return UpdateResponse(
714 executed=False,
714 executed=False,
715 reason=UpdateFailureReason.NO_CHANGE,
715 reason=UpdateFailureReason.NO_CHANGE,
716 old=pull_request, new=None, changes=None,
716 old=pull_request, new=None, changes=None,
717 source_changed=target_changed, target_changed=source_changed)
717 source_changed=target_changed, target_changed=source_changed)
718
718
719 change_in_found = 'target repo' if target_changed else 'source repo'
719 change_in_found = 'target repo' if target_changed else 'source repo'
720 log.debug('Updating pull request because of change in %s detected',
720 log.debug('Updating pull request because of change in %s detected',
721 change_in_found)
721 change_in_found)
722
722
723 # Finally there is a need for an update, in case of source change
723 # Finally there is a need for an update, in case of source change
724 # we create a new version, else just an update
724 # we create a new version, else just an update
725 if source_changed:
725 if source_changed:
726 pull_request_version = self._create_version_from_snapshot(pull_request)
726 pull_request_version = self._create_version_from_snapshot(pull_request)
727 self._link_comments_to_version(pull_request_version)
727 self._link_comments_to_version(pull_request_version)
728 else:
728 else:
729 try:
729 try:
730 ver = pull_request.versions[-1]
730 ver = pull_request.versions[-1]
731 except IndexError:
731 except IndexError:
732 ver = None
732 ver = None
733
733
734 pull_request.pull_request_version_id = \
734 pull_request.pull_request_version_id = \
735 ver.pull_request_version_id if ver else None
735 ver.pull_request_version_id if ver else None
736 pull_request_version = pull_request
736 pull_request_version = pull_request
737
737
738 try:
738 try:
739 if target_ref_type in self.REF_TYPES:
739 if target_ref_type in self.REF_TYPES:
740 target_commit = target_repo.get_commit(target_ref_name)
740 target_commit = target_repo.get_commit(target_ref_name)
741 else:
741 else:
742 target_commit = target_repo.get_commit(target_ref_id)
742 target_commit = target_repo.get_commit(target_ref_id)
743 except CommitDoesNotExistError:
743 except CommitDoesNotExistError:
744 return UpdateResponse(
744 return UpdateResponse(
745 executed=False,
745 executed=False,
746 reason=UpdateFailureReason.MISSING_TARGET_REF,
746 reason=UpdateFailureReason.MISSING_TARGET_REF,
747 old=pull_request, new=None, changes=None,
747 old=pull_request, new=None, changes=None,
748 source_changed=source_changed, target_changed=target_changed)
748 source_changed=source_changed, target_changed=target_changed)
749
749
750 # re-compute commit ids
750 # re-compute commit ids
751 old_commit_ids = pull_request.revisions
751 old_commit_ids = pull_request.revisions
752 pre_load = ["author", "date", "message", "branch"]
752 pre_load = ["author", "date", "message", "branch"]
753 commit_ranges = target_repo.compare(
753 commit_ranges = target_repo.compare(
754 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
754 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
755 pre_load=pre_load)
755 pre_load=pre_load)
756
756
757 ancestor = source_repo.get_common_ancestor(
757 ancestor = source_repo.get_common_ancestor(
758 source_commit.raw_id, target_commit.raw_id, target_repo)
758 source_commit.raw_id, target_commit.raw_id, target_repo)
759
759
760 pull_request.source_ref = '%s:%s:%s' % (
760 pull_request.source_ref = '%s:%s:%s' % (
761 source_ref_type, source_ref_name, source_commit.raw_id)
761 source_ref_type, source_ref_name, source_commit.raw_id)
762 pull_request.target_ref = '%s:%s:%s' % (
762 pull_request.target_ref = '%s:%s:%s' % (
763 target_ref_type, target_ref_name, ancestor)
763 target_ref_type, target_ref_name, ancestor)
764
764
765 pull_request.revisions = [
765 pull_request.revisions = [
766 commit.raw_id for commit in reversed(commit_ranges)]
766 commit.raw_id for commit in reversed(commit_ranges)]
767 pull_request.updated_on = datetime.datetime.now()
767 pull_request.updated_on = datetime.datetime.now()
768 Session().add(pull_request)
768 Session().add(pull_request)
769 new_commit_ids = pull_request.revisions
769 new_commit_ids = pull_request.revisions
770
770
771 old_diff_data, new_diff_data = self._generate_update_diffs(
771 old_diff_data, new_diff_data = self._generate_update_diffs(
772 pull_request, pull_request_version)
772 pull_request, pull_request_version)
773
773
774 # calculate commit and file changes
774 # calculate commit and file changes
775 changes = self._calculate_commit_id_changes(
775 changes = self._calculate_commit_id_changes(
776 old_commit_ids, new_commit_ids)
776 old_commit_ids, new_commit_ids)
777 file_changes = self._calculate_file_changes(
777 file_changes = self._calculate_file_changes(
778 old_diff_data, new_diff_data)
778 old_diff_data, new_diff_data)
779
779
780 # set comments as outdated if DIFFS changed
780 # set comments as outdated if DIFFS changed
781 CommentsModel().outdate_comments(
781 CommentsModel().outdate_comments(
782 pull_request, old_diff_data=old_diff_data,
782 pull_request, old_diff_data=old_diff_data,
783 new_diff_data=new_diff_data)
783 new_diff_data=new_diff_data)
784
784
785 commit_changes = (changes.added or changes.removed)
785 commit_changes = (changes.added or changes.removed)
786 file_node_changes = (
786 file_node_changes = (
787 file_changes.added or file_changes.modified or file_changes.removed)
787 file_changes.added or file_changes.modified or file_changes.removed)
788 pr_has_changes = commit_changes or file_node_changes
788 pr_has_changes = commit_changes or file_node_changes
789
789
790 # Add an automatic comment to the pull request, in case
790 # Add an automatic comment to the pull request, in case
791 # anything has changed
791 # anything has changed
792 if pr_has_changes:
792 if pr_has_changes:
793 update_comment = CommentsModel().create(
793 update_comment = CommentsModel().create(
794 text=self._render_update_message(changes, file_changes),
794 text=self._render_update_message(changes, file_changes),
795 repo=pull_request.target_repo,
795 repo=pull_request.target_repo,
796 user=pull_request.author,
796 user=pull_request.author,
797 pull_request=pull_request,
797 pull_request=pull_request,
798 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
798 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
799
799
800 # Update status to "Under Review" for added commits
800 # Update status to "Under Review" for added commits
801 for commit_id in changes.added:
801 for commit_id in changes.added:
802 ChangesetStatusModel().set_status(
802 ChangesetStatusModel().set_status(
803 repo=pull_request.source_repo,
803 repo=pull_request.source_repo,
804 status=ChangesetStatus.STATUS_UNDER_REVIEW,
804 status=ChangesetStatus.STATUS_UNDER_REVIEW,
805 comment=update_comment,
805 comment=update_comment,
806 user=pull_request.author,
806 user=pull_request.author,
807 pull_request=pull_request,
807 pull_request=pull_request,
808 revision=commit_id)
808 revision=commit_id)
809
809
810 log.debug(
810 log.debug(
811 'Updated pull request %s, added_ids: %s, common_ids: %s, '
811 'Updated pull request %s, added_ids: %s, common_ids: %s, '
812 'removed_ids: %s', pull_request.pull_request_id,
812 'removed_ids: %s', pull_request.pull_request_id,
813 changes.added, changes.common, changes.removed)
813 changes.added, changes.common, changes.removed)
814 log.debug(
814 log.debug(
815 'Updated pull request with the following file changes: %s',
815 'Updated pull request with the following file changes: %s',
816 file_changes)
816 file_changes)
817
817
818 log.info(
818 log.info(
819 "Updated pull request %s from commit %s to commit %s, "
819 "Updated pull request %s from commit %s to commit %s, "
820 "stored new version %s of this pull request.",
820 "stored new version %s of this pull request.",
821 pull_request.pull_request_id, source_ref_id,
821 pull_request.pull_request_id, source_ref_id,
822 pull_request.source_ref_parts.commit_id,
822 pull_request.source_ref_parts.commit_id,
823 pull_request_version.pull_request_version_id)
823 pull_request_version.pull_request_version_id)
824 Session().commit()
824 Session().commit()
825 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
825 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
826
826
827 return UpdateResponse(
827 return UpdateResponse(
828 executed=True, reason=UpdateFailureReason.NONE,
828 executed=True, reason=UpdateFailureReason.NONE,
829 old=pull_request, new=pull_request_version, changes=changes,
829 old=pull_request, new=pull_request_version, changes=changes,
830 source_changed=source_changed, target_changed=target_changed)
830 source_changed=source_changed, target_changed=target_changed)
831
831
832 def _create_version_from_snapshot(self, pull_request):
832 def _create_version_from_snapshot(self, pull_request):
833 version = PullRequestVersion()
833 version = PullRequestVersion()
834 version.title = pull_request.title
834 version.title = pull_request.title
835 version.description = pull_request.description
835 version.description = pull_request.description
836 version.status = pull_request.status
836 version.status = pull_request.status
837 version.pull_request_state = pull_request.pull_request_state
837 version.pull_request_state = pull_request.pull_request_state
838 version.created_on = datetime.datetime.now()
838 version.created_on = datetime.datetime.now()
839 version.updated_on = pull_request.updated_on
839 version.updated_on = pull_request.updated_on
840 version.user_id = pull_request.user_id
840 version.user_id = pull_request.user_id
841 version.source_repo = pull_request.source_repo
841 version.source_repo = pull_request.source_repo
842 version.source_ref = pull_request.source_ref
842 version.source_ref = pull_request.source_ref
843 version.target_repo = pull_request.target_repo
843 version.target_repo = pull_request.target_repo
844 version.target_ref = pull_request.target_ref
844 version.target_ref = pull_request.target_ref
845
845
846 version._last_merge_source_rev = pull_request._last_merge_source_rev
846 version._last_merge_source_rev = pull_request._last_merge_source_rev
847 version._last_merge_target_rev = pull_request._last_merge_target_rev
847 version._last_merge_target_rev = pull_request._last_merge_target_rev
848 version.last_merge_status = pull_request.last_merge_status
848 version.last_merge_status = pull_request.last_merge_status
849 version.shadow_merge_ref = pull_request.shadow_merge_ref
849 version.shadow_merge_ref = pull_request.shadow_merge_ref
850 version.merge_rev = pull_request.merge_rev
850 version.merge_rev = pull_request.merge_rev
851 version.reviewer_data = pull_request.reviewer_data
851 version.reviewer_data = pull_request.reviewer_data
852
852
853 version.revisions = pull_request.revisions
853 version.revisions = pull_request.revisions
854 version.pull_request = pull_request
854 version.pull_request = pull_request
855 Session().add(version)
855 Session().add(version)
856 Session().flush()
856 Session().flush()
857
857
858 return version
858 return version
859
859
860 def _generate_update_diffs(self, pull_request, pull_request_version):
860 def _generate_update_diffs(self, pull_request, pull_request_version):
861
861
862 diff_context = (
862 diff_context = (
863 self.DIFF_CONTEXT +
863 self.DIFF_CONTEXT +
864 CommentsModel.needed_extra_diff_context())
864 CommentsModel.needed_extra_diff_context())
865 hide_whitespace_changes = False
865 hide_whitespace_changes = False
866 source_repo = pull_request_version.source_repo
866 source_repo = pull_request_version.source_repo
867 source_ref_id = pull_request_version.source_ref_parts.commit_id
867 source_ref_id = pull_request_version.source_ref_parts.commit_id
868 target_ref_id = pull_request_version.target_ref_parts.commit_id
868 target_ref_id = pull_request_version.target_ref_parts.commit_id
869 old_diff = self._get_diff_from_pr_or_version(
869 old_diff = self._get_diff_from_pr_or_version(
870 source_repo, source_ref_id, target_ref_id,
870 source_repo, source_ref_id, target_ref_id,
871 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
871 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
872
872
873 source_repo = pull_request.source_repo
873 source_repo = pull_request.source_repo
874 source_ref_id = pull_request.source_ref_parts.commit_id
874 source_ref_id = pull_request.source_ref_parts.commit_id
875 target_ref_id = pull_request.target_ref_parts.commit_id
875 target_ref_id = pull_request.target_ref_parts.commit_id
876
876
877 new_diff = self._get_diff_from_pr_or_version(
877 new_diff = self._get_diff_from_pr_or_version(
878 source_repo, source_ref_id, target_ref_id,
878 source_repo, source_ref_id, target_ref_id,
879 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
879 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
880
880
881 old_diff_data = diffs.DiffProcessor(old_diff)
881 old_diff_data = diffs.DiffProcessor(old_diff)
882 old_diff_data.prepare()
882 old_diff_data.prepare()
883 new_diff_data = diffs.DiffProcessor(new_diff)
883 new_diff_data = diffs.DiffProcessor(new_diff)
884 new_diff_data.prepare()
884 new_diff_data.prepare()
885
885
886 return old_diff_data, new_diff_data
886 return old_diff_data, new_diff_data
887
887
888 def _link_comments_to_version(self, pull_request_version):
888 def _link_comments_to_version(self, pull_request_version):
889 """
889 """
890 Link all unlinked comments of this pull request to the given version.
890 Link all unlinked comments of this pull request to the given version.
891
891
892 :param pull_request_version: The `PullRequestVersion` to which
892 :param pull_request_version: The `PullRequestVersion` to which
893 the comments shall be linked.
893 the comments shall be linked.
894
894
895 """
895 """
896 pull_request = pull_request_version.pull_request
896 pull_request = pull_request_version.pull_request
897 comments = ChangesetComment.query()\
897 comments = ChangesetComment.query()\
898 .filter(
898 .filter(
899 # TODO: johbo: Should we query for the repo at all here?
899 # TODO: johbo: Should we query for the repo at all here?
900 # Pending decision on how comments of PRs are to be related
900 # Pending decision on how comments of PRs are to be related
901 # to either the source repo, the target repo or no repo at all.
901 # to either the source repo, the target repo or no repo at all.
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
903 ChangesetComment.pull_request == pull_request,
903 ChangesetComment.pull_request == pull_request,
904 ChangesetComment.pull_request_version == None)\
904 ChangesetComment.pull_request_version == None)\
905 .order_by(ChangesetComment.comment_id.asc())
905 .order_by(ChangesetComment.comment_id.asc())
906
906
907 # TODO: johbo: Find out why this breaks if it is done in a bulk
907 # TODO: johbo: Find out why this breaks if it is done in a bulk
908 # operation.
908 # operation.
909 for comment in comments:
909 for comment in comments:
910 comment.pull_request_version_id = (
910 comment.pull_request_version_id = (
911 pull_request_version.pull_request_version_id)
911 pull_request_version.pull_request_version_id)
912 Session().add(comment)
912 Session().add(comment)
913
913
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
915 added = [x for x in new_ids if x not in old_ids]
915 added = [x for x in new_ids if x not in old_ids]
916 common = [x for x in new_ids if x in old_ids]
916 common = [x for x in new_ids if x in old_ids]
917 removed = [x for x in old_ids if x not in new_ids]
917 removed = [x for x in old_ids if x not in new_ids]
918 total = new_ids
918 total = new_ids
919 return ChangeTuple(added, common, removed, total)
919 return ChangeTuple(added, common, removed, total)
920
920
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
922
922
923 old_files = OrderedDict()
923 old_files = OrderedDict()
924 for diff_data in old_diff_data.parsed_diff:
924 for diff_data in old_diff_data.parsed_diff:
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
926
926
927 added_files = []
927 added_files = []
928 modified_files = []
928 modified_files = []
929 removed_files = []
929 removed_files = []
930 for diff_data in new_diff_data.parsed_diff:
930 for diff_data in new_diff_data.parsed_diff:
931 new_filename = diff_data['filename']
931 new_filename = diff_data['filename']
932 new_hash = md5_safe(diff_data['raw_diff'])
932 new_hash = md5_safe(diff_data['raw_diff'])
933
933
934 old_hash = old_files.get(new_filename)
934 old_hash = old_files.get(new_filename)
935 if not old_hash:
935 if not old_hash:
936 # file is not present in old diff, means it's added
936 # file is not present in old diff, means it's added
937 added_files.append(new_filename)
937 added_files.append(new_filename)
938 else:
938 else:
939 if new_hash != old_hash:
939 if new_hash != old_hash:
940 modified_files.append(new_filename)
940 modified_files.append(new_filename)
941 # now remove a file from old, since we have seen it already
941 # now remove a file from old, since we have seen it already
942 del old_files[new_filename]
942 del old_files[new_filename]
943
943
944 # removed files is when there are present in old, but not in NEW,
944 # removed files is when there are present in old, but not in NEW,
945 # since we remove old files that are present in new diff, left-overs
945 # since we remove old files that are present in new diff, left-overs
946 # if any should be the removed files
946 # if any should be the removed files
947 removed_files.extend(old_files.keys())
947 removed_files.extend(old_files.keys())
948
948
949 return FileChangeTuple(added_files, modified_files, removed_files)
949 return FileChangeTuple(added_files, modified_files, removed_files)
950
950
951 def _render_update_message(self, changes, file_changes):
951 def _render_update_message(self, changes, file_changes):
952 """
952 """
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
954 so it's always looking the same disregarding on which default
954 so it's always looking the same disregarding on which default
955 renderer system is using.
955 renderer system is using.
956
956
957 :param changes: changes named tuple
957 :param changes: changes named tuple
958 :param file_changes: file changes named tuple
958 :param file_changes: file changes named tuple
959
959
960 """
960 """
961 new_status = ChangesetStatus.get_status_lbl(
961 new_status = ChangesetStatus.get_status_lbl(
962 ChangesetStatus.STATUS_UNDER_REVIEW)
962 ChangesetStatus.STATUS_UNDER_REVIEW)
963
963
964 changed_files = (
964 changed_files = (
965 file_changes.added + file_changes.modified + file_changes.removed)
965 file_changes.added + file_changes.modified + file_changes.removed)
966
966
967 params = {
967 params = {
968 'under_review_label': new_status,
968 'under_review_label': new_status,
969 'added_commits': changes.added,
969 'added_commits': changes.added,
970 'removed_commits': changes.removed,
970 'removed_commits': changes.removed,
971 'changed_files': changed_files,
971 'changed_files': changed_files,
972 'added_files': file_changes.added,
972 'added_files': file_changes.added,
973 'modified_files': file_changes.modified,
973 'modified_files': file_changes.modified,
974 'removed_files': file_changes.removed,
974 'removed_files': file_changes.removed,
975 }
975 }
976 renderer = RstTemplateRenderer()
976 renderer = RstTemplateRenderer()
977 return renderer.render('pull_request_update.mako', **params)
977 return renderer.render('pull_request_update.mako', **params)
978
978
979 def edit(self, pull_request, title, description, description_renderer, user):
979 def edit(self, pull_request, title, description, description_renderer, user):
980 pull_request = self.__get_pull_request(pull_request)
980 pull_request = self.__get_pull_request(pull_request)
981 old_data = pull_request.get_api_data(with_merge_state=False)
981 old_data = pull_request.get_api_data(with_merge_state=False)
982 if pull_request.is_closed():
982 if pull_request.is_closed():
983 raise ValueError('This pull request is closed')
983 raise ValueError('This pull request is closed')
984 if title:
984 if title:
985 pull_request.title = title
985 pull_request.title = title
986 pull_request.description = description
986 pull_request.description = description
987 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
988 pull_request.description_renderer = description_renderer
988 pull_request.description_renderer = description_renderer
989 Session().add(pull_request)
989 Session().add(pull_request)
990 self._log_audit_action(
990 self._log_audit_action(
991 'repo.pull_request.edit', {'old_data': old_data},
991 'repo.pull_request.edit', {'old_data': old_data},
992 user, pull_request)
992 user, pull_request)
993
993
994 def update_reviewers(self, pull_request, reviewer_data, user):
994 def update_reviewers(self, pull_request, reviewer_data, user):
995 """
995 """
996 Update the reviewers in the pull request
996 Update the reviewers in the pull request
997
997
998 :param pull_request: the pr to update
998 :param pull_request: the pr to update
999 :param reviewer_data: list of tuples
999 :param reviewer_data: list of tuples
1000 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1000 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1001 """
1001 """
1002 pull_request = self.__get_pull_request(pull_request)
1002 pull_request = self.__get_pull_request(pull_request)
1003 if pull_request.is_closed():
1003 if pull_request.is_closed():
1004 raise ValueError('This pull request is closed')
1004 raise ValueError('This pull request is closed')
1005
1005
1006 reviewers = {}
1006 reviewers = {}
1007 for user_id, reasons, mandatory, rules in reviewer_data:
1007 for user_id, reasons, mandatory, rules in reviewer_data:
1008 if isinstance(user_id, (int, compat.string_types)):
1008 if isinstance(user_id, (int, compat.string_types)):
1009 user_id = self._get_user(user_id).user_id
1009 user_id = self._get_user(user_id).user_id
1010 reviewers[user_id] = {
1010 reviewers[user_id] = {
1011 'reasons': reasons, 'mandatory': mandatory}
1011 'reasons': reasons, 'mandatory': mandatory}
1012
1012
1013 reviewers_ids = set(reviewers.keys())
1013 reviewers_ids = set(reviewers.keys())
1014 current_reviewers = PullRequestReviewers.query()\
1014 current_reviewers = PullRequestReviewers.query()\
1015 .filter(PullRequestReviewers.pull_request ==
1015 .filter(PullRequestReviewers.pull_request ==
1016 pull_request).all()
1016 pull_request).all()
1017 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1017 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1018
1018
1019 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1019 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1020 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1020 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1021
1021
1022 log.debug("Adding %s reviewers", ids_to_add)
1022 log.debug("Adding %s reviewers", ids_to_add)
1023 log.debug("Removing %s reviewers", ids_to_remove)
1023 log.debug("Removing %s reviewers", ids_to_remove)
1024 changed = False
1024 changed = False
1025 added_audit_reviewers = []
1025 added_audit_reviewers = []
1026 removed_audit_reviewers = []
1026 removed_audit_reviewers = []
1027
1027
1028 for uid in ids_to_add:
1028 for uid in ids_to_add:
1029 changed = True
1029 changed = True
1030 _usr = self._get_user(uid)
1030 _usr = self._get_user(uid)
1031 reviewer = PullRequestReviewers()
1031 reviewer = PullRequestReviewers()
1032 reviewer.user = _usr
1032 reviewer.user = _usr
1033 reviewer.pull_request = pull_request
1033 reviewer.pull_request = pull_request
1034 reviewer.reasons = reviewers[uid]['reasons']
1034 reviewer.reasons = reviewers[uid]['reasons']
1035 # NOTE(marcink): mandatory shouldn't be changed now
1035 # NOTE(marcink): mandatory shouldn't be changed now
1036 # reviewer.mandatory = reviewers[uid]['reasons']
1036 # reviewer.mandatory = reviewers[uid]['reasons']
1037 Session().add(reviewer)
1037 Session().add(reviewer)
1038 added_audit_reviewers.append(reviewer.get_dict())
1038 added_audit_reviewers.append(reviewer.get_dict())
1039
1039
1040 for uid in ids_to_remove:
1040 for uid in ids_to_remove:
1041 changed = True
1041 changed = True
1042 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1042 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1043 # that prevents and fixes cases that we added the same reviewer twice.
1043 # that prevents and fixes cases that we added the same reviewer twice.
1044 # this CAN happen due to the lack of DB checks
1044 # this CAN happen due to the lack of DB checks
1045 reviewers = PullRequestReviewers.query()\
1045 reviewers = PullRequestReviewers.query()\
1046 .filter(PullRequestReviewers.user_id == uid,
1046 .filter(PullRequestReviewers.user_id == uid,
1047 PullRequestReviewers.pull_request == pull_request)\
1047 PullRequestReviewers.pull_request == pull_request)\
1048 .all()
1048 .all()
1049
1049
1050 for obj in reviewers:
1050 for obj in reviewers:
1051 added_audit_reviewers.append(obj.get_dict())
1051 added_audit_reviewers.append(obj.get_dict())
1052 Session().delete(obj)
1052 Session().delete(obj)
1053
1053
1054 if changed:
1054 if changed:
1055 Session().expire_all()
1055 Session().expire_all()
1056 pull_request.updated_on = datetime.datetime.now()
1056 pull_request.updated_on = datetime.datetime.now()
1057 Session().add(pull_request)
1057 Session().add(pull_request)
1058
1058
1059 # finally store audit logs
1059 # finally store audit logs
1060 for user_data in added_audit_reviewers:
1060 for user_data in added_audit_reviewers:
1061 self._log_audit_action(
1061 self._log_audit_action(
1062 'repo.pull_request.reviewer.add', {'data': user_data},
1062 'repo.pull_request.reviewer.add', {'data': user_data},
1063 user, pull_request)
1063 user, pull_request)
1064 for user_data in removed_audit_reviewers:
1064 for user_data in removed_audit_reviewers:
1065 self._log_audit_action(
1065 self._log_audit_action(
1066 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1066 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1067 user, pull_request)
1067 user, pull_request)
1068
1068
1069 self.notify_reviewers(pull_request, ids_to_add)
1069 self.notify_reviewers(pull_request, ids_to_add)
1070 return ids_to_add, ids_to_remove
1070 return ids_to_add, ids_to_remove
1071
1071
1072 def get_url(self, pull_request, request=None, permalink=False):
1072 def get_url(self, pull_request, request=None, permalink=False):
1073 if not request:
1073 if not request:
1074 request = get_current_request()
1074 request = get_current_request()
1075
1075
1076 if permalink:
1076 if permalink:
1077 return request.route_url(
1077 return request.route_url(
1078 'pull_requests_global',
1078 'pull_requests_global',
1079 pull_request_id=pull_request.pull_request_id,)
1079 pull_request_id=pull_request.pull_request_id,)
1080 else:
1080 else:
1081 return request.route_url('pullrequest_show',
1081 return request.route_url('pullrequest_show',
1082 repo_name=safe_str(pull_request.target_repo.repo_name),
1082 repo_name=safe_str(pull_request.target_repo.repo_name),
1083 pull_request_id=pull_request.pull_request_id,)
1083 pull_request_id=pull_request.pull_request_id,)
1084
1084
1085 def get_shadow_clone_url(self, pull_request, request=None):
1085 def get_shadow_clone_url(self, pull_request, request=None):
1086 """
1086 """
1087 Returns qualified url pointing to the shadow repository. If this pull
1087 Returns qualified url pointing to the shadow repository. If this pull
1088 request is closed there is no shadow repository and ``None`` will be
1088 request is closed there is no shadow repository and ``None`` will be
1089 returned.
1089 returned.
1090 """
1090 """
1091 if pull_request.is_closed():
1091 if pull_request.is_closed():
1092 return None
1092 return None
1093 else:
1093 else:
1094 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1094 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1095 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1095 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1096
1096
1097 def notify_reviewers(self, pull_request, reviewers_ids):
1097 def notify_reviewers(self, pull_request, reviewers_ids):
1098 # notification to reviewers
1098 # notification to reviewers
1099 if not reviewers_ids:
1099 if not reviewers_ids:
1100 return
1100 return
1101
1101
1102 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1103
1102 pull_request_obj = pull_request
1104 pull_request_obj = pull_request
1103 # get the current participants of this pull request
1105 # get the current participants of this pull request
1104 recipients = reviewers_ids
1106 recipients = reviewers_ids
1105 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1107 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1106
1108
1107 pr_source_repo = pull_request_obj.source_repo
1109 pr_source_repo = pull_request_obj.source_repo
1108 pr_target_repo = pull_request_obj.target_repo
1110 pr_target_repo = pull_request_obj.target_repo
1109
1111
1110 pr_url = h.route_url('pullrequest_show',
1112 pr_url = h.route_url('pullrequest_show',
1111 repo_name=pr_target_repo.repo_name,
1113 repo_name=pr_target_repo.repo_name,
1112 pull_request_id=pull_request_obj.pull_request_id,)
1114 pull_request_id=pull_request_obj.pull_request_id,)
1113
1115
1114 # set some variables for email notification
1116 # set some variables for email notification
1115 pr_target_repo_url = h.route_url(
1117 pr_target_repo_url = h.route_url(
1116 'repo_summary', repo_name=pr_target_repo.repo_name)
1118 'repo_summary', repo_name=pr_target_repo.repo_name)
1117
1119
1118 pr_source_repo_url = h.route_url(
1120 pr_source_repo_url = h.route_url(
1119 'repo_summary', repo_name=pr_source_repo.repo_name)
1121 'repo_summary', repo_name=pr_source_repo.repo_name)
1120
1122
1121 # pull request specifics
1123 # pull request specifics
1122 pull_request_commits = [
1124 pull_request_commits = [
1123 (x.raw_id, x.message)
1125 (x.raw_id, x.message)
1124 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1126 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1125
1127
1126 kwargs = {
1128 kwargs = {
1127 'user': pull_request.author,
1129 'user': pull_request.author,
1128 'pull_request': pull_request_obj,
1130 'pull_request': pull_request_obj,
1129 'pull_request_commits': pull_request_commits,
1131 'pull_request_commits': pull_request_commits,
1130
1132
1131 'pull_request_target_repo': pr_target_repo,
1133 'pull_request_target_repo': pr_target_repo,
1132 'pull_request_target_repo_url': pr_target_repo_url,
1134 'pull_request_target_repo_url': pr_target_repo_url,
1133
1135
1134 'pull_request_source_repo': pr_source_repo,
1136 'pull_request_source_repo': pr_source_repo,
1135 'pull_request_source_repo_url': pr_source_repo_url,
1137 'pull_request_source_repo_url': pr_source_repo_url,
1136
1138
1137 'pull_request_url': pr_url,
1139 'pull_request_url': pr_url,
1138 }
1140 }
1139
1141
1140 # pre-generate the subject for notification itself
1142 # pre-generate the subject for notification itself
1141 (subject,
1143 (subject,
1142 _h, _e, # we don't care about those
1144 _h, _e, # we don't care about those
1143 body_plaintext) = EmailNotificationModel().render_email(
1145 body_plaintext) = EmailNotificationModel().render_email(
1144 notification_type, **kwargs)
1146 notification_type, **kwargs)
1145
1147
1146 # create notification objects, and emails
1148 # create notification objects, and emails
1147 NotificationModel().create(
1149 NotificationModel().create(
1148 created_by=pull_request.author,
1150 created_by=pull_request.author,
1149 notification_subject=subject,
1151 notification_subject=subject,
1150 notification_body=body_plaintext,
1152 notification_body=body_plaintext,
1151 notification_type=notification_type,
1153 notification_type=notification_type,
1152 recipients=recipients,
1154 recipients=recipients,
1153 email_kwargs=kwargs,
1155 email_kwargs=kwargs,
1154 )
1156 )
1155
1157
1156 def delete(self, pull_request, user):
1158 def delete(self, pull_request, user):
1157 pull_request = self.__get_pull_request(pull_request)
1159 pull_request = self.__get_pull_request(pull_request)
1158 old_data = pull_request.get_api_data(with_merge_state=False)
1160 old_data = pull_request.get_api_data(with_merge_state=False)
1159 self._cleanup_merge_workspace(pull_request)
1161 self._cleanup_merge_workspace(pull_request)
1160 self._log_audit_action(
1162 self._log_audit_action(
1161 'repo.pull_request.delete', {'old_data': old_data},
1163 'repo.pull_request.delete', {'old_data': old_data},
1162 user, pull_request)
1164 user, pull_request)
1163 Session().delete(pull_request)
1165 Session().delete(pull_request)
1164
1166
1165 def close_pull_request(self, pull_request, user):
1167 def close_pull_request(self, pull_request, user):
1166 pull_request = self.__get_pull_request(pull_request)
1168 pull_request = self.__get_pull_request(pull_request)
1167 self._cleanup_merge_workspace(pull_request)
1169 self._cleanup_merge_workspace(pull_request)
1168 pull_request.status = PullRequest.STATUS_CLOSED
1170 pull_request.status = PullRequest.STATUS_CLOSED
1169 pull_request.updated_on = datetime.datetime.now()
1171 pull_request.updated_on = datetime.datetime.now()
1170 Session().add(pull_request)
1172 Session().add(pull_request)
1171 self.trigger_pull_request_hook(
1173 self.trigger_pull_request_hook(
1172 pull_request, pull_request.author, 'close')
1174 pull_request, pull_request.author, 'close')
1173
1175
1174 pr_data = pull_request.get_api_data(with_merge_state=False)
1176 pr_data = pull_request.get_api_data(with_merge_state=False)
1175 self._log_audit_action(
1177 self._log_audit_action(
1176 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1178 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1177
1179
1178 def close_pull_request_with_comment(
1180 def close_pull_request_with_comment(
1179 self, pull_request, user, repo, message=None, auth_user=None):
1181 self, pull_request, user, repo, message=None, auth_user=None):
1180
1182
1181 pull_request_review_status = pull_request.calculated_review_status()
1183 pull_request_review_status = pull_request.calculated_review_status()
1182
1184
1183 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1185 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1184 # approved only if we have voting consent
1186 # approved only if we have voting consent
1185 status = ChangesetStatus.STATUS_APPROVED
1187 status = ChangesetStatus.STATUS_APPROVED
1186 else:
1188 else:
1187 status = ChangesetStatus.STATUS_REJECTED
1189 status = ChangesetStatus.STATUS_REJECTED
1188 status_lbl = ChangesetStatus.get_status_lbl(status)
1190 status_lbl = ChangesetStatus.get_status_lbl(status)
1189
1191
1190 default_message = (
1192 default_message = (
1191 'Closing with status change {transition_icon} {status}.'
1193 'Closing with status change {transition_icon} {status}.'
1192 ).format(transition_icon='>', status=status_lbl)
1194 ).format(transition_icon='>', status=status_lbl)
1193 text = message or default_message
1195 text = message or default_message
1194
1196
1195 # create a comment, and link it to new status
1197 # create a comment, and link it to new status
1196 comment = CommentsModel().create(
1198 comment = CommentsModel().create(
1197 text=text,
1199 text=text,
1198 repo=repo.repo_id,
1200 repo=repo.repo_id,
1199 user=user.user_id,
1201 user=user.user_id,
1200 pull_request=pull_request.pull_request_id,
1202 pull_request=pull_request.pull_request_id,
1201 status_change=status_lbl,
1203 status_change=status_lbl,
1202 status_change_type=status,
1204 status_change_type=status,
1203 closing_pr=True,
1205 closing_pr=True,
1204 auth_user=auth_user,
1206 auth_user=auth_user,
1205 )
1207 )
1206
1208
1207 # calculate old status before we change it
1209 # calculate old status before we change it
1208 old_calculated_status = pull_request.calculated_review_status()
1210 old_calculated_status = pull_request.calculated_review_status()
1209 ChangesetStatusModel().set_status(
1211 ChangesetStatusModel().set_status(
1210 repo.repo_id,
1212 repo.repo_id,
1211 status,
1213 status,
1212 user.user_id,
1214 user.user_id,
1213 comment=comment,
1215 comment=comment,
1214 pull_request=pull_request.pull_request_id
1216 pull_request=pull_request.pull_request_id
1215 )
1217 )
1216
1218
1217 Session().flush()
1219 Session().flush()
1218 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1220 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1219 # we now calculate the status of pull request again, and based on that
1221 # we now calculate the status of pull request again, and based on that
1220 # calculation trigger status change. This might happen in cases
1222 # calculation trigger status change. This might happen in cases
1221 # that non-reviewer admin closes a pr, which means his vote doesn't
1223 # that non-reviewer admin closes a pr, which means his vote doesn't
1222 # change the status, while if he's a reviewer this might change it.
1224 # change the status, while if he's a reviewer this might change it.
1223 calculated_status = pull_request.calculated_review_status()
1225 calculated_status = pull_request.calculated_review_status()
1224 if old_calculated_status != calculated_status:
1226 if old_calculated_status != calculated_status:
1225 self.trigger_pull_request_hook(
1227 self.trigger_pull_request_hook(
1226 pull_request, user, 'review_status_change',
1228 pull_request, user, 'review_status_change',
1227 data={'status': calculated_status})
1229 data={'status': calculated_status})
1228
1230
1229 # finally close the PR
1231 # finally close the PR
1230 PullRequestModel().close_pull_request(
1232 PullRequestModel().close_pull_request(
1231 pull_request.pull_request_id, user)
1233 pull_request.pull_request_id, user)
1232
1234
1233 return comment, status
1235 return comment, status
1234
1236
1235 def merge_status(self, pull_request, translator=None,
1237 def merge_status(self, pull_request, translator=None,
1236 force_shadow_repo_refresh=False):
1238 force_shadow_repo_refresh=False):
1237 _ = translator or get_current_request().translate
1239 _ = translator or get_current_request().translate
1238
1240
1239 if not self._is_merge_enabled(pull_request):
1241 if not self._is_merge_enabled(pull_request):
1240 return False, _('Server-side pull request merging is disabled.')
1242 return False, _('Server-side pull request merging is disabled.')
1241 if pull_request.is_closed():
1243 if pull_request.is_closed():
1242 return False, _('This pull request is closed.')
1244 return False, _('This pull request is closed.')
1243 merge_possible, msg = self._check_repo_requirements(
1245 merge_possible, msg = self._check_repo_requirements(
1244 target=pull_request.target_repo, source=pull_request.source_repo,
1246 target=pull_request.target_repo, source=pull_request.source_repo,
1245 translator=_)
1247 translator=_)
1246 if not merge_possible:
1248 if not merge_possible:
1247 return merge_possible, msg
1249 return merge_possible, msg
1248
1250
1249 try:
1251 try:
1250 resp = self._try_merge(
1252 resp = self._try_merge(
1251 pull_request,
1253 pull_request,
1252 force_shadow_repo_refresh=force_shadow_repo_refresh)
1254 force_shadow_repo_refresh=force_shadow_repo_refresh)
1253 log.debug("Merge response: %s", resp)
1255 log.debug("Merge response: %s", resp)
1254 status = resp.possible, resp.merge_status_message
1256 status = resp.possible, resp.merge_status_message
1255 except NotImplementedError:
1257 except NotImplementedError:
1256 status = False, _('Pull request merging is not supported.')
1258 status = False, _('Pull request merging is not supported.')
1257
1259
1258 return status
1260 return status
1259
1261
1260 def _check_repo_requirements(self, target, source, translator):
1262 def _check_repo_requirements(self, target, source, translator):
1261 """
1263 """
1262 Check if `target` and `source` have compatible requirements.
1264 Check if `target` and `source` have compatible requirements.
1263
1265
1264 Currently this is just checking for largefiles.
1266 Currently this is just checking for largefiles.
1265 """
1267 """
1266 _ = translator
1268 _ = translator
1267 target_has_largefiles = self._has_largefiles(target)
1269 target_has_largefiles = self._has_largefiles(target)
1268 source_has_largefiles = self._has_largefiles(source)
1270 source_has_largefiles = self._has_largefiles(source)
1269 merge_possible = True
1271 merge_possible = True
1270 message = u''
1272 message = u''
1271
1273
1272 if target_has_largefiles != source_has_largefiles:
1274 if target_has_largefiles != source_has_largefiles:
1273 merge_possible = False
1275 merge_possible = False
1274 if source_has_largefiles:
1276 if source_has_largefiles:
1275 message = _(
1277 message = _(
1276 'Target repository large files support is disabled.')
1278 'Target repository large files support is disabled.')
1277 else:
1279 else:
1278 message = _(
1280 message = _(
1279 'Source repository large files support is disabled.')
1281 'Source repository large files support is disabled.')
1280
1282
1281 return merge_possible, message
1283 return merge_possible, message
1282
1284
1283 def _has_largefiles(self, repo):
1285 def _has_largefiles(self, repo):
1284 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1286 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1285 'extensions', 'largefiles')
1287 'extensions', 'largefiles')
1286 return largefiles_ui and largefiles_ui[0].active
1288 return largefiles_ui and largefiles_ui[0].active
1287
1289
1288 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1290 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1289 """
1291 """
1290 Try to merge the pull request and return the merge status.
1292 Try to merge the pull request and return the merge status.
1291 """
1293 """
1292 log.debug(
1294 log.debug(
1293 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1295 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1294 pull_request.pull_request_id, force_shadow_repo_refresh)
1296 pull_request.pull_request_id, force_shadow_repo_refresh)
1295 target_vcs = pull_request.target_repo.scm_instance()
1297 target_vcs = pull_request.target_repo.scm_instance()
1296 # Refresh the target reference.
1298 # Refresh the target reference.
1297 try:
1299 try:
1298 target_ref = self._refresh_reference(
1300 target_ref = self._refresh_reference(
1299 pull_request.target_ref_parts, target_vcs)
1301 pull_request.target_ref_parts, target_vcs)
1300 except CommitDoesNotExistError:
1302 except CommitDoesNotExistError:
1301 merge_state = MergeResponse(
1303 merge_state = MergeResponse(
1302 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1304 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1303 metadata={'target_ref': pull_request.target_ref_parts})
1305 metadata={'target_ref': pull_request.target_ref_parts})
1304 return merge_state
1306 return merge_state
1305
1307
1306 target_locked = pull_request.target_repo.locked
1308 target_locked = pull_request.target_repo.locked
1307 if target_locked and target_locked[0]:
1309 if target_locked and target_locked[0]:
1308 locked_by = 'user:{}'.format(target_locked[0])
1310 locked_by = 'user:{}'.format(target_locked[0])
1309 log.debug("The target repository is locked by %s.", locked_by)
1311 log.debug("The target repository is locked by %s.", locked_by)
1310 merge_state = MergeResponse(
1312 merge_state = MergeResponse(
1311 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1313 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1312 metadata={'locked_by': locked_by})
1314 metadata={'locked_by': locked_by})
1313 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1315 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1314 pull_request, target_ref):
1316 pull_request, target_ref):
1315 log.debug("Refreshing the merge status of the repository.")
1317 log.debug("Refreshing the merge status of the repository.")
1316 merge_state = self._refresh_merge_state(
1318 merge_state = self._refresh_merge_state(
1317 pull_request, target_vcs, target_ref)
1319 pull_request, target_vcs, target_ref)
1318 else:
1320 else:
1319 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1321 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1320 metadata = {
1322 metadata = {
1321 'target_ref': pull_request.target_ref_parts,
1323 'target_ref': pull_request.target_ref_parts,
1322 'source_ref': pull_request.source_ref_parts,
1324 'source_ref': pull_request.source_ref_parts,
1323 }
1325 }
1324 if not possible and target_ref.type == 'branch':
1326 if not possible and target_ref.type == 'branch':
1325 # NOTE(marcink): case for mercurial multiple heads on branch
1327 # NOTE(marcink): case for mercurial multiple heads on branch
1326 heads = target_vcs._heads(target_ref.name)
1328 heads = target_vcs._heads(target_ref.name)
1327 if len(heads) != 1:
1329 if len(heads) != 1:
1328 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1330 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1329 metadata.update({
1331 metadata.update({
1330 'heads': heads
1332 'heads': heads
1331 })
1333 })
1332 merge_state = MergeResponse(
1334 merge_state = MergeResponse(
1333 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1335 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1334
1336
1335 return merge_state
1337 return merge_state
1336
1338
1337 def _refresh_reference(self, reference, vcs_repository):
1339 def _refresh_reference(self, reference, vcs_repository):
1338 if reference.type in self.UPDATABLE_REF_TYPES:
1340 if reference.type in self.UPDATABLE_REF_TYPES:
1339 name_or_id = reference.name
1341 name_or_id = reference.name
1340 else:
1342 else:
1341 name_or_id = reference.commit_id
1343 name_or_id = reference.commit_id
1342
1344
1343 refreshed_commit = vcs_repository.get_commit(name_or_id)
1345 refreshed_commit = vcs_repository.get_commit(name_or_id)
1344 refreshed_reference = Reference(
1346 refreshed_reference = Reference(
1345 reference.type, reference.name, refreshed_commit.raw_id)
1347 reference.type, reference.name, refreshed_commit.raw_id)
1346 return refreshed_reference
1348 return refreshed_reference
1347
1349
1348 def _needs_merge_state_refresh(self, pull_request, target_reference):
1350 def _needs_merge_state_refresh(self, pull_request, target_reference):
1349 return not(
1351 return not(
1350 pull_request.revisions and
1352 pull_request.revisions and
1351 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1353 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1352 target_reference.commit_id == pull_request._last_merge_target_rev)
1354 target_reference.commit_id == pull_request._last_merge_target_rev)
1353
1355
1354 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1356 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1355 workspace_id = self._workspace_id(pull_request)
1357 workspace_id = self._workspace_id(pull_request)
1356 source_vcs = pull_request.source_repo.scm_instance()
1358 source_vcs = pull_request.source_repo.scm_instance()
1357 repo_id = pull_request.target_repo.repo_id
1359 repo_id = pull_request.target_repo.repo_id
1358 use_rebase = self._use_rebase_for_merging(pull_request)
1360 use_rebase = self._use_rebase_for_merging(pull_request)
1359 close_branch = self._close_branch_before_merging(pull_request)
1361 close_branch = self._close_branch_before_merging(pull_request)
1360 merge_state = target_vcs.merge(
1362 merge_state = target_vcs.merge(
1361 repo_id, workspace_id,
1363 repo_id, workspace_id,
1362 target_reference, source_vcs, pull_request.source_ref_parts,
1364 target_reference, source_vcs, pull_request.source_ref_parts,
1363 dry_run=True, use_rebase=use_rebase,
1365 dry_run=True, use_rebase=use_rebase,
1364 close_branch=close_branch)
1366 close_branch=close_branch)
1365
1367
1366 # Do not store the response if there was an unknown error.
1368 # Do not store the response if there was an unknown error.
1367 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1369 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1368 pull_request._last_merge_source_rev = \
1370 pull_request._last_merge_source_rev = \
1369 pull_request.source_ref_parts.commit_id
1371 pull_request.source_ref_parts.commit_id
1370 pull_request._last_merge_target_rev = target_reference.commit_id
1372 pull_request._last_merge_target_rev = target_reference.commit_id
1371 pull_request.last_merge_status = merge_state.failure_reason
1373 pull_request.last_merge_status = merge_state.failure_reason
1372 pull_request.shadow_merge_ref = merge_state.merge_ref
1374 pull_request.shadow_merge_ref = merge_state.merge_ref
1373 Session().add(pull_request)
1375 Session().add(pull_request)
1374 Session().commit()
1376 Session().commit()
1375
1377
1376 return merge_state
1378 return merge_state
1377
1379
1378 def _workspace_id(self, pull_request):
1380 def _workspace_id(self, pull_request):
1379 workspace_id = 'pr-%s' % pull_request.pull_request_id
1381 workspace_id = 'pr-%s' % pull_request.pull_request_id
1380 return workspace_id
1382 return workspace_id
1381
1383
1382 def generate_repo_data(self, repo, commit_id=None, branch=None,
1384 def generate_repo_data(self, repo, commit_id=None, branch=None,
1383 bookmark=None, translator=None):
1385 bookmark=None, translator=None):
1384 from rhodecode.model.repo import RepoModel
1386 from rhodecode.model.repo import RepoModel
1385
1387
1386 all_refs, selected_ref = \
1388 all_refs, selected_ref = \
1387 self._get_repo_pullrequest_sources(
1389 self._get_repo_pullrequest_sources(
1388 repo.scm_instance(), commit_id=commit_id,
1390 repo.scm_instance(), commit_id=commit_id,
1389 branch=branch, bookmark=bookmark, translator=translator)
1391 branch=branch, bookmark=bookmark, translator=translator)
1390
1392
1391 refs_select2 = []
1393 refs_select2 = []
1392 for element in all_refs:
1394 for element in all_refs:
1393 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1395 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1394 refs_select2.append({'text': element[1], 'children': children})
1396 refs_select2.append({'text': element[1], 'children': children})
1395
1397
1396 return {
1398 return {
1397 'user': {
1399 'user': {
1398 'user_id': repo.user.user_id,
1400 'user_id': repo.user.user_id,
1399 'username': repo.user.username,
1401 'username': repo.user.username,
1400 'firstname': repo.user.first_name,
1402 'firstname': repo.user.first_name,
1401 'lastname': repo.user.last_name,
1403 'lastname': repo.user.last_name,
1402 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1404 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1403 },
1405 },
1404 'name': repo.repo_name,
1406 'name': repo.repo_name,
1405 'link': RepoModel().get_url(repo),
1407 'link': RepoModel().get_url(repo),
1406 'description': h.chop_at_smart(repo.description_safe, '\n'),
1408 'description': h.chop_at_smart(repo.description_safe, '\n'),
1407 'refs': {
1409 'refs': {
1408 'all_refs': all_refs,
1410 'all_refs': all_refs,
1409 'selected_ref': selected_ref,
1411 'selected_ref': selected_ref,
1410 'select2_refs': refs_select2
1412 'select2_refs': refs_select2
1411 }
1413 }
1412 }
1414 }
1413
1415
1414 def generate_pullrequest_title(self, source, source_ref, target):
1416 def generate_pullrequest_title(self, source, source_ref, target):
1415 return u'{source}#{at_ref} to {target}'.format(
1417 return u'{source}#{at_ref} to {target}'.format(
1416 source=source,
1418 source=source,
1417 at_ref=source_ref,
1419 at_ref=source_ref,
1418 target=target,
1420 target=target,
1419 )
1421 )
1420
1422
1421 def _cleanup_merge_workspace(self, pull_request):
1423 def _cleanup_merge_workspace(self, pull_request):
1422 # Merging related cleanup
1424 # Merging related cleanup
1423 repo_id = pull_request.target_repo.repo_id
1425 repo_id = pull_request.target_repo.repo_id
1424 target_scm = pull_request.target_repo.scm_instance()
1426 target_scm = pull_request.target_repo.scm_instance()
1425 workspace_id = self._workspace_id(pull_request)
1427 workspace_id = self._workspace_id(pull_request)
1426
1428
1427 try:
1429 try:
1428 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1430 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1429 except NotImplementedError:
1431 except NotImplementedError:
1430 pass
1432 pass
1431
1433
1432 def _get_repo_pullrequest_sources(
1434 def _get_repo_pullrequest_sources(
1433 self, repo, commit_id=None, branch=None, bookmark=None,
1435 self, repo, commit_id=None, branch=None, bookmark=None,
1434 translator=None):
1436 translator=None):
1435 """
1437 """
1436 Return a structure with repo's interesting commits, suitable for
1438 Return a structure with repo's interesting commits, suitable for
1437 the selectors in pullrequest controller
1439 the selectors in pullrequest controller
1438
1440
1439 :param commit_id: a commit that must be in the list somehow
1441 :param commit_id: a commit that must be in the list somehow
1440 and selected by default
1442 and selected by default
1441 :param branch: a branch that must be in the list and selected
1443 :param branch: a branch that must be in the list and selected
1442 by default - even if closed
1444 by default - even if closed
1443 :param bookmark: a bookmark that must be in the list and selected
1445 :param bookmark: a bookmark that must be in the list and selected
1444 """
1446 """
1445 _ = translator or get_current_request().translate
1447 _ = translator or get_current_request().translate
1446
1448
1447 commit_id = safe_str(commit_id) if commit_id else None
1449 commit_id = safe_str(commit_id) if commit_id else None
1448 branch = safe_unicode(branch) if branch else None
1450 branch = safe_unicode(branch) if branch else None
1449 bookmark = safe_unicode(bookmark) if bookmark else None
1451 bookmark = safe_unicode(bookmark) if bookmark else None
1450
1452
1451 selected = None
1453 selected = None
1452
1454
1453 # order matters: first source that has commit_id in it will be selected
1455 # order matters: first source that has commit_id in it will be selected
1454 sources = []
1456 sources = []
1455 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1457 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1456 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1458 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1457
1459
1458 if commit_id:
1460 if commit_id:
1459 ref_commit = (h.short_id(commit_id), commit_id)
1461 ref_commit = (h.short_id(commit_id), commit_id)
1460 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1462 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1461
1463
1462 sources.append(
1464 sources.append(
1463 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1465 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1464 )
1466 )
1465
1467
1466 groups = []
1468 groups = []
1467
1469
1468 for group_key, ref_list, group_name, match in sources:
1470 for group_key, ref_list, group_name, match in sources:
1469 group_refs = []
1471 group_refs = []
1470 for ref_name, ref_id in ref_list:
1472 for ref_name, ref_id in ref_list:
1471 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1473 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1472 group_refs.append((ref_key, ref_name))
1474 group_refs.append((ref_key, ref_name))
1473
1475
1474 if not selected:
1476 if not selected:
1475 if set([commit_id, match]) & set([ref_id, ref_name]):
1477 if set([commit_id, match]) & set([ref_id, ref_name]):
1476 selected = ref_key
1478 selected = ref_key
1477
1479
1478 if group_refs:
1480 if group_refs:
1479 groups.append((group_refs, group_name))
1481 groups.append((group_refs, group_name))
1480
1482
1481 if not selected:
1483 if not selected:
1482 ref = commit_id or branch or bookmark
1484 ref = commit_id or branch or bookmark
1483 if ref:
1485 if ref:
1484 raise CommitDoesNotExistError(
1486 raise CommitDoesNotExistError(
1485 u'No commit refs could be found matching: {}'.format(ref))
1487 u'No commit refs could be found matching: {}'.format(ref))
1486 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1488 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1487 selected = u'branch:{}:{}'.format(
1489 selected = u'branch:{}:{}'.format(
1488 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1490 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1489 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1491 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1490 )
1492 )
1491 elif repo.commit_ids:
1493 elif repo.commit_ids:
1492 # make the user select in this case
1494 # make the user select in this case
1493 selected = None
1495 selected = None
1494 else:
1496 else:
1495 raise EmptyRepositoryError()
1497 raise EmptyRepositoryError()
1496 return groups, selected
1498 return groups, selected
1497
1499
1498 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1500 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1499 hide_whitespace_changes, diff_context):
1501 hide_whitespace_changes, diff_context):
1500
1502
1501 return self._get_diff_from_pr_or_version(
1503 return self._get_diff_from_pr_or_version(
1502 source_repo, source_ref_id, target_ref_id,
1504 source_repo, source_ref_id, target_ref_id,
1503 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1505 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1504
1506
1505 def _get_diff_from_pr_or_version(
1507 def _get_diff_from_pr_or_version(
1506 self, source_repo, source_ref_id, target_ref_id,
1508 self, source_repo, source_ref_id, target_ref_id,
1507 hide_whitespace_changes, diff_context):
1509 hide_whitespace_changes, diff_context):
1508
1510
1509 target_commit = source_repo.get_commit(
1511 target_commit = source_repo.get_commit(
1510 commit_id=safe_str(target_ref_id))
1512 commit_id=safe_str(target_ref_id))
1511 source_commit = source_repo.get_commit(
1513 source_commit = source_repo.get_commit(
1512 commit_id=safe_str(source_ref_id))
1514 commit_id=safe_str(source_ref_id))
1513 if isinstance(source_repo, Repository):
1515 if isinstance(source_repo, Repository):
1514 vcs_repo = source_repo.scm_instance()
1516 vcs_repo = source_repo.scm_instance()
1515 else:
1517 else:
1516 vcs_repo = source_repo
1518 vcs_repo = source_repo
1517
1519
1518 # TODO: johbo: In the context of an update, we cannot reach
1520 # TODO: johbo: In the context of an update, we cannot reach
1519 # the old commit anymore with our normal mechanisms. It needs
1521 # the old commit anymore with our normal mechanisms. It needs
1520 # some sort of special support in the vcs layer to avoid this
1522 # some sort of special support in the vcs layer to avoid this
1521 # workaround.
1523 # workaround.
1522 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1524 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1523 vcs_repo.alias == 'git'):
1525 vcs_repo.alias == 'git'):
1524 source_commit.raw_id = safe_str(source_ref_id)
1526 source_commit.raw_id = safe_str(source_ref_id)
1525
1527
1526 log.debug('calculating diff between '
1528 log.debug('calculating diff between '
1527 'source_ref:%s and target_ref:%s for repo `%s`',
1529 'source_ref:%s and target_ref:%s for repo `%s`',
1528 target_ref_id, source_ref_id,
1530 target_ref_id, source_ref_id,
1529 safe_unicode(vcs_repo.path))
1531 safe_unicode(vcs_repo.path))
1530
1532
1531 vcs_diff = vcs_repo.get_diff(
1533 vcs_diff = vcs_repo.get_diff(
1532 commit1=target_commit, commit2=source_commit,
1534 commit1=target_commit, commit2=source_commit,
1533 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1535 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1534 return vcs_diff
1536 return vcs_diff
1535
1537
1536 def _is_merge_enabled(self, pull_request):
1538 def _is_merge_enabled(self, pull_request):
1537 return self._get_general_setting(
1539 return self._get_general_setting(
1538 pull_request, 'rhodecode_pr_merge_enabled')
1540 pull_request, 'rhodecode_pr_merge_enabled')
1539
1541
1540 def _use_rebase_for_merging(self, pull_request):
1542 def _use_rebase_for_merging(self, pull_request):
1541 repo_type = pull_request.target_repo.repo_type
1543 repo_type = pull_request.target_repo.repo_type
1542 if repo_type == 'hg':
1544 if repo_type == 'hg':
1543 return self._get_general_setting(
1545 return self._get_general_setting(
1544 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1546 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1545 elif repo_type == 'git':
1547 elif repo_type == 'git':
1546 return self._get_general_setting(
1548 return self._get_general_setting(
1547 pull_request, 'rhodecode_git_use_rebase_for_merging')
1549 pull_request, 'rhodecode_git_use_rebase_for_merging')
1548
1550
1549 return False
1551 return False
1550
1552
1551 def _close_branch_before_merging(self, pull_request):
1553 def _close_branch_before_merging(self, pull_request):
1552 repo_type = pull_request.target_repo.repo_type
1554 repo_type = pull_request.target_repo.repo_type
1553 if repo_type == 'hg':
1555 if repo_type == 'hg':
1554 return self._get_general_setting(
1556 return self._get_general_setting(
1555 pull_request, 'rhodecode_hg_close_branch_before_merging')
1557 pull_request, 'rhodecode_hg_close_branch_before_merging')
1556 elif repo_type == 'git':
1558 elif repo_type == 'git':
1557 return self._get_general_setting(
1559 return self._get_general_setting(
1558 pull_request, 'rhodecode_git_close_branch_before_merging')
1560 pull_request, 'rhodecode_git_close_branch_before_merging')
1559
1561
1560 return False
1562 return False
1561
1563
1562 def _get_general_setting(self, pull_request, settings_key, default=False):
1564 def _get_general_setting(self, pull_request, settings_key, default=False):
1563 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1565 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1564 settings = settings_model.get_general_settings()
1566 settings = settings_model.get_general_settings()
1565 return settings.get(settings_key, default)
1567 return settings.get(settings_key, default)
1566
1568
1567 def _log_audit_action(self, action, action_data, user, pull_request):
1569 def _log_audit_action(self, action, action_data, user, pull_request):
1568 audit_logger.store(
1570 audit_logger.store(
1569 action=action,
1571 action=action,
1570 action_data=action_data,
1572 action_data=action_data,
1571 user=user,
1573 user=user,
1572 repo=pull_request.target_repo)
1574 repo=pull_request.target_repo)
1573
1575
1574 def get_reviewer_functions(self):
1576 def get_reviewer_functions(self):
1575 """
1577 """
1576 Fetches functions for validation and fetching default reviewers.
1578 Fetches functions for validation and fetching default reviewers.
1577 If available we use the EE package, else we fallback to CE
1579 If available we use the EE package, else we fallback to CE
1578 package functions
1580 package functions
1579 """
1581 """
1580 try:
1582 try:
1581 from rc_reviewers.utils import get_default_reviewers_data
1583 from rc_reviewers.utils import get_default_reviewers_data
1582 from rc_reviewers.utils import validate_default_reviewers
1584 from rc_reviewers.utils import validate_default_reviewers
1583 except ImportError:
1585 except ImportError:
1584 from rhodecode.apps.repository.utils import get_default_reviewers_data
1586 from rhodecode.apps.repository.utils import get_default_reviewers_data
1585 from rhodecode.apps.repository.utils import validate_default_reviewers
1587 from rhodecode.apps.repository.utils import validate_default_reviewers
1586
1588
1587 return get_default_reviewers_data, validate_default_reviewers
1589 return get_default_reviewers_data, validate_default_reviewers
1588
1590
1589
1591
1590 class MergeCheck(object):
1592 class MergeCheck(object):
1591 """
1593 """
1592 Perform Merge Checks and returns a check object which stores information
1594 Perform Merge Checks and returns a check object which stores information
1593 about merge errors, and merge conditions
1595 about merge errors, and merge conditions
1594 """
1596 """
1595 TODO_CHECK = 'todo'
1597 TODO_CHECK = 'todo'
1596 PERM_CHECK = 'perm'
1598 PERM_CHECK = 'perm'
1597 REVIEW_CHECK = 'review'
1599 REVIEW_CHECK = 'review'
1598 MERGE_CHECK = 'merge'
1600 MERGE_CHECK = 'merge'
1599
1601
1600 def __init__(self):
1602 def __init__(self):
1601 self.review_status = None
1603 self.review_status = None
1602 self.merge_possible = None
1604 self.merge_possible = None
1603 self.merge_msg = ''
1605 self.merge_msg = ''
1604 self.failed = None
1606 self.failed = None
1605 self.errors = []
1607 self.errors = []
1606 self.error_details = OrderedDict()
1608 self.error_details = OrderedDict()
1607
1609
1608 def push_error(self, error_type, message, error_key, details):
1610 def push_error(self, error_type, message, error_key, details):
1609 self.failed = True
1611 self.failed = True
1610 self.errors.append([error_type, message])
1612 self.errors.append([error_type, message])
1611 self.error_details[error_key] = dict(
1613 self.error_details[error_key] = dict(
1612 details=details,
1614 details=details,
1613 error_type=error_type,
1615 error_type=error_type,
1614 message=message
1616 message=message
1615 )
1617 )
1616
1618
1617 @classmethod
1619 @classmethod
1618 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1620 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1619 force_shadow_repo_refresh=False):
1621 force_shadow_repo_refresh=False):
1620 _ = translator
1622 _ = translator
1621 merge_check = cls()
1623 merge_check = cls()
1622
1624
1623 # permissions to merge
1625 # permissions to merge
1624 user_allowed_to_merge = PullRequestModel().check_user_merge(
1626 user_allowed_to_merge = PullRequestModel().check_user_merge(
1625 pull_request, auth_user)
1627 pull_request, auth_user)
1626 if not user_allowed_to_merge:
1628 if not user_allowed_to_merge:
1627 log.debug("MergeCheck: cannot merge, approval is pending.")
1629 log.debug("MergeCheck: cannot merge, approval is pending.")
1628
1630
1629 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1631 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1630 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1632 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1631 if fail_early:
1633 if fail_early:
1632 return merge_check
1634 return merge_check
1633
1635
1634 # permission to merge into the target branch
1636 # permission to merge into the target branch
1635 target_commit_id = pull_request.target_ref_parts.commit_id
1637 target_commit_id = pull_request.target_ref_parts.commit_id
1636 if pull_request.target_ref_parts.type == 'branch':
1638 if pull_request.target_ref_parts.type == 'branch':
1637 branch_name = pull_request.target_ref_parts.name
1639 branch_name = pull_request.target_ref_parts.name
1638 else:
1640 else:
1639 # for mercurial we can always figure out the branch from the commit
1641 # for mercurial we can always figure out the branch from the commit
1640 # in case of bookmark
1642 # in case of bookmark
1641 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1643 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1642 branch_name = target_commit.branch
1644 branch_name = target_commit.branch
1643
1645
1644 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1646 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1645 pull_request.target_repo.repo_name, branch_name)
1647 pull_request.target_repo.repo_name, branch_name)
1646 if branch_perm and branch_perm == 'branch.none':
1648 if branch_perm and branch_perm == 'branch.none':
1647 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1649 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1648 branch_name, rule)
1650 branch_name, rule)
1649 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1651 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1650 if fail_early:
1652 if fail_early:
1651 return merge_check
1653 return merge_check
1652
1654
1653 # review status, must be always present
1655 # review status, must be always present
1654 review_status = pull_request.calculated_review_status()
1656 review_status = pull_request.calculated_review_status()
1655 merge_check.review_status = review_status
1657 merge_check.review_status = review_status
1656
1658
1657 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1659 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1658 if not status_approved:
1660 if not status_approved:
1659 log.debug("MergeCheck: cannot merge, approval is pending.")
1661 log.debug("MergeCheck: cannot merge, approval is pending.")
1660
1662
1661 msg = _('Pull request reviewer approval is pending.')
1663 msg = _('Pull request reviewer approval is pending.')
1662
1664
1663 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1665 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1664
1666
1665 if fail_early:
1667 if fail_early:
1666 return merge_check
1668 return merge_check
1667
1669
1668 # left over TODOs
1670 # left over TODOs
1669 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1671 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1670 if todos:
1672 if todos:
1671 log.debug("MergeCheck: cannot merge, {} "
1673 log.debug("MergeCheck: cannot merge, {} "
1672 "unresolved TODOs left.".format(len(todos)))
1674 "unresolved TODOs left.".format(len(todos)))
1673
1675
1674 if len(todos) == 1:
1676 if len(todos) == 1:
1675 msg = _('Cannot merge, {} TODO still not resolved.').format(
1677 msg = _('Cannot merge, {} TODO still not resolved.').format(
1676 len(todos))
1678 len(todos))
1677 else:
1679 else:
1678 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1680 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1679 len(todos))
1681 len(todos))
1680
1682
1681 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1683 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1682
1684
1683 if fail_early:
1685 if fail_early:
1684 return merge_check
1686 return merge_check
1685
1687
1686 # merge possible, here is the filesystem simulation + shadow repo
1688 # merge possible, here is the filesystem simulation + shadow repo
1687 merge_status, msg = PullRequestModel().merge_status(
1689 merge_status, msg = PullRequestModel().merge_status(
1688 pull_request, translator=translator,
1690 pull_request, translator=translator,
1689 force_shadow_repo_refresh=force_shadow_repo_refresh)
1691 force_shadow_repo_refresh=force_shadow_repo_refresh)
1690 merge_check.merge_possible = merge_status
1692 merge_check.merge_possible = merge_status
1691 merge_check.merge_msg = msg
1693 merge_check.merge_msg = msg
1692 if not merge_status:
1694 if not merge_status:
1693 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1695 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1694 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1696 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1695
1697
1696 if fail_early:
1698 if fail_early:
1697 return merge_check
1699 return merge_check
1698
1700
1699 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1701 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1700 return merge_check
1702 return merge_check
1701
1703
1702 @classmethod
1704 @classmethod
1703 def get_merge_conditions(cls, pull_request, translator):
1705 def get_merge_conditions(cls, pull_request, translator):
1704 _ = translator
1706 _ = translator
1705 merge_details = {}
1707 merge_details = {}
1706
1708
1707 model = PullRequestModel()
1709 model = PullRequestModel()
1708 use_rebase = model._use_rebase_for_merging(pull_request)
1710 use_rebase = model._use_rebase_for_merging(pull_request)
1709
1711
1710 if use_rebase:
1712 if use_rebase:
1711 merge_details['merge_strategy'] = dict(
1713 merge_details['merge_strategy'] = dict(
1712 details={},
1714 details={},
1713 message=_('Merge strategy: rebase')
1715 message=_('Merge strategy: rebase')
1714 )
1716 )
1715 else:
1717 else:
1716 merge_details['merge_strategy'] = dict(
1718 merge_details['merge_strategy'] = dict(
1717 details={},
1719 details={},
1718 message=_('Merge strategy: explicit merge commit')
1720 message=_('Merge strategy: explicit merge commit')
1719 )
1721 )
1720
1722
1721 close_branch = model._close_branch_before_merging(pull_request)
1723 close_branch = model._close_branch_before_merging(pull_request)
1722 if close_branch:
1724 if close_branch:
1723 repo_type = pull_request.target_repo.repo_type
1725 repo_type = pull_request.target_repo.repo_type
1724 close_msg = ''
1726 close_msg = ''
1725 if repo_type == 'hg':
1727 if repo_type == 'hg':
1726 close_msg = _('Source branch will be closed after merge.')
1728 close_msg = _('Source branch will be closed after merge.')
1727 elif repo_type == 'git':
1729 elif repo_type == 'git':
1728 close_msg = _('Source branch will be deleted after merge.')
1730 close_msg = _('Source branch will be deleted after merge.')
1729
1731
1730 merge_details['close_branch'] = dict(
1732 merge_details['close_branch'] = dict(
1731 details={},
1733 details={},
1732 message=close_msg
1734 message=close_msg
1733 )
1735 )
1734
1736
1735 return merge_details
1737 return merge_details
1736
1738
1737
1739
1738 ChangeTuple = collections.namedtuple(
1740 ChangeTuple = collections.namedtuple(
1739 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1741 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1740
1742
1741 FileChangeTuple = collections.namedtuple(
1743 FileChangeTuple = collections.namedtuple(
1742 'FileChangeTuple', ['added', 'modified', 'removed'])
1744 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now