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