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