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