##// END OF EJS Templates
pr-model: don't use set to calculate commit ranges as they generate random order.
marcink -
r1372:b4f72747 default
parent child Browse files
Show More
@@ -1,1420 +1,1420 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31
31
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.i18n.translation import lazy_ugettext
33 from pylons.i18n.translation import lazy_ugettext
34 from sqlalchemy import or_
34 from sqlalchemy import or_
35
35
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
39 from rhodecode.lib.markup_renderer import (
39 from rhodecode.lib.markup_renderer import (
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
41 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils import action_logger
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
44 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
45 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 CommitDoesNotExistError, EmptyRepositoryError)
47 CommitDoesNotExistError, EmptyRepositoryError)
48 from rhodecode.model import BaseModel
48 from rhodecode.model import BaseModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.db import (
51 from rhodecode.model.db import (
52 PullRequest, PullRequestReviewers, ChangesetStatus,
52 PullRequest, PullRequestReviewers, ChangesetStatus,
53 PullRequestVersion, ChangesetComment, Repository)
53 PullRequestVersion, ChangesetComment, Repository)
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.notification import NotificationModel, \
55 from rhodecode.model.notification import NotificationModel, \
56 EmailNotificationModel
56 EmailNotificationModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.settings import VcsSettingsModel
58 from rhodecode.model.settings import VcsSettingsModel
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 # Data structure to hold the response data when updating commits during a pull
64 # Data structure to hold the response data when updating commits during a pull
65 # request update.
65 # request update.
66 UpdateResponse = namedtuple(
66 UpdateResponse = namedtuple(
67 'UpdateResponse', 'executed, reason, new, old, changes')
67 'UpdateResponse', 'executed, reason, new, old, changes')
68
68
69
69
70 class PullRequestModel(BaseModel):
70 class PullRequestModel(BaseModel):
71
71
72 cls = PullRequest
72 cls = PullRequest
73
73
74 DIFF_CONTEXT = 3
74 DIFF_CONTEXT = 3
75
75
76 MERGE_STATUS_MESSAGES = {
76 MERGE_STATUS_MESSAGES = {
77 MergeFailureReason.NONE: lazy_ugettext(
77 MergeFailureReason.NONE: lazy_ugettext(
78 'This pull request can be automatically merged.'),
78 'This pull request can be automatically merged.'),
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
79 MergeFailureReason.UNKNOWN: lazy_ugettext(
80 'This pull request cannot be merged because of an unhandled'
80 'This pull request cannot be merged because of an unhandled'
81 ' exception.'),
81 ' exception.'),
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
82 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
83 'This pull request cannot be merged because of merge conflicts.'),
83 'This pull request cannot be merged because of merge conflicts.'),
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
84 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
85 'This pull request could not be merged because push to target'
85 'This pull request could not be merged because push to target'
86 ' failed.'),
86 ' failed.'),
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
87 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
88 'This pull request cannot be merged because the target is not a'
88 'This pull request cannot be merged because the target is not a'
89 ' head.'),
89 ' head.'),
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
90 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
91 'This pull request cannot be merged because the source contains'
91 'This pull request cannot be merged because the source contains'
92 ' more branches than the target.'),
92 ' more branches than the target.'),
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
93 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
94 'This pull request cannot be merged because the target has'
94 'This pull request cannot be merged because the target has'
95 ' multiple heads.'),
95 ' multiple heads.'),
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
96 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
97 'This pull request cannot be merged because the target repository'
97 'This pull request cannot be merged because the target repository'
98 ' is locked.'),
98 ' is locked.'),
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
99 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
100 'This pull request cannot be merged because the target or the '
100 'This pull request cannot be merged because the target or the '
101 'source reference is missing.'),
101 'source reference is missing.'),
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
103 'This pull request cannot be merged because the target '
103 'This pull request cannot be merged because the target '
104 'reference is missing.'),
104 'reference is missing.'),
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
106 'This pull request cannot be merged because the source '
106 'This pull request cannot be merged because the source '
107 'reference is missing.'),
107 'reference is missing.'),
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
108 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
109 'This pull request cannot be merged because of conflicts related '
109 'This pull request cannot be merged because of conflicts related '
110 'to sub repositories.'),
110 'to sub repositories.'),
111 }
111 }
112
112
113 UPDATE_STATUS_MESSAGES = {
113 UPDATE_STATUS_MESSAGES = {
114 UpdateFailureReason.NONE: lazy_ugettext(
114 UpdateFailureReason.NONE: lazy_ugettext(
115 'Pull request update successful.'),
115 'Pull request update successful.'),
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
116 UpdateFailureReason.UNKNOWN: lazy_ugettext(
117 'Pull request update failed because of an unknown error.'),
117 'Pull request update failed because of an unknown error.'),
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
118 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
119 'No update needed because the source reference is already '
119 'No update needed because the source reference is already '
120 'up to date.'),
120 'up to date.'),
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
121 UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext(
122 'Pull request cannot be updated because the reference type is '
122 'Pull request cannot be updated because the reference type is '
123 'not supported for an update.'),
123 'not supported for an update.'),
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
124 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 'This pull request cannot be updated because the target '
125 'This pull request cannot be updated because the target '
126 'reference is missing.'),
126 'reference is missing.'),
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
127 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 'This pull request cannot be updated because the source '
128 'This pull request cannot be updated because the source '
129 'reference is missing.'),
129 'reference is missing.'),
130 }
130 }
131
131
132 def __get_pull_request(self, pull_request):
132 def __get_pull_request(self, pull_request):
133 return self._get_instance((
133 return self._get_instance((
134 PullRequest, PullRequestVersion), pull_request)
134 PullRequest, PullRequestVersion), pull_request)
135
135
136 def _check_perms(self, perms, pull_request, user, api=False):
136 def _check_perms(self, perms, pull_request, user, api=False):
137 if not api:
137 if not api:
138 return h.HasRepoPermissionAny(*perms)(
138 return h.HasRepoPermissionAny(*perms)(
139 user=user, repo_name=pull_request.target_repo.repo_name)
139 user=user, repo_name=pull_request.target_repo.repo_name)
140 else:
140 else:
141 return h.HasRepoPermissionAnyApi(*perms)(
141 return h.HasRepoPermissionAnyApi(*perms)(
142 user=user, repo_name=pull_request.target_repo.repo_name)
142 user=user, repo_name=pull_request.target_repo.repo_name)
143
143
144 def check_user_read(self, pull_request, user, api=False):
144 def check_user_read(self, pull_request, user, api=False):
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
145 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 return self._check_perms(_perms, pull_request, user, api)
146 return self._check_perms(_perms, pull_request, user, api)
147
147
148 def check_user_merge(self, pull_request, user, api=False):
148 def check_user_merge(self, pull_request, user, api=False):
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
149 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 return self._check_perms(_perms, pull_request, user, api)
150 return self._check_perms(_perms, pull_request, user, api)
151
151
152 def check_user_update(self, pull_request, user, api=False):
152 def check_user_update(self, pull_request, user, api=False):
153 owner = user.user_id == pull_request.user_id
153 owner = user.user_id == pull_request.user_id
154 return self.check_user_merge(pull_request, user, api) or owner
154 return self.check_user_merge(pull_request, user, api) or owner
155
155
156 def check_user_delete(self, pull_request, user):
156 def check_user_delete(self, pull_request, user):
157 owner = user.user_id == pull_request.user_id
157 owner = user.user_id == pull_request.user_id
158 _perms = ('repository.admin')
158 _perms = ('repository.admin')
159 return self._check_perms(_perms, pull_request, user) or owner
159 return self._check_perms(_perms, pull_request, user) or owner
160
160
161 def check_user_change_status(self, pull_request, user, api=False):
161 def check_user_change_status(self, pull_request, user, api=False):
162 reviewer = user.user_id in [x.user_id for x in
162 reviewer = user.user_id in [x.user_id for x in
163 pull_request.reviewers]
163 pull_request.reviewers]
164 return self.check_user_update(pull_request, user, api) or reviewer
164 return self.check_user_update(pull_request, user, api) or reviewer
165
165
166 def get(self, pull_request):
166 def get(self, pull_request):
167 return self.__get_pull_request(pull_request)
167 return self.__get_pull_request(pull_request)
168
168
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
169 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
170 opened_by=None, order_by=None,
170 opened_by=None, order_by=None,
171 order_dir='desc'):
171 order_dir='desc'):
172 repo = None
172 repo = None
173 if repo_name:
173 if repo_name:
174 repo = self._get_repo(repo_name)
174 repo = self._get_repo(repo_name)
175
175
176 q = PullRequest.query()
176 q = PullRequest.query()
177
177
178 # source or target
178 # source or target
179 if repo and source:
179 if repo and source:
180 q = q.filter(PullRequest.source_repo == repo)
180 q = q.filter(PullRequest.source_repo == repo)
181 elif repo:
181 elif repo:
182 q = q.filter(PullRequest.target_repo == repo)
182 q = q.filter(PullRequest.target_repo == repo)
183
183
184 # closed,opened
184 # closed,opened
185 if statuses:
185 if statuses:
186 q = q.filter(PullRequest.status.in_(statuses))
186 q = q.filter(PullRequest.status.in_(statuses))
187
187
188 # opened by filter
188 # opened by filter
189 if opened_by:
189 if opened_by:
190 q = q.filter(PullRequest.user_id.in_(opened_by))
190 q = q.filter(PullRequest.user_id.in_(opened_by))
191
191
192 if order_by:
192 if order_by:
193 order_map = {
193 order_map = {
194 'name_raw': PullRequest.pull_request_id,
194 'name_raw': PullRequest.pull_request_id,
195 'title': PullRequest.title,
195 'title': PullRequest.title,
196 'updated_on_raw': PullRequest.updated_on,
196 'updated_on_raw': PullRequest.updated_on,
197 'target_repo': PullRequest.target_repo_id
197 'target_repo': PullRequest.target_repo_id
198 }
198 }
199 if order_dir == 'asc':
199 if order_dir == 'asc':
200 q = q.order_by(order_map[order_by].asc())
200 q = q.order_by(order_map[order_by].asc())
201 else:
201 else:
202 q = q.order_by(order_map[order_by].desc())
202 q = q.order_by(order_map[order_by].desc())
203
203
204 return q
204 return q
205
205
206 def count_all(self, repo_name, source=False, statuses=None,
206 def count_all(self, repo_name, source=False, statuses=None,
207 opened_by=None):
207 opened_by=None):
208 """
208 """
209 Count the number of pull requests for a specific repository.
209 Count the number of pull requests for a specific repository.
210
210
211 :param repo_name: target or source repo
211 :param repo_name: target or source repo
212 :param source: boolean flag to specify if repo_name refers to source
212 :param source: boolean flag to specify if repo_name refers to source
213 :param statuses: list of pull request statuses
213 :param statuses: list of pull request statuses
214 :param opened_by: author user of the pull request
214 :param opened_by: author user of the pull request
215 :returns: int number of pull requests
215 :returns: int number of pull requests
216 """
216 """
217 q = self._prepare_get_all_query(
217 q = self._prepare_get_all_query(
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
218 repo_name, source=source, statuses=statuses, opened_by=opened_by)
219
219
220 return q.count()
220 return q.count()
221
221
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
222 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
223 offset=0, length=None, order_by=None, order_dir='desc'):
223 offset=0, length=None, order_by=None, order_dir='desc'):
224 """
224 """
225 Get all pull requests for a specific repository.
225 Get all pull requests for a specific repository.
226
226
227 :param repo_name: target or source repo
227 :param repo_name: target or source repo
228 :param source: boolean flag to specify if repo_name refers to source
228 :param source: boolean flag to specify if repo_name refers to source
229 :param statuses: list of pull request statuses
229 :param statuses: list of pull request statuses
230 :param opened_by: author user of the pull request
230 :param opened_by: author user of the pull request
231 :param offset: pagination offset
231 :param offset: pagination offset
232 :param length: length of returned list
232 :param length: length of returned list
233 :param order_by: order of the returned list
233 :param order_by: order of the returned list
234 :param order_dir: 'asc' or 'desc' ordering direction
234 :param order_dir: 'asc' or 'desc' ordering direction
235 :returns: list of pull requests
235 :returns: list of pull requests
236 """
236 """
237 q = self._prepare_get_all_query(
237 q = self._prepare_get_all_query(
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
238 repo_name, source=source, statuses=statuses, opened_by=opened_by,
239 order_by=order_by, order_dir=order_dir)
239 order_by=order_by, order_dir=order_dir)
240
240
241 if length:
241 if length:
242 pull_requests = q.limit(length).offset(offset).all()
242 pull_requests = q.limit(length).offset(offset).all()
243 else:
243 else:
244 pull_requests = q.all()
244 pull_requests = q.all()
245
245
246 return pull_requests
246 return pull_requests
247
247
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
248 def count_awaiting_review(self, repo_name, source=False, statuses=None,
249 opened_by=None):
249 opened_by=None):
250 """
250 """
251 Count the number of pull requests for a specific repository that are
251 Count the number of pull requests for a specific repository that are
252 awaiting review.
252 awaiting review.
253
253
254 :param repo_name: target or source repo
254 :param repo_name: target or source repo
255 :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
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param source: boolean flag to specify if repo_name refers to source
273 :param source: boolean flag to specify if repo_name refers to source
274 :param statuses: list of pull request statuses
274 :param statuses: list of pull request statuses
275 :param opened_by: author user of the pull request
275 :param opened_by: author user of the pull request
276 :param offset: pagination offset
276 :param offset: pagination offset
277 :param length: length of returned list
277 :param length: length of returned list
278 :param order_by: order of the returned list
278 :param order_by: order of the returned list
279 :param order_dir: 'asc' or 'desc' ordering direction
279 :param order_dir: 'asc' or 'desc' ordering direction
280 :returns: list of pull requests
280 :returns: list of pull requests
281 """
281 """
282 pull_requests = self.get_all(
282 pull_requests = self.get_all(
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
283 repo_name, source=source, statuses=statuses, opened_by=opened_by,
284 order_by=order_by, order_dir=order_dir)
284 order_by=order_by, order_dir=order_dir)
285
285
286 _filtered_pull_requests = []
286 _filtered_pull_requests = []
287 for pr in pull_requests:
287 for pr in pull_requests:
288 status = pr.calculated_review_status()
288 status = pr.calculated_review_status()
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
289 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
290 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 _filtered_pull_requests.append(pr)
291 _filtered_pull_requests.append(pr)
292 if length:
292 if length:
293 return _filtered_pull_requests[offset:offset+length]
293 return _filtered_pull_requests[offset:offset+length]
294 else:
294 else:
295 return _filtered_pull_requests
295 return _filtered_pull_requests
296
296
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
297 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
298 opened_by=None, user_id=None):
298 opened_by=None, user_id=None):
299 """
299 """
300 Count the number of pull requests for a specific repository that are
300 Count the number of pull requests for a specific repository that are
301 awaiting review from a specific user.
301 awaiting review from a specific user.
302
302
303 :param repo_name: target or source repo
303 :param repo_name: target or source repo
304 :param source: boolean flag to specify if repo_name refers to source
304 :param source: boolean flag to specify if repo_name refers to source
305 :param statuses: list of pull request statuses
305 :param statuses: list of pull request statuses
306 :param opened_by: author user of the pull request
306 :param opened_by: author user of the pull request
307 :param user_id: reviewer user of the pull request
307 :param user_id: reviewer user of the pull request
308 :returns: int number of pull requests
308 :returns: int number of pull requests
309 """
309 """
310 pull_requests = self.get_awaiting_my_review(
310 pull_requests = self.get_awaiting_my_review(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 user_id=user_id)
312 user_id=user_id)
313
313
314 return len(pull_requests)
314 return len(pull_requests)
315
315
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
316 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
317 opened_by=None, user_id=None, offset=0,
317 opened_by=None, user_id=None, offset=0,
318 length=None, order_by=None, order_dir='desc'):
318 length=None, order_by=None, order_dir='desc'):
319 """
319 """
320 Get all pull requests for a specific repository that are awaiting
320 Get all pull requests for a specific repository that are awaiting
321 review from a specific user.
321 review from a specific user.
322
322
323 :param repo_name: target or source repo
323 :param repo_name: target or source repo
324 :param source: boolean flag to specify if repo_name refers to source
324 :param source: boolean flag to specify if repo_name refers to source
325 :param statuses: list of pull request statuses
325 :param statuses: list of pull request statuses
326 :param opened_by: author user of the pull request
326 :param opened_by: author user of the pull request
327 :param user_id: reviewer user of the pull request
327 :param user_id: reviewer user of the pull request
328 :param offset: pagination offset
328 :param offset: pagination offset
329 :param length: length of returned list
329 :param length: length of returned list
330 :param order_by: order of the returned list
330 :param order_by: order of the returned list
331 :param order_dir: 'asc' or 'desc' ordering direction
331 :param order_dir: 'asc' or 'desc' ordering direction
332 :returns: list of pull requests
332 :returns: list of pull requests
333 """
333 """
334 pull_requests = self.get_all(
334 pull_requests = self.get_all(
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
335 repo_name, source=source, statuses=statuses, opened_by=opened_by,
336 order_by=order_by, order_dir=order_dir)
336 order_by=order_by, order_dir=order_dir)
337
337
338 _my = PullRequestModel().get_not_reviewed(user_id)
338 _my = PullRequestModel().get_not_reviewed(user_id)
339 my_participation = []
339 my_participation = []
340 for pr in pull_requests:
340 for pr in pull_requests:
341 if pr in _my:
341 if pr in _my:
342 my_participation.append(pr)
342 my_participation.append(pr)
343 _filtered_pull_requests = my_participation
343 _filtered_pull_requests = my_participation
344 if length:
344 if length:
345 return _filtered_pull_requests[offset:offset+length]
345 return _filtered_pull_requests[offset:offset+length]
346 else:
346 else:
347 return _filtered_pull_requests
347 return _filtered_pull_requests
348
348
349 def get_not_reviewed(self, user_id):
349 def get_not_reviewed(self, user_id):
350 return [
350 return [
351 x.pull_request for x in PullRequestReviewers.query().filter(
351 x.pull_request for x in PullRequestReviewers.query().filter(
352 PullRequestReviewers.user_id == user_id).all()
352 PullRequestReviewers.user_id == user_id).all()
353 ]
353 ]
354
354
355 def _prepare_participating_query(self, user_id=None, statuses=None,
355 def _prepare_participating_query(self, user_id=None, statuses=None,
356 order_by=None, order_dir='desc'):
356 order_by=None, order_dir='desc'):
357 q = PullRequest.query()
357 q = PullRequest.query()
358 if user_id:
358 if user_id:
359 reviewers_subquery = Session().query(
359 reviewers_subquery = Session().query(
360 PullRequestReviewers.pull_request_id).filter(
360 PullRequestReviewers.pull_request_id).filter(
361 PullRequestReviewers.user_id == user_id).subquery()
361 PullRequestReviewers.user_id == user_id).subquery()
362 user_filter= or_(
362 user_filter= or_(
363 PullRequest.user_id == user_id,
363 PullRequest.user_id == user_id,
364 PullRequest.pull_request_id.in_(reviewers_subquery)
364 PullRequest.pull_request_id.in_(reviewers_subquery)
365 )
365 )
366 q = PullRequest.query().filter(user_filter)
366 q = PullRequest.query().filter(user_filter)
367
367
368 # closed,opened
368 # closed,opened
369 if statuses:
369 if statuses:
370 q = q.filter(PullRequest.status.in_(statuses))
370 q = q.filter(PullRequest.status.in_(statuses))
371
371
372 if order_by:
372 if order_by:
373 order_map = {
373 order_map = {
374 'name_raw': PullRequest.pull_request_id,
374 'name_raw': PullRequest.pull_request_id,
375 'title': PullRequest.title,
375 'title': PullRequest.title,
376 'updated_on_raw': PullRequest.updated_on,
376 'updated_on_raw': PullRequest.updated_on,
377 'target_repo': PullRequest.target_repo_id
377 'target_repo': PullRequest.target_repo_id
378 }
378 }
379 if order_dir == 'asc':
379 if order_dir == 'asc':
380 q = q.order_by(order_map[order_by].asc())
380 q = q.order_by(order_map[order_by].asc())
381 else:
381 else:
382 q = q.order_by(order_map[order_by].desc())
382 q = q.order_by(order_map[order_by].desc())
383
383
384 return q
384 return q
385
385
386 def count_im_participating_in(self, user_id=None, statuses=None):
386 def count_im_participating_in(self, user_id=None, statuses=None):
387 q = self._prepare_participating_query(user_id, statuses=statuses)
387 q = self._prepare_participating_query(user_id, statuses=statuses)
388 return q.count()
388 return q.count()
389
389
390 def get_im_participating_in(
390 def get_im_participating_in(
391 self, user_id=None, statuses=None, offset=0,
391 self, user_id=None, statuses=None, offset=0,
392 length=None, order_by=None, order_dir='desc'):
392 length=None, order_by=None, order_dir='desc'):
393 """
393 """
394 Get all Pull requests that i'm participating in, or i have opened
394 Get all Pull requests that i'm participating in, or i have opened
395 """
395 """
396
396
397 q = self._prepare_participating_query(
397 q = self._prepare_participating_query(
398 user_id, statuses=statuses, order_by=order_by,
398 user_id, statuses=statuses, order_by=order_by,
399 order_dir=order_dir)
399 order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def get_versions(self, pull_request):
408 def get_versions(self, pull_request):
409 """
409 """
410 returns version of pull request sorted by ID descending
410 returns version of pull request sorted by ID descending
411 """
411 """
412 return PullRequestVersion.query()\
412 return PullRequestVersion.query()\
413 .filter(PullRequestVersion.pull_request == pull_request)\
413 .filter(PullRequestVersion.pull_request == pull_request)\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
414 .order_by(PullRequestVersion.pull_request_version_id.asc())\
415 .all()
415 .all()
416
416
417 def create(self, created_by, source_repo, source_ref, target_repo,
417 def create(self, created_by, source_repo, source_ref, target_repo,
418 target_ref, revisions, reviewers, title, description=None):
418 target_ref, revisions, reviewers, title, description=None):
419 created_by_user = self._get_user(created_by)
419 created_by_user = self._get_user(created_by)
420 source_repo = self._get_repo(source_repo)
420 source_repo = self._get_repo(source_repo)
421 target_repo = self._get_repo(target_repo)
421 target_repo = self._get_repo(target_repo)
422
422
423 pull_request = PullRequest()
423 pull_request = PullRequest()
424 pull_request.source_repo = source_repo
424 pull_request.source_repo = source_repo
425 pull_request.source_ref = source_ref
425 pull_request.source_ref = source_ref
426 pull_request.target_repo = target_repo
426 pull_request.target_repo = target_repo
427 pull_request.target_ref = target_ref
427 pull_request.target_ref = target_ref
428 pull_request.revisions = revisions
428 pull_request.revisions = revisions
429 pull_request.title = title
429 pull_request.title = title
430 pull_request.description = description
430 pull_request.description = description
431 pull_request.author = created_by_user
431 pull_request.author = created_by_user
432
432
433 Session().add(pull_request)
433 Session().add(pull_request)
434 Session().flush()
434 Session().flush()
435
435
436 reviewer_ids = set()
436 reviewer_ids = set()
437 # members / reviewers
437 # members / reviewers
438 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
439 if isinstance(reviewer_object, tuple):
439 if isinstance(reviewer_object, tuple):
440 user_id, reasons = reviewer_object
440 user_id, reasons = reviewer_object
441 else:
441 else:
442 user_id, reasons = reviewer_object, []
442 user_id, reasons = reviewer_object, []
443
443
444 user = self._get_user(user_id)
444 user = self._get_user(user_id)
445 reviewer_ids.add(user.user_id)
445 reviewer_ids.add(user.user_id)
446
446
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
447 reviewer = PullRequestReviewers(user, pull_request, reasons)
448 Session().add(reviewer)
448 Session().add(reviewer)
449
449
450 # Set approval status to "Under Review" for all commits which are
450 # Set approval status to "Under Review" for all commits which are
451 # part of this pull request.
451 # part of this pull request.
452 ChangesetStatusModel().set_status(
452 ChangesetStatusModel().set_status(
453 repo=target_repo,
453 repo=target_repo,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
454 status=ChangesetStatus.STATUS_UNDER_REVIEW,
455 user=created_by_user,
455 user=created_by_user,
456 pull_request=pull_request
456 pull_request=pull_request
457 )
457 )
458
458
459 self.notify_reviewers(pull_request, reviewer_ids)
459 self.notify_reviewers(pull_request, reviewer_ids)
460 self._trigger_pull_request_hook(
460 self._trigger_pull_request_hook(
461 pull_request, created_by_user, 'create')
461 pull_request, created_by_user, 'create')
462
462
463 return pull_request
463 return pull_request
464
464
465 def _trigger_pull_request_hook(self, pull_request, user, action):
465 def _trigger_pull_request_hook(self, pull_request, user, action):
466 pull_request = self.__get_pull_request(pull_request)
466 pull_request = self.__get_pull_request(pull_request)
467 target_scm = pull_request.target_repo.scm_instance()
467 target_scm = pull_request.target_repo.scm_instance()
468 if action == 'create':
468 if action == 'create':
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
469 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
470 elif action == 'merge':
470 elif action == 'merge':
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
471 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
472 elif action == 'close':
472 elif action == 'close':
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
473 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
474 elif action == 'review_status_change':
474 elif action == 'review_status_change':
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
475 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
476 elif action == 'update':
476 elif action == 'update':
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
477 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
478 else:
478 else:
479 return
479 return
480
480
481 trigger_hook(
481 trigger_hook(
482 username=user.username,
482 username=user.username,
483 repo_name=pull_request.target_repo.repo_name,
483 repo_name=pull_request.target_repo.repo_name,
484 repo_alias=target_scm.alias,
484 repo_alias=target_scm.alias,
485 pull_request=pull_request)
485 pull_request=pull_request)
486
486
487 def _get_commit_ids(self, pull_request):
487 def _get_commit_ids(self, pull_request):
488 """
488 """
489 Return the commit ids of the merged pull request.
489 Return the commit ids of the merged pull request.
490
490
491 This method is not dealing correctly yet with the lack of autoupdates
491 This method is not dealing correctly yet with the lack of autoupdates
492 nor with the implicit target updates.
492 nor with the implicit target updates.
493 For example: if a commit in the source repo is already in the target it
493 For example: if a commit in the source repo is already in the target it
494 will be reported anyways.
494 will be reported anyways.
495 """
495 """
496 merge_rev = pull_request.merge_rev
496 merge_rev = pull_request.merge_rev
497 if merge_rev is None:
497 if merge_rev is None:
498 raise ValueError('This pull request was not merged yet')
498 raise ValueError('This pull request was not merged yet')
499
499
500 commit_ids = list(pull_request.revisions)
500 commit_ids = list(pull_request.revisions)
501 if merge_rev not in commit_ids:
501 if merge_rev not in commit_ids:
502 commit_ids.append(merge_rev)
502 commit_ids.append(merge_rev)
503
503
504 return commit_ids
504 return commit_ids
505
505
506 def merge(self, pull_request, user, extras):
506 def merge(self, pull_request, user, extras):
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
507 log.debug("Merging pull request %s", pull_request.pull_request_id)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
508 merge_state = self._merge_pull_request(pull_request, user, extras)
509 if merge_state.executed:
509 if merge_state.executed:
510 log.debug(
510 log.debug(
511 "Merge was successful, updating the pull request comments.")
511 "Merge was successful, updating the pull request comments.")
512 self._comment_and_close_pr(pull_request, user, merge_state)
512 self._comment_and_close_pr(pull_request, user, merge_state)
513 self._log_action('user_merged_pull_request', user, pull_request)
513 self._log_action('user_merged_pull_request', user, pull_request)
514 else:
514 else:
515 log.warn("Merge failed, not updating the pull request.")
515 log.warn("Merge failed, not updating the pull request.")
516 return merge_state
516 return merge_state
517
517
518 def _merge_pull_request(self, pull_request, user, extras):
518 def _merge_pull_request(self, pull_request, user, extras):
519 target_vcs = pull_request.target_repo.scm_instance()
519 target_vcs = pull_request.target_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
520 source_vcs = pull_request.source_repo.scm_instance()
521 target_ref = self._refresh_reference(
521 target_ref = self._refresh_reference(
522 pull_request.target_ref_parts, target_vcs)
522 pull_request.target_ref_parts, target_vcs)
523
523
524 message = _(
524 message = _(
525 'Merge pull request #%(pr_id)s from '
525 'Merge pull request #%(pr_id)s from '
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
526 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
527 'pr_id': pull_request.pull_request_id,
527 'pr_id': pull_request.pull_request_id,
528 'source_repo': source_vcs.name,
528 'source_repo': source_vcs.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
529 'source_ref_name': pull_request.source_ref_parts.name,
530 'pr_title': pull_request.title
530 'pr_title': pull_request.title
531 }
531 }
532
532
533 workspace_id = self._workspace_id(pull_request)
533 workspace_id = self._workspace_id(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
534 use_rebase = self._use_rebase_for_merging(pull_request)
535
535
536 callback_daemon, extras = prepare_callback_daemon(
536 callback_daemon, extras = prepare_callback_daemon(
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
537 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
538 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
539
539
540 with callback_daemon:
540 with callback_daemon:
541 # TODO: johbo: Implement a clean way to run a config_override
541 # TODO: johbo: Implement a clean way to run a config_override
542 # for a single call.
542 # for a single call.
543 target_vcs.config.set(
543 target_vcs.config.set(
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
544 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
545 merge_state = target_vcs.merge(
545 merge_state = target_vcs.merge(
546 target_ref, source_vcs, pull_request.source_ref_parts,
546 target_ref, source_vcs, pull_request.source_ref_parts,
547 workspace_id, user_name=user.username,
547 workspace_id, user_name=user.username,
548 user_email=user.email, message=message, use_rebase=use_rebase)
548 user_email=user.email, message=message, use_rebase=use_rebase)
549 return merge_state
549 return merge_state
550
550
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
551 def _comment_and_close_pr(self, pull_request, user, merge_state):
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
552 pull_request.merge_rev = merge_state.merge_ref.commit_id
553 pull_request.updated_on = datetime.datetime.now()
553 pull_request.updated_on = datetime.datetime.now()
554
554
555 CommentsModel().create(
555 CommentsModel().create(
556 text=unicode(_('Pull request merged and closed')),
556 text=unicode(_('Pull request merged and closed')),
557 repo=pull_request.target_repo.repo_id,
557 repo=pull_request.target_repo.repo_id,
558 user=user.user_id,
558 user=user.user_id,
559 pull_request=pull_request.pull_request_id,
559 pull_request=pull_request.pull_request_id,
560 f_path=None,
560 f_path=None,
561 line_no=None,
561 line_no=None,
562 closing_pr=True
562 closing_pr=True
563 )
563 )
564
564
565 Session().add(pull_request)
565 Session().add(pull_request)
566 Session().flush()
566 Session().flush()
567 # TODO: paris: replace invalidation with less radical solution
567 # TODO: paris: replace invalidation with less radical solution
568 ScmModel().mark_for_invalidation(
568 ScmModel().mark_for_invalidation(
569 pull_request.target_repo.repo_name)
569 pull_request.target_repo.repo_name)
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
570 self._trigger_pull_request_hook(pull_request, user, 'merge')
571
571
572 def has_valid_update_type(self, pull_request):
572 def has_valid_update_type(self, pull_request):
573 source_ref_type = pull_request.source_ref_parts.type
573 source_ref_type = pull_request.source_ref_parts.type
574 return source_ref_type in ['book', 'branch', 'tag']
574 return source_ref_type in ['book', 'branch', 'tag']
575
575
576 def update_commits(self, pull_request):
576 def update_commits(self, pull_request):
577 """
577 """
578 Get the updated list of commits for the pull request
578 Get the updated list of commits for the pull request
579 and return the new pull request version and the list
579 and return the new pull request version and the list
580 of commits processed by this update action
580 of commits processed by this update action
581 """
581 """
582 pull_request = self.__get_pull_request(pull_request)
582 pull_request = self.__get_pull_request(pull_request)
583 source_ref_type = pull_request.source_ref_parts.type
583 source_ref_type = pull_request.source_ref_parts.type
584 source_ref_name = pull_request.source_ref_parts.name
584 source_ref_name = pull_request.source_ref_parts.name
585 source_ref_id = pull_request.source_ref_parts.commit_id
585 source_ref_id = pull_request.source_ref_parts.commit_id
586
586
587 if not self.has_valid_update_type(pull_request):
587 if not self.has_valid_update_type(pull_request):
588 log.debug(
588 log.debug(
589 "Skipping update of pull request %s due to ref type: %s",
589 "Skipping update of pull request %s due to ref type: %s",
590 pull_request, source_ref_type)
590 pull_request, source_ref_type)
591 return UpdateResponse(
591 return UpdateResponse(
592 executed=False,
592 executed=False,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
593 reason=UpdateFailureReason.WRONG_REF_TPYE,
594 old=pull_request, new=None, changes=None)
594 old=pull_request, new=None, changes=None)
595
595
596 source_repo = pull_request.source_repo.scm_instance()
596 source_repo = pull_request.source_repo.scm_instance()
597 try:
597 try:
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
598 source_commit = source_repo.get_commit(commit_id=source_ref_name)
599 except CommitDoesNotExistError:
599 except CommitDoesNotExistError:
600 return UpdateResponse(
600 return UpdateResponse(
601 executed=False,
601 executed=False,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
602 reason=UpdateFailureReason.MISSING_SOURCE_REF,
603 old=pull_request, new=None, changes=None)
603 old=pull_request, new=None, changes=None)
604
604
605 if source_ref_id == source_commit.raw_id:
605 if source_ref_id == source_commit.raw_id:
606 log.debug("Nothing changed in pull request %s", pull_request)
606 log.debug("Nothing changed in pull request %s", pull_request)
607 return UpdateResponse(
607 return UpdateResponse(
608 executed=False,
608 executed=False,
609 reason=UpdateFailureReason.NO_CHANGE,
609 reason=UpdateFailureReason.NO_CHANGE,
610 old=pull_request, new=None, changes=None)
610 old=pull_request, new=None, changes=None)
611
611
612 # Finally there is a need for an update
612 # Finally there is a need for an update
613 pull_request_version = self._create_version_from_snapshot(pull_request)
613 pull_request_version = self._create_version_from_snapshot(pull_request)
614 self._link_comments_to_version(pull_request_version)
614 self._link_comments_to_version(pull_request_version)
615
615
616 target_ref_type = pull_request.target_ref_parts.type
616 target_ref_type = pull_request.target_ref_parts.type
617 target_ref_name = pull_request.target_ref_parts.name
617 target_ref_name = pull_request.target_ref_parts.name
618 target_ref_id = pull_request.target_ref_parts.commit_id
618 target_ref_id = pull_request.target_ref_parts.commit_id
619 target_repo = pull_request.target_repo.scm_instance()
619 target_repo = pull_request.target_repo.scm_instance()
620
620
621 try:
621 try:
622 if target_ref_type in ('tag', 'branch', 'book'):
622 if target_ref_type in ('tag', 'branch', 'book'):
623 target_commit = target_repo.get_commit(target_ref_name)
623 target_commit = target_repo.get_commit(target_ref_name)
624 else:
624 else:
625 target_commit = target_repo.get_commit(target_ref_id)
625 target_commit = target_repo.get_commit(target_ref_id)
626 except CommitDoesNotExistError:
626 except CommitDoesNotExistError:
627 return UpdateResponse(
627 return UpdateResponse(
628 executed=False,
628 executed=False,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
629 reason=UpdateFailureReason.MISSING_TARGET_REF,
630 old=pull_request, new=None, changes=None)
630 old=pull_request, new=None, changes=None)
631
631
632 # re-compute commit ids
632 # re-compute commit ids
633 old_commit_ids = set(pull_request.revisions)
633 old_commit_ids = pull_request.revisions
634 pre_load = ["author", "branch", "date", "message"]
634 pre_load = ["author", "branch", "date", "message"]
635 commit_ranges = target_repo.compare(
635 commit_ranges = target_repo.compare(
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
636 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
637 pre_load=pre_load)
637 pre_load=pre_load)
638
638
639 ancestor = target_repo.get_common_ancestor(
639 ancestor = target_repo.get_common_ancestor(
640 target_commit.raw_id, source_commit.raw_id, source_repo)
640 target_commit.raw_id, source_commit.raw_id, source_repo)
641
641
642 pull_request.source_ref = '%s:%s:%s' % (
642 pull_request.source_ref = '%s:%s:%s' % (
643 source_ref_type, source_ref_name, source_commit.raw_id)
643 source_ref_type, source_ref_name, source_commit.raw_id)
644 pull_request.target_ref = '%s:%s:%s' % (
644 pull_request.target_ref = '%s:%s:%s' % (
645 target_ref_type, target_ref_name, ancestor)
645 target_ref_type, target_ref_name, ancestor)
646 pull_request.revisions = [
646 pull_request.revisions = [
647 commit.raw_id for commit in reversed(commit_ranges)]
647 commit.raw_id for commit in reversed(commit_ranges)]
648 pull_request.updated_on = datetime.datetime.now()
648 pull_request.updated_on = datetime.datetime.now()
649 Session().add(pull_request)
649 Session().add(pull_request)
650 new_commit_ids = set(pull_request.revisions)
650 new_commit_ids = pull_request.revisions
651
651
652 changes = self._calculate_commit_id_changes(
652 changes = self._calculate_commit_id_changes(
653 old_commit_ids, new_commit_ids)
653 old_commit_ids, new_commit_ids)
654
654
655 old_diff_data, new_diff_data = self._generate_update_diffs(
655 old_diff_data, new_diff_data = self._generate_update_diffs(
656 pull_request, pull_request_version)
656 pull_request, pull_request_version)
657
657
658 CommentsModel().outdate_comments(
658 CommentsModel().outdate_comments(
659 pull_request, old_diff_data=old_diff_data,
659 pull_request, old_diff_data=old_diff_data,
660 new_diff_data=new_diff_data)
660 new_diff_data=new_diff_data)
661
661
662 file_changes = self._calculate_file_changes(
662 file_changes = self._calculate_file_changes(
663 old_diff_data, new_diff_data)
663 old_diff_data, new_diff_data)
664
664
665 # Add an automatic comment to the pull request
665 # Add an automatic comment to the pull request
666 update_comment = CommentsModel().create(
666 update_comment = CommentsModel().create(
667 text=self._render_update_message(changes, file_changes),
667 text=self._render_update_message(changes, file_changes),
668 repo=pull_request.target_repo,
668 repo=pull_request.target_repo,
669 user=pull_request.author,
669 user=pull_request.author,
670 pull_request=pull_request,
670 pull_request=pull_request,
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
671 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
672
672
673 # Update status to "Under Review" for added commits
673 # Update status to "Under Review" for added commits
674 for commit_id in changes.added:
674 for commit_id in changes.added:
675 ChangesetStatusModel().set_status(
675 ChangesetStatusModel().set_status(
676 repo=pull_request.source_repo,
676 repo=pull_request.source_repo,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
677 status=ChangesetStatus.STATUS_UNDER_REVIEW,
678 comment=update_comment,
678 comment=update_comment,
679 user=pull_request.author,
679 user=pull_request.author,
680 pull_request=pull_request,
680 pull_request=pull_request,
681 revision=commit_id)
681 revision=commit_id)
682
682
683 log.debug(
683 log.debug(
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
684 'Updated pull request %s, added_ids: %s, common_ids: %s, '
685 'removed_ids: %s', pull_request.pull_request_id,
685 'removed_ids: %s', pull_request.pull_request_id,
686 changes.added, changes.common, changes.removed)
686 changes.added, changes.common, changes.removed)
687 log.debug('Updated pull request with the following file changes: %s',
687 log.debug('Updated pull request with the following file changes: %s',
688 file_changes)
688 file_changes)
689
689
690 log.info(
690 log.info(
691 "Updated pull request %s from commit %s to commit %s, "
691 "Updated pull request %s from commit %s to commit %s, "
692 "stored new version %s of this pull request.",
692 "stored new version %s of this pull request.",
693 pull_request.pull_request_id, source_ref_id,
693 pull_request.pull_request_id, source_ref_id,
694 pull_request.source_ref_parts.commit_id,
694 pull_request.source_ref_parts.commit_id,
695 pull_request_version.pull_request_version_id)
695 pull_request_version.pull_request_version_id)
696 Session().commit()
696 Session().commit()
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
697 self._trigger_pull_request_hook(pull_request, pull_request.author,
698 'update')
698 'update')
699
699
700 return UpdateResponse(
700 return UpdateResponse(
701 executed=True, reason=UpdateFailureReason.NONE,
701 executed=True, reason=UpdateFailureReason.NONE,
702 old=pull_request, new=pull_request_version, changes=changes)
702 old=pull_request, new=pull_request_version, changes=changes)
703
703
704 def _create_version_from_snapshot(self, pull_request):
704 def _create_version_from_snapshot(self, pull_request):
705 version = PullRequestVersion()
705 version = PullRequestVersion()
706 version.title = pull_request.title
706 version.title = pull_request.title
707 version.description = pull_request.description
707 version.description = pull_request.description
708 version.status = pull_request.status
708 version.status = pull_request.status
709 version.created_on = datetime.datetime.now()
709 version.created_on = datetime.datetime.now()
710 version.updated_on = pull_request.updated_on
710 version.updated_on = pull_request.updated_on
711 version.user_id = pull_request.user_id
711 version.user_id = pull_request.user_id
712 version.source_repo = pull_request.source_repo
712 version.source_repo = pull_request.source_repo
713 version.source_ref = pull_request.source_ref
713 version.source_ref = pull_request.source_ref
714 version.target_repo = pull_request.target_repo
714 version.target_repo = pull_request.target_repo
715 version.target_ref = pull_request.target_ref
715 version.target_ref = pull_request.target_ref
716
716
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
717 version._last_merge_source_rev = pull_request._last_merge_source_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
718 version._last_merge_target_rev = pull_request._last_merge_target_rev
719 version._last_merge_status = pull_request._last_merge_status
719 version._last_merge_status = pull_request._last_merge_status
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
720 version.shadow_merge_ref = pull_request.shadow_merge_ref
721 version.merge_rev = pull_request.merge_rev
721 version.merge_rev = pull_request.merge_rev
722
722
723 version.revisions = pull_request.revisions
723 version.revisions = pull_request.revisions
724 version.pull_request = pull_request
724 version.pull_request = pull_request
725 Session().add(version)
725 Session().add(version)
726 Session().flush()
726 Session().flush()
727
727
728 return version
728 return version
729
729
730 def _generate_update_diffs(self, pull_request, pull_request_version):
730 def _generate_update_diffs(self, pull_request, pull_request_version):
731
731
732 diff_context = (
732 diff_context = (
733 self.DIFF_CONTEXT +
733 self.DIFF_CONTEXT +
734 CommentsModel.needed_extra_diff_context())
734 CommentsModel.needed_extra_diff_context())
735
735
736 source_repo = pull_request_version.source_repo
736 source_repo = pull_request_version.source_repo
737 source_ref_id = pull_request_version.source_ref_parts.commit_id
737 source_ref_id = pull_request_version.source_ref_parts.commit_id
738 target_ref_id = pull_request_version.target_ref_parts.commit_id
738 target_ref_id = pull_request_version.target_ref_parts.commit_id
739 old_diff = self._get_diff_from_pr_or_version(
739 old_diff = self._get_diff_from_pr_or_version(
740 source_repo, source_ref_id, target_ref_id, context=diff_context)
740 source_repo, source_ref_id, target_ref_id, context=diff_context)
741
741
742 source_repo = pull_request.source_repo
742 source_repo = pull_request.source_repo
743 source_ref_id = pull_request.source_ref_parts.commit_id
743 source_ref_id = pull_request.source_ref_parts.commit_id
744 target_ref_id = pull_request.target_ref_parts.commit_id
744 target_ref_id = pull_request.target_ref_parts.commit_id
745
745
746 new_diff = self._get_diff_from_pr_or_version(
746 new_diff = self._get_diff_from_pr_or_version(
747 source_repo, source_ref_id, target_ref_id, context=diff_context)
747 source_repo, source_ref_id, target_ref_id, context=diff_context)
748
748
749 old_diff_data = diffs.DiffProcessor(old_diff)
749 old_diff_data = diffs.DiffProcessor(old_diff)
750 old_diff_data.prepare()
750 old_diff_data.prepare()
751 new_diff_data = diffs.DiffProcessor(new_diff)
751 new_diff_data = diffs.DiffProcessor(new_diff)
752 new_diff_data.prepare()
752 new_diff_data.prepare()
753
753
754 return old_diff_data, new_diff_data
754 return old_diff_data, new_diff_data
755
755
756 def _link_comments_to_version(self, pull_request_version):
756 def _link_comments_to_version(self, pull_request_version):
757 """
757 """
758 Link all unlinked comments of this pull request to the given version.
758 Link all unlinked comments of this pull request to the given version.
759
759
760 :param pull_request_version: The `PullRequestVersion` to which
760 :param pull_request_version: The `PullRequestVersion` to which
761 the comments shall be linked.
761 the comments shall be linked.
762
762
763 """
763 """
764 pull_request = pull_request_version.pull_request
764 pull_request = pull_request_version.pull_request
765 comments = ChangesetComment.query().filter(
765 comments = ChangesetComment.query().filter(
766 # TODO: johbo: Should we query for the repo at all here?
766 # TODO: johbo: Should we query for the repo at all here?
767 # Pending decision on how comments of PRs are to be related
767 # Pending decision on how comments of PRs are to be related
768 # to either the source repo, the target repo or no repo at all.
768 # to either the source repo, the target repo or no repo at all.
769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
769 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
770 ChangesetComment.pull_request == pull_request,
770 ChangesetComment.pull_request == pull_request,
771 ChangesetComment.pull_request_version == None)
771 ChangesetComment.pull_request_version == None)
772
772
773 # TODO: johbo: Find out why this breaks if it is done in a bulk
773 # TODO: johbo: Find out why this breaks if it is done in a bulk
774 # operation.
774 # operation.
775 for comment in comments:
775 for comment in comments:
776 comment.pull_request_version_id = (
776 comment.pull_request_version_id = (
777 pull_request_version.pull_request_version_id)
777 pull_request_version.pull_request_version_id)
778 Session().add(comment)
778 Session().add(comment)
779
779
780 def _calculate_commit_id_changes(self, old_ids, new_ids):
780 def _calculate_commit_id_changes(self, old_ids, new_ids):
781 added = [x for x in new_ids if x not in old_ids]
781 added = [x for x in new_ids if x not in old_ids]
782 common = [x for x in new_ids if x in old_ids]
782 common = [x for x in new_ids if x in old_ids]
783 removed = [x for x in old_ids if x not in new_ids]
783 removed = [x for x in old_ids if x not in new_ids]
784 total = new_ids
784 total = new_ids
785 return ChangeTuple(added, common, removed, total)
785 return ChangeTuple(added, common, removed, total)
786
786
787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
787 def _calculate_file_changes(self, old_diff_data, new_diff_data):
788
788
789 old_files = OrderedDict()
789 old_files = OrderedDict()
790 for diff_data in old_diff_data.parsed_diff:
790 for diff_data in old_diff_data.parsed_diff:
791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
791 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
792
792
793 added_files = []
793 added_files = []
794 modified_files = []
794 modified_files = []
795 removed_files = []
795 removed_files = []
796 for diff_data in new_diff_data.parsed_diff:
796 for diff_data in new_diff_data.parsed_diff:
797 new_filename = diff_data['filename']
797 new_filename = diff_data['filename']
798 new_hash = md5_safe(diff_data['raw_diff'])
798 new_hash = md5_safe(diff_data['raw_diff'])
799
799
800 old_hash = old_files.get(new_filename)
800 old_hash = old_files.get(new_filename)
801 if not old_hash:
801 if not old_hash:
802 # file is not present in old diff, means it's added
802 # file is not present in old diff, means it's added
803 added_files.append(new_filename)
803 added_files.append(new_filename)
804 else:
804 else:
805 if new_hash != old_hash:
805 if new_hash != old_hash:
806 modified_files.append(new_filename)
806 modified_files.append(new_filename)
807 # now remove a file from old, since we have seen it already
807 # now remove a file from old, since we have seen it already
808 del old_files[new_filename]
808 del old_files[new_filename]
809
809
810 # removed files is when there are present in old, but not in NEW,
810 # removed files is when there are present in old, but not in NEW,
811 # since we remove old files that are present in new diff, left-overs
811 # since we remove old files that are present in new diff, left-overs
812 # if any should be the removed files
812 # if any should be the removed files
813 removed_files.extend(old_files.keys())
813 removed_files.extend(old_files.keys())
814
814
815 return FileChangeTuple(added_files, modified_files, removed_files)
815 return FileChangeTuple(added_files, modified_files, removed_files)
816
816
817 def _render_update_message(self, changes, file_changes):
817 def _render_update_message(self, changes, file_changes):
818 """
818 """
819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
819 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
820 so it's always looking the same disregarding on which default
820 so it's always looking the same disregarding on which default
821 renderer system is using.
821 renderer system is using.
822
822
823 :param changes: changes named tuple
823 :param changes: changes named tuple
824 :param file_changes: file changes named tuple
824 :param file_changes: file changes named tuple
825
825
826 """
826 """
827 new_status = ChangesetStatus.get_status_lbl(
827 new_status = ChangesetStatus.get_status_lbl(
828 ChangesetStatus.STATUS_UNDER_REVIEW)
828 ChangesetStatus.STATUS_UNDER_REVIEW)
829
829
830 changed_files = (
830 changed_files = (
831 file_changes.added + file_changes.modified + file_changes.removed)
831 file_changes.added + file_changes.modified + file_changes.removed)
832
832
833 params = {
833 params = {
834 'under_review_label': new_status,
834 'under_review_label': new_status,
835 'added_commits': changes.added,
835 'added_commits': changes.added,
836 'removed_commits': changes.removed,
836 'removed_commits': changes.removed,
837 'changed_files': changed_files,
837 'changed_files': changed_files,
838 'added_files': file_changes.added,
838 'added_files': file_changes.added,
839 'modified_files': file_changes.modified,
839 'modified_files': file_changes.modified,
840 'removed_files': file_changes.removed,
840 'removed_files': file_changes.removed,
841 }
841 }
842 renderer = RstTemplateRenderer()
842 renderer = RstTemplateRenderer()
843 return renderer.render('pull_request_update.mako', **params)
843 return renderer.render('pull_request_update.mako', **params)
844
844
845 def edit(self, pull_request, title, description):
845 def edit(self, pull_request, title, description):
846 pull_request = self.__get_pull_request(pull_request)
846 pull_request = self.__get_pull_request(pull_request)
847 if pull_request.is_closed():
847 if pull_request.is_closed():
848 raise ValueError('This pull request is closed')
848 raise ValueError('This pull request is closed')
849 if title:
849 if title:
850 pull_request.title = title
850 pull_request.title = title
851 pull_request.description = description
851 pull_request.description = description
852 pull_request.updated_on = datetime.datetime.now()
852 pull_request.updated_on = datetime.datetime.now()
853 Session().add(pull_request)
853 Session().add(pull_request)
854
854
855 def update_reviewers(self, pull_request, reviewer_data):
855 def update_reviewers(self, pull_request, reviewer_data):
856 """
856 """
857 Update the reviewers in the pull request
857 Update the reviewers in the pull request
858
858
859 :param pull_request: the pr to update
859 :param pull_request: the pr to update
860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
860 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
861 """
861 """
862
862
863 reviewers_reasons = {}
863 reviewers_reasons = {}
864 for user_id, reasons in reviewer_data:
864 for user_id, reasons in reviewer_data:
865 if isinstance(user_id, (int, basestring)):
865 if isinstance(user_id, (int, basestring)):
866 user_id = self._get_user(user_id).user_id
866 user_id = self._get_user(user_id).user_id
867 reviewers_reasons[user_id] = reasons
867 reviewers_reasons[user_id] = reasons
868
868
869 reviewers_ids = set(reviewers_reasons.keys())
869 reviewers_ids = set(reviewers_reasons.keys())
870 pull_request = self.__get_pull_request(pull_request)
870 pull_request = self.__get_pull_request(pull_request)
871 current_reviewers = PullRequestReviewers.query()\
871 current_reviewers = PullRequestReviewers.query()\
872 .filter(PullRequestReviewers.pull_request ==
872 .filter(PullRequestReviewers.pull_request ==
873 pull_request).all()
873 pull_request).all()
874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
874 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
875
875
876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
876 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
877 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
878
878
879 log.debug("Adding %s reviewers", ids_to_add)
879 log.debug("Adding %s reviewers", ids_to_add)
880 log.debug("Removing %s reviewers", ids_to_remove)
880 log.debug("Removing %s reviewers", ids_to_remove)
881 changed = False
881 changed = False
882 for uid in ids_to_add:
882 for uid in ids_to_add:
883 changed = True
883 changed = True
884 _usr = self._get_user(uid)
884 _usr = self._get_user(uid)
885 reasons = reviewers_reasons[uid]
885 reasons = reviewers_reasons[uid]
886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
886 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
887 Session().add(reviewer)
887 Session().add(reviewer)
888
888
889 self.notify_reviewers(pull_request, ids_to_add)
889 self.notify_reviewers(pull_request, ids_to_add)
890
890
891 for uid in ids_to_remove:
891 for uid in ids_to_remove:
892 changed = True
892 changed = True
893 reviewer = PullRequestReviewers.query()\
893 reviewer = PullRequestReviewers.query()\
894 .filter(PullRequestReviewers.user_id == uid,
894 .filter(PullRequestReviewers.user_id == uid,
895 PullRequestReviewers.pull_request == pull_request)\
895 PullRequestReviewers.pull_request == pull_request)\
896 .scalar()
896 .scalar()
897 if reviewer:
897 if reviewer:
898 Session().delete(reviewer)
898 Session().delete(reviewer)
899 if changed:
899 if changed:
900 pull_request.updated_on = datetime.datetime.now()
900 pull_request.updated_on = datetime.datetime.now()
901 Session().add(pull_request)
901 Session().add(pull_request)
902
902
903 return ids_to_add, ids_to_remove
903 return ids_to_add, ids_to_remove
904
904
905 def get_url(self, pull_request):
905 def get_url(self, pull_request):
906 return h.url('pullrequest_show',
906 return h.url('pullrequest_show',
907 repo_name=safe_str(pull_request.target_repo.repo_name),
907 repo_name=safe_str(pull_request.target_repo.repo_name),
908 pull_request_id=pull_request.pull_request_id,
908 pull_request_id=pull_request.pull_request_id,
909 qualified=True)
909 qualified=True)
910
910
911 def get_shadow_clone_url(self, pull_request):
911 def get_shadow_clone_url(self, pull_request):
912 """
912 """
913 Returns qualified url pointing to the shadow repository. If this pull
913 Returns qualified url pointing to the shadow repository. If this pull
914 request is closed there is no shadow repository and ``None`` will be
914 request is closed there is no shadow repository and ``None`` will be
915 returned.
915 returned.
916 """
916 """
917 if pull_request.is_closed():
917 if pull_request.is_closed():
918 return None
918 return None
919 else:
919 else:
920 pr_url = urllib.unquote(self.get_url(pull_request))
920 pr_url = urllib.unquote(self.get_url(pull_request))
921 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
921 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
922
922
923 def notify_reviewers(self, pull_request, reviewers_ids):
923 def notify_reviewers(self, pull_request, reviewers_ids):
924 # notification to reviewers
924 # notification to reviewers
925 if not reviewers_ids:
925 if not reviewers_ids:
926 return
926 return
927
927
928 pull_request_obj = pull_request
928 pull_request_obj = pull_request
929 # get the current participants of this pull request
929 # get the current participants of this pull request
930 recipients = reviewers_ids
930 recipients = reviewers_ids
931 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
931 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
932
932
933 pr_source_repo = pull_request_obj.source_repo
933 pr_source_repo = pull_request_obj.source_repo
934 pr_target_repo = pull_request_obj.target_repo
934 pr_target_repo = pull_request_obj.target_repo
935
935
936 pr_url = h.url(
936 pr_url = h.url(
937 'pullrequest_show',
937 'pullrequest_show',
938 repo_name=pr_target_repo.repo_name,
938 repo_name=pr_target_repo.repo_name,
939 pull_request_id=pull_request_obj.pull_request_id,
939 pull_request_id=pull_request_obj.pull_request_id,
940 qualified=True,)
940 qualified=True,)
941
941
942 # set some variables for email notification
942 # set some variables for email notification
943 pr_target_repo_url = h.url(
943 pr_target_repo_url = h.url(
944 'summary_home',
944 'summary_home',
945 repo_name=pr_target_repo.repo_name,
945 repo_name=pr_target_repo.repo_name,
946 qualified=True)
946 qualified=True)
947
947
948 pr_source_repo_url = h.url(
948 pr_source_repo_url = h.url(
949 'summary_home',
949 'summary_home',
950 repo_name=pr_source_repo.repo_name,
950 repo_name=pr_source_repo.repo_name,
951 qualified=True)
951 qualified=True)
952
952
953 # pull request specifics
953 # pull request specifics
954 pull_request_commits = [
954 pull_request_commits = [
955 (x.raw_id, x.message)
955 (x.raw_id, x.message)
956 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
956 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
957
957
958 kwargs = {
958 kwargs = {
959 'user': pull_request.author,
959 'user': pull_request.author,
960 'pull_request': pull_request_obj,
960 'pull_request': pull_request_obj,
961 'pull_request_commits': pull_request_commits,
961 'pull_request_commits': pull_request_commits,
962
962
963 'pull_request_target_repo': pr_target_repo,
963 'pull_request_target_repo': pr_target_repo,
964 'pull_request_target_repo_url': pr_target_repo_url,
964 'pull_request_target_repo_url': pr_target_repo_url,
965
965
966 'pull_request_source_repo': pr_source_repo,
966 'pull_request_source_repo': pr_source_repo,
967 'pull_request_source_repo_url': pr_source_repo_url,
967 'pull_request_source_repo_url': pr_source_repo_url,
968
968
969 'pull_request_url': pr_url,
969 'pull_request_url': pr_url,
970 }
970 }
971
971
972 # pre-generate the subject for notification itself
972 # pre-generate the subject for notification itself
973 (subject,
973 (subject,
974 _h, _e, # we don't care about those
974 _h, _e, # we don't care about those
975 body_plaintext) = EmailNotificationModel().render_email(
975 body_plaintext) = EmailNotificationModel().render_email(
976 notification_type, **kwargs)
976 notification_type, **kwargs)
977
977
978 # create notification objects, and emails
978 # create notification objects, and emails
979 NotificationModel().create(
979 NotificationModel().create(
980 created_by=pull_request.author,
980 created_by=pull_request.author,
981 notification_subject=subject,
981 notification_subject=subject,
982 notification_body=body_plaintext,
982 notification_body=body_plaintext,
983 notification_type=notification_type,
983 notification_type=notification_type,
984 recipients=recipients,
984 recipients=recipients,
985 email_kwargs=kwargs,
985 email_kwargs=kwargs,
986 )
986 )
987
987
988 def delete(self, pull_request):
988 def delete(self, pull_request):
989 pull_request = self.__get_pull_request(pull_request)
989 pull_request = self.__get_pull_request(pull_request)
990 self._cleanup_merge_workspace(pull_request)
990 self._cleanup_merge_workspace(pull_request)
991 Session().delete(pull_request)
991 Session().delete(pull_request)
992
992
993 def close_pull_request(self, pull_request, user):
993 def close_pull_request(self, pull_request, user):
994 pull_request = self.__get_pull_request(pull_request)
994 pull_request = self.__get_pull_request(pull_request)
995 self._cleanup_merge_workspace(pull_request)
995 self._cleanup_merge_workspace(pull_request)
996 pull_request.status = PullRequest.STATUS_CLOSED
996 pull_request.status = PullRequest.STATUS_CLOSED
997 pull_request.updated_on = datetime.datetime.now()
997 pull_request.updated_on = datetime.datetime.now()
998 Session().add(pull_request)
998 Session().add(pull_request)
999 self._trigger_pull_request_hook(
999 self._trigger_pull_request_hook(
1000 pull_request, pull_request.author, 'close')
1000 pull_request, pull_request.author, 'close')
1001 self._log_action('user_closed_pull_request', user, pull_request)
1001 self._log_action('user_closed_pull_request', user, pull_request)
1002
1002
1003 def close_pull_request_with_comment(self, pull_request, user, repo,
1003 def close_pull_request_with_comment(self, pull_request, user, repo,
1004 message=None):
1004 message=None):
1005 status = ChangesetStatus.STATUS_REJECTED
1005 status = ChangesetStatus.STATUS_REJECTED
1006
1006
1007 if not message:
1007 if not message:
1008 message = (
1008 message = (
1009 _('Status change %(transition_icon)s %(status)s') % {
1009 _('Status change %(transition_icon)s %(status)s') % {
1010 'transition_icon': '>',
1010 'transition_icon': '>',
1011 'status': ChangesetStatus.get_status_lbl(status)})
1011 'status': ChangesetStatus.get_status_lbl(status)})
1012
1012
1013 internal_message = _('Closing with') + ' ' + message
1013 internal_message = _('Closing with') + ' ' + message
1014
1014
1015 comm = CommentsModel().create(
1015 comm = CommentsModel().create(
1016 text=internal_message,
1016 text=internal_message,
1017 repo=repo.repo_id,
1017 repo=repo.repo_id,
1018 user=user.user_id,
1018 user=user.user_id,
1019 pull_request=pull_request.pull_request_id,
1019 pull_request=pull_request.pull_request_id,
1020 f_path=None,
1020 f_path=None,
1021 line_no=None,
1021 line_no=None,
1022 status_change=ChangesetStatus.get_status_lbl(status),
1022 status_change=ChangesetStatus.get_status_lbl(status),
1023 status_change_type=status,
1023 status_change_type=status,
1024 closing_pr=True
1024 closing_pr=True
1025 )
1025 )
1026
1026
1027 ChangesetStatusModel().set_status(
1027 ChangesetStatusModel().set_status(
1028 repo.repo_id,
1028 repo.repo_id,
1029 status,
1029 status,
1030 user.user_id,
1030 user.user_id,
1031 comm,
1031 comm,
1032 pull_request=pull_request.pull_request_id
1032 pull_request=pull_request.pull_request_id
1033 )
1033 )
1034 Session().flush()
1034 Session().flush()
1035
1035
1036 PullRequestModel().close_pull_request(
1036 PullRequestModel().close_pull_request(
1037 pull_request.pull_request_id, user)
1037 pull_request.pull_request_id, user)
1038
1038
1039 def merge_status(self, pull_request):
1039 def merge_status(self, pull_request):
1040 if not self._is_merge_enabled(pull_request):
1040 if not self._is_merge_enabled(pull_request):
1041 return False, _('Server-side pull request merging is disabled.')
1041 return False, _('Server-side pull request merging is disabled.')
1042 if pull_request.is_closed():
1042 if pull_request.is_closed():
1043 return False, _('This pull request is closed.')
1043 return False, _('This pull request is closed.')
1044 merge_possible, msg = self._check_repo_requirements(
1044 merge_possible, msg = self._check_repo_requirements(
1045 target=pull_request.target_repo, source=pull_request.source_repo)
1045 target=pull_request.target_repo, source=pull_request.source_repo)
1046 if not merge_possible:
1046 if not merge_possible:
1047 return merge_possible, msg
1047 return merge_possible, msg
1048
1048
1049 try:
1049 try:
1050 resp = self._try_merge(pull_request)
1050 resp = self._try_merge(pull_request)
1051 log.debug("Merge response: %s", resp)
1051 log.debug("Merge response: %s", resp)
1052 status = resp.possible, self.merge_status_message(
1052 status = resp.possible, self.merge_status_message(
1053 resp.failure_reason)
1053 resp.failure_reason)
1054 except NotImplementedError:
1054 except NotImplementedError:
1055 status = False, _('Pull request merging is not supported.')
1055 status = False, _('Pull request merging is not supported.')
1056
1056
1057 return status
1057 return status
1058
1058
1059 def _check_repo_requirements(self, target, source):
1059 def _check_repo_requirements(self, target, source):
1060 """
1060 """
1061 Check if `target` and `source` have compatible requirements.
1061 Check if `target` and `source` have compatible requirements.
1062
1062
1063 Currently this is just checking for largefiles.
1063 Currently this is just checking for largefiles.
1064 """
1064 """
1065 target_has_largefiles = self._has_largefiles(target)
1065 target_has_largefiles = self._has_largefiles(target)
1066 source_has_largefiles = self._has_largefiles(source)
1066 source_has_largefiles = self._has_largefiles(source)
1067 merge_possible = True
1067 merge_possible = True
1068 message = u''
1068 message = u''
1069
1069
1070 if target_has_largefiles != source_has_largefiles:
1070 if target_has_largefiles != source_has_largefiles:
1071 merge_possible = False
1071 merge_possible = False
1072 if source_has_largefiles:
1072 if source_has_largefiles:
1073 message = _(
1073 message = _(
1074 'Target repository large files support is disabled.')
1074 'Target repository large files support is disabled.')
1075 else:
1075 else:
1076 message = _(
1076 message = _(
1077 'Source repository large files support is disabled.')
1077 'Source repository large files support is disabled.')
1078
1078
1079 return merge_possible, message
1079 return merge_possible, message
1080
1080
1081 def _has_largefiles(self, repo):
1081 def _has_largefiles(self, repo):
1082 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1082 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1083 'extensions', 'largefiles')
1083 'extensions', 'largefiles')
1084 return largefiles_ui and largefiles_ui[0].active
1084 return largefiles_ui and largefiles_ui[0].active
1085
1085
1086 def _try_merge(self, pull_request):
1086 def _try_merge(self, pull_request):
1087 """
1087 """
1088 Try to merge the pull request and return the merge status.
1088 Try to merge the pull request and return the merge status.
1089 """
1089 """
1090 log.debug(
1090 log.debug(
1091 "Trying out if the pull request %s can be merged.",
1091 "Trying out if the pull request %s can be merged.",
1092 pull_request.pull_request_id)
1092 pull_request.pull_request_id)
1093 target_vcs = pull_request.target_repo.scm_instance()
1093 target_vcs = pull_request.target_repo.scm_instance()
1094
1094
1095 # Refresh the target reference.
1095 # Refresh the target reference.
1096 try:
1096 try:
1097 target_ref = self._refresh_reference(
1097 target_ref = self._refresh_reference(
1098 pull_request.target_ref_parts, target_vcs)
1098 pull_request.target_ref_parts, target_vcs)
1099 except CommitDoesNotExistError:
1099 except CommitDoesNotExistError:
1100 merge_state = MergeResponse(
1100 merge_state = MergeResponse(
1101 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1101 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1102 return merge_state
1102 return merge_state
1103
1103
1104 target_locked = pull_request.target_repo.locked
1104 target_locked = pull_request.target_repo.locked
1105 if target_locked and target_locked[0]:
1105 if target_locked and target_locked[0]:
1106 log.debug("The target repository is locked.")
1106 log.debug("The target repository is locked.")
1107 merge_state = MergeResponse(
1107 merge_state = MergeResponse(
1108 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1108 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1109 elif self._needs_merge_state_refresh(pull_request, target_ref):
1109 elif self._needs_merge_state_refresh(pull_request, target_ref):
1110 log.debug("Refreshing the merge status of the repository.")
1110 log.debug("Refreshing the merge status of the repository.")
1111 merge_state = self._refresh_merge_state(
1111 merge_state = self._refresh_merge_state(
1112 pull_request, target_vcs, target_ref)
1112 pull_request, target_vcs, target_ref)
1113 else:
1113 else:
1114 possible = pull_request.\
1114 possible = pull_request.\
1115 _last_merge_status == MergeFailureReason.NONE
1115 _last_merge_status == MergeFailureReason.NONE
1116 merge_state = MergeResponse(
1116 merge_state = MergeResponse(
1117 possible, False, None, pull_request._last_merge_status)
1117 possible, False, None, pull_request._last_merge_status)
1118
1118
1119 return merge_state
1119 return merge_state
1120
1120
1121 def _refresh_reference(self, reference, vcs_repository):
1121 def _refresh_reference(self, reference, vcs_repository):
1122 if reference.type in ('branch', 'book'):
1122 if reference.type in ('branch', 'book'):
1123 name_or_id = reference.name
1123 name_or_id = reference.name
1124 else:
1124 else:
1125 name_or_id = reference.commit_id
1125 name_or_id = reference.commit_id
1126 refreshed_commit = vcs_repository.get_commit(name_or_id)
1126 refreshed_commit = vcs_repository.get_commit(name_or_id)
1127 refreshed_reference = Reference(
1127 refreshed_reference = Reference(
1128 reference.type, reference.name, refreshed_commit.raw_id)
1128 reference.type, reference.name, refreshed_commit.raw_id)
1129 return refreshed_reference
1129 return refreshed_reference
1130
1130
1131 def _needs_merge_state_refresh(self, pull_request, target_reference):
1131 def _needs_merge_state_refresh(self, pull_request, target_reference):
1132 return not(
1132 return not(
1133 pull_request.revisions and
1133 pull_request.revisions and
1134 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1134 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1135 target_reference.commit_id == pull_request._last_merge_target_rev)
1135 target_reference.commit_id == pull_request._last_merge_target_rev)
1136
1136
1137 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1137 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1138 workspace_id = self._workspace_id(pull_request)
1138 workspace_id = self._workspace_id(pull_request)
1139 source_vcs = pull_request.source_repo.scm_instance()
1139 source_vcs = pull_request.source_repo.scm_instance()
1140 use_rebase = self._use_rebase_for_merging(pull_request)
1140 use_rebase = self._use_rebase_for_merging(pull_request)
1141 merge_state = target_vcs.merge(
1141 merge_state = target_vcs.merge(
1142 target_reference, source_vcs, pull_request.source_ref_parts,
1142 target_reference, source_vcs, pull_request.source_ref_parts,
1143 workspace_id, dry_run=True, use_rebase=use_rebase)
1143 workspace_id, dry_run=True, use_rebase=use_rebase)
1144
1144
1145 # Do not store the response if there was an unknown error.
1145 # Do not store the response if there was an unknown error.
1146 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1146 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1147 pull_request._last_merge_source_rev = \
1147 pull_request._last_merge_source_rev = \
1148 pull_request.source_ref_parts.commit_id
1148 pull_request.source_ref_parts.commit_id
1149 pull_request._last_merge_target_rev = target_reference.commit_id
1149 pull_request._last_merge_target_rev = target_reference.commit_id
1150 pull_request._last_merge_status = merge_state.failure_reason
1150 pull_request._last_merge_status = merge_state.failure_reason
1151 pull_request.shadow_merge_ref = merge_state.merge_ref
1151 pull_request.shadow_merge_ref = merge_state.merge_ref
1152 Session().add(pull_request)
1152 Session().add(pull_request)
1153 Session().commit()
1153 Session().commit()
1154
1154
1155 return merge_state
1155 return merge_state
1156
1156
1157 def _workspace_id(self, pull_request):
1157 def _workspace_id(self, pull_request):
1158 workspace_id = 'pr-%s' % pull_request.pull_request_id
1158 workspace_id = 'pr-%s' % pull_request.pull_request_id
1159 return workspace_id
1159 return workspace_id
1160
1160
1161 def merge_status_message(self, status_code):
1161 def merge_status_message(self, status_code):
1162 """
1162 """
1163 Return a human friendly error message for the given merge status code.
1163 Return a human friendly error message for the given merge status code.
1164 """
1164 """
1165 return self.MERGE_STATUS_MESSAGES[status_code]
1165 return self.MERGE_STATUS_MESSAGES[status_code]
1166
1166
1167 def generate_repo_data(self, repo, commit_id=None, branch=None,
1167 def generate_repo_data(self, repo, commit_id=None, branch=None,
1168 bookmark=None):
1168 bookmark=None):
1169 all_refs, selected_ref = \
1169 all_refs, selected_ref = \
1170 self._get_repo_pullrequest_sources(
1170 self._get_repo_pullrequest_sources(
1171 repo.scm_instance(), commit_id=commit_id,
1171 repo.scm_instance(), commit_id=commit_id,
1172 branch=branch, bookmark=bookmark)
1172 branch=branch, bookmark=bookmark)
1173
1173
1174 refs_select2 = []
1174 refs_select2 = []
1175 for element in all_refs:
1175 for element in all_refs:
1176 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1176 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1177 refs_select2.append({'text': element[1], 'children': children})
1177 refs_select2.append({'text': element[1], 'children': children})
1178
1178
1179 return {
1179 return {
1180 'user': {
1180 'user': {
1181 'user_id': repo.user.user_id,
1181 'user_id': repo.user.user_id,
1182 'username': repo.user.username,
1182 'username': repo.user.username,
1183 'firstname': repo.user.firstname,
1183 'firstname': repo.user.firstname,
1184 'lastname': repo.user.lastname,
1184 'lastname': repo.user.lastname,
1185 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1185 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1186 },
1186 },
1187 'description': h.chop_at_smart(repo.description, '\n'),
1187 'description': h.chop_at_smart(repo.description, '\n'),
1188 'refs': {
1188 'refs': {
1189 'all_refs': all_refs,
1189 'all_refs': all_refs,
1190 'selected_ref': selected_ref,
1190 'selected_ref': selected_ref,
1191 'select2_refs': refs_select2
1191 'select2_refs': refs_select2
1192 }
1192 }
1193 }
1193 }
1194
1194
1195 def generate_pullrequest_title(self, source, source_ref, target):
1195 def generate_pullrequest_title(self, source, source_ref, target):
1196 return u'{source}#{at_ref} to {target}'.format(
1196 return u'{source}#{at_ref} to {target}'.format(
1197 source=source,
1197 source=source,
1198 at_ref=source_ref,
1198 at_ref=source_ref,
1199 target=target,
1199 target=target,
1200 )
1200 )
1201
1201
1202 def _cleanup_merge_workspace(self, pull_request):
1202 def _cleanup_merge_workspace(self, pull_request):
1203 # Merging related cleanup
1203 # Merging related cleanup
1204 target_scm = pull_request.target_repo.scm_instance()
1204 target_scm = pull_request.target_repo.scm_instance()
1205 workspace_id = 'pr-%s' % pull_request.pull_request_id
1205 workspace_id = 'pr-%s' % pull_request.pull_request_id
1206
1206
1207 try:
1207 try:
1208 target_scm.cleanup_merge_workspace(workspace_id)
1208 target_scm.cleanup_merge_workspace(workspace_id)
1209 except NotImplementedError:
1209 except NotImplementedError:
1210 pass
1210 pass
1211
1211
1212 def _get_repo_pullrequest_sources(
1212 def _get_repo_pullrequest_sources(
1213 self, repo, commit_id=None, branch=None, bookmark=None):
1213 self, repo, commit_id=None, branch=None, bookmark=None):
1214 """
1214 """
1215 Return a structure with repo's interesting commits, suitable for
1215 Return a structure with repo's interesting commits, suitable for
1216 the selectors in pullrequest controller
1216 the selectors in pullrequest controller
1217
1217
1218 :param commit_id: a commit that must be in the list somehow
1218 :param commit_id: a commit that must be in the list somehow
1219 and selected by default
1219 and selected by default
1220 :param branch: a branch that must be in the list and selected
1220 :param branch: a branch that must be in the list and selected
1221 by default - even if closed
1221 by default - even if closed
1222 :param bookmark: a bookmark that must be in the list and selected
1222 :param bookmark: a bookmark that must be in the list and selected
1223 """
1223 """
1224
1224
1225 commit_id = safe_str(commit_id) if commit_id else None
1225 commit_id = safe_str(commit_id) if commit_id else None
1226 branch = safe_str(branch) if branch else None
1226 branch = safe_str(branch) if branch else None
1227 bookmark = safe_str(bookmark) if bookmark else None
1227 bookmark = safe_str(bookmark) if bookmark else None
1228
1228
1229 selected = None
1229 selected = None
1230
1230
1231 # order matters: first source that has commit_id in it will be selected
1231 # order matters: first source that has commit_id in it will be selected
1232 sources = []
1232 sources = []
1233 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1233 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1234 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1234 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1235
1235
1236 if commit_id:
1236 if commit_id:
1237 ref_commit = (h.short_id(commit_id), commit_id)
1237 ref_commit = (h.short_id(commit_id), commit_id)
1238 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1238 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1239
1239
1240 sources.append(
1240 sources.append(
1241 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1241 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1242 )
1242 )
1243
1243
1244 groups = []
1244 groups = []
1245 for group_key, ref_list, group_name, match in sources:
1245 for group_key, ref_list, group_name, match in sources:
1246 group_refs = []
1246 group_refs = []
1247 for ref_name, ref_id in ref_list:
1247 for ref_name, ref_id in ref_list:
1248 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1248 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1249 group_refs.append((ref_key, ref_name))
1249 group_refs.append((ref_key, ref_name))
1250
1250
1251 if not selected:
1251 if not selected:
1252 if set([commit_id, match]) & set([ref_id, ref_name]):
1252 if set([commit_id, match]) & set([ref_id, ref_name]):
1253 selected = ref_key
1253 selected = ref_key
1254
1254
1255 if group_refs:
1255 if group_refs:
1256 groups.append((group_refs, group_name))
1256 groups.append((group_refs, group_name))
1257
1257
1258 if not selected:
1258 if not selected:
1259 ref = commit_id or branch or bookmark
1259 ref = commit_id or branch or bookmark
1260 if ref:
1260 if ref:
1261 raise CommitDoesNotExistError(
1261 raise CommitDoesNotExistError(
1262 'No commit refs could be found matching: %s' % ref)
1262 'No commit refs could be found matching: %s' % ref)
1263 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1263 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1264 selected = 'branch:%s:%s' % (
1264 selected = 'branch:%s:%s' % (
1265 repo.DEFAULT_BRANCH_NAME,
1265 repo.DEFAULT_BRANCH_NAME,
1266 repo.branches[repo.DEFAULT_BRANCH_NAME]
1266 repo.branches[repo.DEFAULT_BRANCH_NAME]
1267 )
1267 )
1268 elif repo.commit_ids:
1268 elif repo.commit_ids:
1269 rev = repo.commit_ids[0]
1269 rev = repo.commit_ids[0]
1270 selected = 'rev:%s:%s' % (rev, rev)
1270 selected = 'rev:%s:%s' % (rev, rev)
1271 else:
1271 else:
1272 raise EmptyRepositoryError()
1272 raise EmptyRepositoryError()
1273 return groups, selected
1273 return groups, selected
1274
1274
1275 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1275 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1276 return self._get_diff_from_pr_or_version(
1276 return self._get_diff_from_pr_or_version(
1277 source_repo, source_ref_id, target_ref_id, context=context)
1277 source_repo, source_ref_id, target_ref_id, context=context)
1278
1278
1279 def _get_diff_from_pr_or_version(
1279 def _get_diff_from_pr_or_version(
1280 self, source_repo, source_ref_id, target_ref_id, context):
1280 self, source_repo, source_ref_id, target_ref_id, context):
1281 target_commit = source_repo.get_commit(
1281 target_commit = source_repo.get_commit(
1282 commit_id=safe_str(target_ref_id))
1282 commit_id=safe_str(target_ref_id))
1283 source_commit = source_repo.get_commit(
1283 source_commit = source_repo.get_commit(
1284 commit_id=safe_str(source_ref_id))
1284 commit_id=safe_str(source_ref_id))
1285 if isinstance(source_repo, Repository):
1285 if isinstance(source_repo, Repository):
1286 vcs_repo = source_repo.scm_instance()
1286 vcs_repo = source_repo.scm_instance()
1287 else:
1287 else:
1288 vcs_repo = source_repo
1288 vcs_repo = source_repo
1289
1289
1290 # TODO: johbo: In the context of an update, we cannot reach
1290 # TODO: johbo: In the context of an update, we cannot reach
1291 # the old commit anymore with our normal mechanisms. It needs
1291 # the old commit anymore with our normal mechanisms. It needs
1292 # some sort of special support in the vcs layer to avoid this
1292 # some sort of special support in the vcs layer to avoid this
1293 # workaround.
1293 # workaround.
1294 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1294 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1295 vcs_repo.alias == 'git'):
1295 vcs_repo.alias == 'git'):
1296 source_commit.raw_id = safe_str(source_ref_id)
1296 source_commit.raw_id = safe_str(source_ref_id)
1297
1297
1298 log.debug('calculating diff between '
1298 log.debug('calculating diff between '
1299 'source_ref:%s and target_ref:%s for repo `%s`',
1299 'source_ref:%s and target_ref:%s for repo `%s`',
1300 target_ref_id, source_ref_id,
1300 target_ref_id, source_ref_id,
1301 safe_unicode(vcs_repo.path))
1301 safe_unicode(vcs_repo.path))
1302
1302
1303 vcs_diff = vcs_repo.get_diff(
1303 vcs_diff = vcs_repo.get_diff(
1304 commit1=target_commit, commit2=source_commit, context=context)
1304 commit1=target_commit, commit2=source_commit, context=context)
1305 return vcs_diff
1305 return vcs_diff
1306
1306
1307 def _is_merge_enabled(self, pull_request):
1307 def _is_merge_enabled(self, pull_request):
1308 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1308 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1309 settings = settings_model.get_general_settings()
1309 settings = settings_model.get_general_settings()
1310 return settings.get('rhodecode_pr_merge_enabled', False)
1310 return settings.get('rhodecode_pr_merge_enabled', False)
1311
1311
1312 def _use_rebase_for_merging(self, pull_request):
1312 def _use_rebase_for_merging(self, pull_request):
1313 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1313 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1314 settings = settings_model.get_general_settings()
1314 settings = settings_model.get_general_settings()
1315 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1315 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1316
1316
1317 def _log_action(self, action, user, pull_request):
1317 def _log_action(self, action, user, pull_request):
1318 action_logger(
1318 action_logger(
1319 user,
1319 user,
1320 '{action}:{pr_id}'.format(
1320 '{action}:{pr_id}'.format(
1321 action=action, pr_id=pull_request.pull_request_id),
1321 action=action, pr_id=pull_request.pull_request_id),
1322 pull_request.target_repo)
1322 pull_request.target_repo)
1323
1323
1324
1324
1325 class MergeCheck(object):
1325 class MergeCheck(object):
1326 """
1326 """
1327 Perform Merge Checks and returns a check object which stores information
1327 Perform Merge Checks and returns a check object which stores information
1328 about merge errors, and merge conditions
1328 about merge errors, and merge conditions
1329 """
1329 """
1330 TODO_CHECK = 'todo'
1330 TODO_CHECK = 'todo'
1331 PERM_CHECK = 'perm'
1331 PERM_CHECK = 'perm'
1332 REVIEW_CHECK = 'review'
1332 REVIEW_CHECK = 'review'
1333 MERGE_CHECK = 'merge'
1333 MERGE_CHECK = 'merge'
1334
1334
1335 def __init__(self):
1335 def __init__(self):
1336 self.merge_possible = None
1336 self.merge_possible = None
1337 self.merge_msg = ''
1337 self.merge_msg = ''
1338 self.failed = None
1338 self.failed = None
1339 self.errors = []
1339 self.errors = []
1340 self.error_details = OrderedDict()
1340 self.error_details = OrderedDict()
1341
1341
1342 def push_error(self, error_type, message, error_key, details):
1342 def push_error(self, error_type, message, error_key, details):
1343 self.failed = True
1343 self.failed = True
1344 self.errors.append([error_type, message])
1344 self.errors.append([error_type, message])
1345 self.error_details[error_key] = dict(
1345 self.error_details[error_key] = dict(
1346 details=details,
1346 details=details,
1347 error_type=error_type,
1347 error_type=error_type,
1348 message=message
1348 message=message
1349 )
1349 )
1350
1350
1351 @classmethod
1351 @classmethod
1352 def validate(cls, pull_request, user, fail_early=False, translator=None):
1352 def validate(cls, pull_request, user, fail_early=False, translator=None):
1353 # if migrated to pyramid...
1353 # if migrated to pyramid...
1354 # _ = lambda: translator or _ # use passed in translator if any
1354 # _ = lambda: translator or _ # use passed in translator if any
1355
1355
1356 merge_check = cls()
1356 merge_check = cls()
1357
1357
1358 # permissions
1358 # permissions
1359 user_allowed_to_merge = PullRequestModel().check_user_merge(
1359 user_allowed_to_merge = PullRequestModel().check_user_merge(
1360 pull_request, user)
1360 pull_request, user)
1361 if not user_allowed_to_merge:
1361 if not user_allowed_to_merge:
1362 log.debug("MergeCheck: cannot merge, approval is pending.")
1362 log.debug("MergeCheck: cannot merge, approval is pending.")
1363
1363
1364 msg = _('User `{}` not allowed to perform merge').format(user)
1364 msg = _('User `{}` not allowed to perform merge').format(user)
1365 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1365 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1366 if fail_early:
1366 if fail_early:
1367 return merge_check
1367 return merge_check
1368
1368
1369 # review status
1369 # review status
1370 review_status = pull_request.calculated_review_status()
1370 review_status = pull_request.calculated_review_status()
1371 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1371 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1372 if not status_approved:
1372 if not status_approved:
1373 log.debug("MergeCheck: cannot merge, approval is pending.")
1373 log.debug("MergeCheck: cannot merge, approval is pending.")
1374
1374
1375 msg = _('Pull request reviewer approval is pending.')
1375 msg = _('Pull request reviewer approval is pending.')
1376
1376
1377 merge_check.push_error(
1377 merge_check.push_error(
1378 'warning', msg, cls.REVIEW_CHECK, review_status)
1378 'warning', msg, cls.REVIEW_CHECK, review_status)
1379
1379
1380 if fail_early:
1380 if fail_early:
1381 return merge_check
1381 return merge_check
1382
1382
1383 # left over TODOs
1383 # left over TODOs
1384 todos = CommentsModel().get_unresolved_todos(pull_request)
1384 todos = CommentsModel().get_unresolved_todos(pull_request)
1385 if todos:
1385 if todos:
1386 log.debug("MergeCheck: cannot merge, {} "
1386 log.debug("MergeCheck: cannot merge, {} "
1387 "unresolved todos left.".format(len(todos)))
1387 "unresolved todos left.".format(len(todos)))
1388
1388
1389 if len(todos) == 1:
1389 if len(todos) == 1:
1390 msg = _('Cannot merge, {} TODO still not resolved.').format(
1390 msg = _('Cannot merge, {} TODO still not resolved.').format(
1391 len(todos))
1391 len(todos))
1392 else:
1392 else:
1393 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1393 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1394 len(todos))
1394 len(todos))
1395
1395
1396 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1396 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1397
1397
1398 if fail_early:
1398 if fail_early:
1399 return merge_check
1399 return merge_check
1400
1400
1401 # merge possible
1401 # merge possible
1402 merge_status, msg = PullRequestModel().merge_status(pull_request)
1402 merge_status, msg = PullRequestModel().merge_status(pull_request)
1403 merge_check.merge_possible = merge_status
1403 merge_check.merge_possible = merge_status
1404 merge_check.merge_msg = msg
1404 merge_check.merge_msg = msg
1405 if not merge_status:
1405 if not merge_status:
1406 log.debug(
1406 log.debug(
1407 "MergeCheck: cannot merge, pull request merge not possible.")
1407 "MergeCheck: cannot merge, pull request merge not possible.")
1408 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1408 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1409
1409
1410 if fail_early:
1410 if fail_early:
1411 return merge_check
1411 return merge_check
1412
1412
1413 return merge_check
1413 return merge_check
1414
1414
1415
1415
1416 ChangeTuple = namedtuple('ChangeTuple',
1416 ChangeTuple = namedtuple('ChangeTuple',
1417 ['added', 'common', 'removed', 'total'])
1417 ['added', 'common', 'removed', 'total'])
1418
1418
1419 FileChangeTuple = namedtuple('FileChangeTuple',
1419 FileChangeTuple = namedtuple('FileChangeTuple',
1420 ['added', 'modified', 'removed'])
1420 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now