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