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