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