##// END OF EJS Templates
pull-requests: updates on pull requests that don't have versions shouldn't...
marcink -
r1596:892a8297 default
parent child Browse files
Show More
@@ -1,1450 +1,1454 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 target_ref_type = pull_request.target_ref_parts.type
587 target_ref_type = pull_request.target_ref_parts.type
588 target_ref_name = pull_request.target_ref_parts.name
588 target_ref_name = pull_request.target_ref_parts.name
589 target_ref_id = pull_request.target_ref_parts.commit_id
589 target_ref_id = pull_request.target_ref_parts.commit_id
590
590
591 if not self.has_valid_update_type(pull_request):
591 if not self.has_valid_update_type(pull_request):
592 log.debug(
592 log.debug(
593 "Skipping update of pull request %s due to ref type: %s",
593 "Skipping update of pull request %s due to ref type: %s",
594 pull_request, source_ref_type)
594 pull_request, source_ref_type)
595 return UpdateResponse(
595 return UpdateResponse(
596 executed=False,
596 executed=False,
597 reason=UpdateFailureReason.WRONG_REF_TPYE,
597 reason=UpdateFailureReason.WRONG_REF_TPYE,
598 old=pull_request, new=None, changes=None)
598 old=pull_request, new=None, changes=None)
599
599
600 # source repo
600 # source repo
601 source_repo = pull_request.source_repo.scm_instance()
601 source_repo = pull_request.source_repo.scm_instance()
602 try:
602 try:
603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
603 source_commit = source_repo.get_commit(commit_id=source_ref_name)
604 except CommitDoesNotExistError:
604 except CommitDoesNotExistError:
605 return UpdateResponse(
605 return UpdateResponse(
606 executed=False,
606 executed=False,
607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
607 reason=UpdateFailureReason.MISSING_SOURCE_REF,
608 old=pull_request, new=None, changes=None)
608 old=pull_request, new=None, changes=None)
609
609
610 source_changed = source_ref_id != source_commit.raw_id
610 source_changed = source_ref_id != source_commit.raw_id
611
611
612 # target repo
612 # target repo
613 target_repo = pull_request.target_repo.scm_instance()
613 target_repo = pull_request.target_repo.scm_instance()
614 try:
614 try:
615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
615 target_commit = target_repo.get_commit(commit_id=target_ref_name)
616 except CommitDoesNotExistError:
616 except CommitDoesNotExistError:
617 return UpdateResponse(
617 return UpdateResponse(
618 executed=False,
618 executed=False,
619 reason=UpdateFailureReason.MISSING_TARGET_REF,
619 reason=UpdateFailureReason.MISSING_TARGET_REF,
620 old=pull_request, new=None, changes=None)
620 old=pull_request, new=None, changes=None)
621 target_changed = target_ref_id != target_commit.raw_id
621 target_changed = target_ref_id != target_commit.raw_id
622
622
623 if not (source_changed or target_changed):
623 if not (source_changed or target_changed):
624 log.debug("Nothing changed in pull request %s", pull_request)
624 log.debug("Nothing changed in pull request %s", pull_request)
625 return UpdateResponse(
625 return UpdateResponse(
626 executed=False,
626 executed=False,
627 reason=UpdateFailureReason.NO_CHANGE,
627 reason=UpdateFailureReason.NO_CHANGE,
628 old=pull_request, new=None, changes=None)
628 old=pull_request, new=None, changes=None)
629
629
630 change_in_found = 'target repo' if target_changed else 'source repo'
630 change_in_found = 'target repo' if target_changed else 'source repo'
631 log.debug('Updating pull request because of change in %s detected',
631 log.debug('Updating pull request because of change in %s detected',
632 change_in_found)
632 change_in_found)
633
633
634 # Finally there is a need for an update, in case of source change
634 # Finally there is a need for an update, in case of source change
635 # we create a new version, else just an update
635 # we create a new version, else just an update
636 if source_changed:
636 if source_changed:
637 pull_request_version = self._create_version_from_snapshot(pull_request)
637 pull_request_version = self._create_version_from_snapshot(pull_request)
638 self._link_comments_to_version(pull_request_version)
638 self._link_comments_to_version(pull_request_version)
639 else:
639 else:
640 ver = pull_request.versions[-1]
640 try:
641 ver = pull_request.versions[-1]
642 except IndexError:
643 ver = None
644
641 pull_request.pull_request_version_id = \
645 pull_request.pull_request_version_id = \
642 ver.pull_request_version_id if ver else None
646 ver.pull_request_version_id if ver else None
643 pull_request_version = pull_request
647 pull_request_version = pull_request
644
648
645 try:
649 try:
646 if target_ref_type in ('tag', 'branch', 'book'):
650 if target_ref_type in ('tag', 'branch', 'book'):
647 target_commit = target_repo.get_commit(target_ref_name)
651 target_commit = target_repo.get_commit(target_ref_name)
648 else:
652 else:
649 target_commit = target_repo.get_commit(target_ref_id)
653 target_commit = target_repo.get_commit(target_ref_id)
650 except CommitDoesNotExistError:
654 except CommitDoesNotExistError:
651 return UpdateResponse(
655 return UpdateResponse(
652 executed=False,
656 executed=False,
653 reason=UpdateFailureReason.MISSING_TARGET_REF,
657 reason=UpdateFailureReason.MISSING_TARGET_REF,
654 old=pull_request, new=None, changes=None)
658 old=pull_request, new=None, changes=None)
655
659
656 # re-compute commit ids
660 # re-compute commit ids
657 old_commit_ids = pull_request.revisions
661 old_commit_ids = pull_request.revisions
658 pre_load = ["author", "branch", "date", "message"]
662 pre_load = ["author", "branch", "date", "message"]
659 commit_ranges = target_repo.compare(
663 commit_ranges = target_repo.compare(
660 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
664 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
661 pre_load=pre_load)
665 pre_load=pre_load)
662
666
663 ancestor = target_repo.get_common_ancestor(
667 ancestor = target_repo.get_common_ancestor(
664 target_commit.raw_id, source_commit.raw_id, source_repo)
668 target_commit.raw_id, source_commit.raw_id, source_repo)
665
669
666 pull_request.source_ref = '%s:%s:%s' % (
670 pull_request.source_ref = '%s:%s:%s' % (
667 source_ref_type, source_ref_name, source_commit.raw_id)
671 source_ref_type, source_ref_name, source_commit.raw_id)
668 pull_request.target_ref = '%s:%s:%s' % (
672 pull_request.target_ref = '%s:%s:%s' % (
669 target_ref_type, target_ref_name, ancestor)
673 target_ref_type, target_ref_name, ancestor)
670
674
671 pull_request.revisions = [
675 pull_request.revisions = [
672 commit.raw_id for commit in reversed(commit_ranges)]
676 commit.raw_id for commit in reversed(commit_ranges)]
673 pull_request.updated_on = datetime.datetime.now()
677 pull_request.updated_on = datetime.datetime.now()
674 Session().add(pull_request)
678 Session().add(pull_request)
675 new_commit_ids = pull_request.revisions
679 new_commit_ids = pull_request.revisions
676
680
677 changes = self._calculate_commit_id_changes(
681 changes = self._calculate_commit_id_changes(
678 old_commit_ids, new_commit_ids)
682 old_commit_ids, new_commit_ids)
679
683
680 old_diff_data, new_diff_data = self._generate_update_diffs(
684 old_diff_data, new_diff_data = self._generate_update_diffs(
681 pull_request, pull_request_version)
685 pull_request, pull_request_version)
682
686
683 CommentsModel().outdate_comments(
687 CommentsModel().outdate_comments(
684 pull_request, old_diff_data=old_diff_data,
688 pull_request, old_diff_data=old_diff_data,
685 new_diff_data=new_diff_data)
689 new_diff_data=new_diff_data)
686
690
687 file_changes = self._calculate_file_changes(
691 file_changes = self._calculate_file_changes(
688 old_diff_data, new_diff_data)
692 old_diff_data, new_diff_data)
689
693
690 # Add an automatic comment to the pull request
694 # Add an automatic comment to the pull request
691 update_comment = CommentsModel().create(
695 update_comment = CommentsModel().create(
692 text=self._render_update_message(changes, file_changes),
696 text=self._render_update_message(changes, file_changes),
693 repo=pull_request.target_repo,
697 repo=pull_request.target_repo,
694 user=pull_request.author,
698 user=pull_request.author,
695 pull_request=pull_request,
699 pull_request=pull_request,
696 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
700 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
697
701
698 # Update status to "Under Review" for added commits
702 # Update status to "Under Review" for added commits
699 for commit_id in changes.added:
703 for commit_id in changes.added:
700 ChangesetStatusModel().set_status(
704 ChangesetStatusModel().set_status(
701 repo=pull_request.source_repo,
705 repo=pull_request.source_repo,
702 status=ChangesetStatus.STATUS_UNDER_REVIEW,
706 status=ChangesetStatus.STATUS_UNDER_REVIEW,
703 comment=update_comment,
707 comment=update_comment,
704 user=pull_request.author,
708 user=pull_request.author,
705 pull_request=pull_request,
709 pull_request=pull_request,
706 revision=commit_id)
710 revision=commit_id)
707
711
708 log.debug(
712 log.debug(
709 'Updated pull request %s, added_ids: %s, common_ids: %s, '
713 'Updated pull request %s, added_ids: %s, common_ids: %s, '
710 'removed_ids: %s', pull_request.pull_request_id,
714 'removed_ids: %s', pull_request.pull_request_id,
711 changes.added, changes.common, changes.removed)
715 changes.added, changes.common, changes.removed)
712 log.debug('Updated pull request with the following file changes: %s',
716 log.debug('Updated pull request with the following file changes: %s',
713 file_changes)
717 file_changes)
714
718
715 log.info(
719 log.info(
716 "Updated pull request %s from commit %s to commit %s, "
720 "Updated pull request %s from commit %s to commit %s, "
717 "stored new version %s of this pull request.",
721 "stored new version %s of this pull request.",
718 pull_request.pull_request_id, source_ref_id,
722 pull_request.pull_request_id, source_ref_id,
719 pull_request.source_ref_parts.commit_id,
723 pull_request.source_ref_parts.commit_id,
720 pull_request_version.pull_request_version_id)
724 pull_request_version.pull_request_version_id)
721 Session().commit()
725 Session().commit()
722 self._trigger_pull_request_hook(pull_request, pull_request.author,
726 self._trigger_pull_request_hook(pull_request, pull_request.author,
723 'update')
727 'update')
724
728
725 return UpdateResponse(
729 return UpdateResponse(
726 executed=True, reason=UpdateFailureReason.NONE,
730 executed=True, reason=UpdateFailureReason.NONE,
727 old=pull_request, new=pull_request_version, changes=changes)
731 old=pull_request, new=pull_request_version, changes=changes)
728
732
729 def _create_version_from_snapshot(self, pull_request):
733 def _create_version_from_snapshot(self, pull_request):
730 version = PullRequestVersion()
734 version = PullRequestVersion()
731 version.title = pull_request.title
735 version.title = pull_request.title
732 version.description = pull_request.description
736 version.description = pull_request.description
733 version.status = pull_request.status
737 version.status = pull_request.status
734 version.created_on = datetime.datetime.now()
738 version.created_on = datetime.datetime.now()
735 version.updated_on = pull_request.updated_on
739 version.updated_on = pull_request.updated_on
736 version.user_id = pull_request.user_id
740 version.user_id = pull_request.user_id
737 version.source_repo = pull_request.source_repo
741 version.source_repo = pull_request.source_repo
738 version.source_ref = pull_request.source_ref
742 version.source_ref = pull_request.source_ref
739 version.target_repo = pull_request.target_repo
743 version.target_repo = pull_request.target_repo
740 version.target_ref = pull_request.target_ref
744 version.target_ref = pull_request.target_ref
741
745
742 version._last_merge_source_rev = pull_request._last_merge_source_rev
746 version._last_merge_source_rev = pull_request._last_merge_source_rev
743 version._last_merge_target_rev = pull_request._last_merge_target_rev
747 version._last_merge_target_rev = pull_request._last_merge_target_rev
744 version._last_merge_status = pull_request._last_merge_status
748 version._last_merge_status = pull_request._last_merge_status
745 version.shadow_merge_ref = pull_request.shadow_merge_ref
749 version.shadow_merge_ref = pull_request.shadow_merge_ref
746 version.merge_rev = pull_request.merge_rev
750 version.merge_rev = pull_request.merge_rev
747
751
748 version.revisions = pull_request.revisions
752 version.revisions = pull_request.revisions
749 version.pull_request = pull_request
753 version.pull_request = pull_request
750 Session().add(version)
754 Session().add(version)
751 Session().flush()
755 Session().flush()
752
756
753 return version
757 return version
754
758
755 def _generate_update_diffs(self, pull_request, pull_request_version):
759 def _generate_update_diffs(self, pull_request, pull_request_version):
756
760
757 diff_context = (
761 diff_context = (
758 self.DIFF_CONTEXT +
762 self.DIFF_CONTEXT +
759 CommentsModel.needed_extra_diff_context())
763 CommentsModel.needed_extra_diff_context())
760
764
761 source_repo = pull_request_version.source_repo
765 source_repo = pull_request_version.source_repo
762 source_ref_id = pull_request_version.source_ref_parts.commit_id
766 source_ref_id = pull_request_version.source_ref_parts.commit_id
763 target_ref_id = pull_request_version.target_ref_parts.commit_id
767 target_ref_id = pull_request_version.target_ref_parts.commit_id
764 old_diff = self._get_diff_from_pr_or_version(
768 old_diff = self._get_diff_from_pr_or_version(
765 source_repo, source_ref_id, target_ref_id, context=diff_context)
769 source_repo, source_ref_id, target_ref_id, context=diff_context)
766
770
767 source_repo = pull_request.source_repo
771 source_repo = pull_request.source_repo
768 source_ref_id = pull_request.source_ref_parts.commit_id
772 source_ref_id = pull_request.source_ref_parts.commit_id
769 target_ref_id = pull_request.target_ref_parts.commit_id
773 target_ref_id = pull_request.target_ref_parts.commit_id
770
774
771 new_diff = self._get_diff_from_pr_or_version(
775 new_diff = self._get_diff_from_pr_or_version(
772 source_repo, source_ref_id, target_ref_id, context=diff_context)
776 source_repo, source_ref_id, target_ref_id, context=diff_context)
773
777
774 old_diff_data = diffs.DiffProcessor(old_diff)
778 old_diff_data = diffs.DiffProcessor(old_diff)
775 old_diff_data.prepare()
779 old_diff_data.prepare()
776 new_diff_data = diffs.DiffProcessor(new_diff)
780 new_diff_data = diffs.DiffProcessor(new_diff)
777 new_diff_data.prepare()
781 new_diff_data.prepare()
778
782
779 return old_diff_data, new_diff_data
783 return old_diff_data, new_diff_data
780
784
781 def _link_comments_to_version(self, pull_request_version):
785 def _link_comments_to_version(self, pull_request_version):
782 """
786 """
783 Link all unlinked comments of this pull request to the given version.
787 Link all unlinked comments of this pull request to the given version.
784
788
785 :param pull_request_version: The `PullRequestVersion` to which
789 :param pull_request_version: The `PullRequestVersion` to which
786 the comments shall be linked.
790 the comments shall be linked.
787
791
788 """
792 """
789 pull_request = pull_request_version.pull_request
793 pull_request = pull_request_version.pull_request
790 comments = ChangesetComment.query().filter(
794 comments = ChangesetComment.query().filter(
791 # TODO: johbo: Should we query for the repo at all here?
795 # TODO: johbo: Should we query for the repo at all here?
792 # Pending decision on how comments of PRs are to be related
796 # Pending decision on how comments of PRs are to be related
793 # to either the source repo, the target repo or no repo at all.
797 # to either the source repo, the target repo or no repo at all.
794 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
798 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
795 ChangesetComment.pull_request == pull_request,
799 ChangesetComment.pull_request == pull_request,
796 ChangesetComment.pull_request_version == None)
800 ChangesetComment.pull_request_version == None)
797
801
798 # TODO: johbo: Find out why this breaks if it is done in a bulk
802 # TODO: johbo: Find out why this breaks if it is done in a bulk
799 # operation.
803 # operation.
800 for comment in comments:
804 for comment in comments:
801 comment.pull_request_version_id = (
805 comment.pull_request_version_id = (
802 pull_request_version.pull_request_version_id)
806 pull_request_version.pull_request_version_id)
803 Session().add(comment)
807 Session().add(comment)
804
808
805 def _calculate_commit_id_changes(self, old_ids, new_ids):
809 def _calculate_commit_id_changes(self, old_ids, new_ids):
806 added = [x for x in new_ids if x not in old_ids]
810 added = [x for x in new_ids if x not in old_ids]
807 common = [x for x in new_ids if x in old_ids]
811 common = [x for x in new_ids if x in old_ids]
808 removed = [x for x in old_ids if x not in new_ids]
812 removed = [x for x in old_ids if x not in new_ids]
809 total = new_ids
813 total = new_ids
810 return ChangeTuple(added, common, removed, total)
814 return ChangeTuple(added, common, removed, total)
811
815
812 def _calculate_file_changes(self, old_diff_data, new_diff_data):
816 def _calculate_file_changes(self, old_diff_data, new_diff_data):
813
817
814 old_files = OrderedDict()
818 old_files = OrderedDict()
815 for diff_data in old_diff_data.parsed_diff:
819 for diff_data in old_diff_data.parsed_diff:
816 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
820 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
817
821
818 added_files = []
822 added_files = []
819 modified_files = []
823 modified_files = []
820 removed_files = []
824 removed_files = []
821 for diff_data in new_diff_data.parsed_diff:
825 for diff_data in new_diff_data.parsed_diff:
822 new_filename = diff_data['filename']
826 new_filename = diff_data['filename']
823 new_hash = md5_safe(diff_data['raw_diff'])
827 new_hash = md5_safe(diff_data['raw_diff'])
824
828
825 old_hash = old_files.get(new_filename)
829 old_hash = old_files.get(new_filename)
826 if not old_hash:
830 if not old_hash:
827 # file is not present in old diff, means it's added
831 # file is not present in old diff, means it's added
828 added_files.append(new_filename)
832 added_files.append(new_filename)
829 else:
833 else:
830 if new_hash != old_hash:
834 if new_hash != old_hash:
831 modified_files.append(new_filename)
835 modified_files.append(new_filename)
832 # now remove a file from old, since we have seen it already
836 # now remove a file from old, since we have seen it already
833 del old_files[new_filename]
837 del old_files[new_filename]
834
838
835 # removed files is when there are present in old, but not in NEW,
839 # removed files is when there are present in old, but not in NEW,
836 # since we remove old files that are present in new diff, left-overs
840 # since we remove old files that are present in new diff, left-overs
837 # if any should be the removed files
841 # if any should be the removed files
838 removed_files.extend(old_files.keys())
842 removed_files.extend(old_files.keys())
839
843
840 return FileChangeTuple(added_files, modified_files, removed_files)
844 return FileChangeTuple(added_files, modified_files, removed_files)
841
845
842 def _render_update_message(self, changes, file_changes):
846 def _render_update_message(self, changes, file_changes):
843 """
847 """
844 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
848 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
845 so it's always looking the same disregarding on which default
849 so it's always looking the same disregarding on which default
846 renderer system is using.
850 renderer system is using.
847
851
848 :param changes: changes named tuple
852 :param changes: changes named tuple
849 :param file_changes: file changes named tuple
853 :param file_changes: file changes named tuple
850
854
851 """
855 """
852 new_status = ChangesetStatus.get_status_lbl(
856 new_status = ChangesetStatus.get_status_lbl(
853 ChangesetStatus.STATUS_UNDER_REVIEW)
857 ChangesetStatus.STATUS_UNDER_REVIEW)
854
858
855 changed_files = (
859 changed_files = (
856 file_changes.added + file_changes.modified + file_changes.removed)
860 file_changes.added + file_changes.modified + file_changes.removed)
857
861
858 params = {
862 params = {
859 'under_review_label': new_status,
863 'under_review_label': new_status,
860 'added_commits': changes.added,
864 'added_commits': changes.added,
861 'removed_commits': changes.removed,
865 'removed_commits': changes.removed,
862 'changed_files': changed_files,
866 'changed_files': changed_files,
863 'added_files': file_changes.added,
867 'added_files': file_changes.added,
864 'modified_files': file_changes.modified,
868 'modified_files': file_changes.modified,
865 'removed_files': file_changes.removed,
869 'removed_files': file_changes.removed,
866 }
870 }
867 renderer = RstTemplateRenderer()
871 renderer = RstTemplateRenderer()
868 return renderer.render('pull_request_update.mako', **params)
872 return renderer.render('pull_request_update.mako', **params)
869
873
870 def edit(self, pull_request, title, description):
874 def edit(self, pull_request, title, description):
871 pull_request = self.__get_pull_request(pull_request)
875 pull_request = self.__get_pull_request(pull_request)
872 if pull_request.is_closed():
876 if pull_request.is_closed():
873 raise ValueError('This pull request is closed')
877 raise ValueError('This pull request is closed')
874 if title:
878 if title:
875 pull_request.title = title
879 pull_request.title = title
876 pull_request.description = description
880 pull_request.description = description
877 pull_request.updated_on = datetime.datetime.now()
881 pull_request.updated_on = datetime.datetime.now()
878 Session().add(pull_request)
882 Session().add(pull_request)
879
883
880 def update_reviewers(self, pull_request, reviewer_data):
884 def update_reviewers(self, pull_request, reviewer_data):
881 """
885 """
882 Update the reviewers in the pull request
886 Update the reviewers in the pull request
883
887
884 :param pull_request: the pr to update
888 :param pull_request: the pr to update
885 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
889 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
886 """
890 """
887
891
888 reviewers_reasons = {}
892 reviewers_reasons = {}
889 for user_id, reasons in reviewer_data:
893 for user_id, reasons in reviewer_data:
890 if isinstance(user_id, (int, basestring)):
894 if isinstance(user_id, (int, basestring)):
891 user_id = self._get_user(user_id).user_id
895 user_id = self._get_user(user_id).user_id
892 reviewers_reasons[user_id] = reasons
896 reviewers_reasons[user_id] = reasons
893
897
894 reviewers_ids = set(reviewers_reasons.keys())
898 reviewers_ids = set(reviewers_reasons.keys())
895 pull_request = self.__get_pull_request(pull_request)
899 pull_request = self.__get_pull_request(pull_request)
896 current_reviewers = PullRequestReviewers.query()\
900 current_reviewers = PullRequestReviewers.query()\
897 .filter(PullRequestReviewers.pull_request ==
901 .filter(PullRequestReviewers.pull_request ==
898 pull_request).all()
902 pull_request).all()
899 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
903 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
900
904
901 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
905 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
902 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
906 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
903
907
904 log.debug("Adding %s reviewers", ids_to_add)
908 log.debug("Adding %s reviewers", ids_to_add)
905 log.debug("Removing %s reviewers", ids_to_remove)
909 log.debug("Removing %s reviewers", ids_to_remove)
906 changed = False
910 changed = False
907 for uid in ids_to_add:
911 for uid in ids_to_add:
908 changed = True
912 changed = True
909 _usr = self._get_user(uid)
913 _usr = self._get_user(uid)
910 reasons = reviewers_reasons[uid]
914 reasons = reviewers_reasons[uid]
911 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
915 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
912 Session().add(reviewer)
916 Session().add(reviewer)
913
917
914 for uid in ids_to_remove:
918 for uid in ids_to_remove:
915 changed = True
919 changed = True
916 reviewers = PullRequestReviewers.query()\
920 reviewers = PullRequestReviewers.query()\
917 .filter(PullRequestReviewers.user_id == uid,
921 .filter(PullRequestReviewers.user_id == uid,
918 PullRequestReviewers.pull_request == pull_request)\
922 PullRequestReviewers.pull_request == pull_request)\
919 .all()
923 .all()
920 # use .all() in case we accidentally added the same person twice
924 # use .all() in case we accidentally added the same person twice
921 # this CAN happen due to the lack of DB checks
925 # this CAN happen due to the lack of DB checks
922 for obj in reviewers:
926 for obj in reviewers:
923 Session().delete(obj)
927 Session().delete(obj)
924
928
925 if changed:
929 if changed:
926 pull_request.updated_on = datetime.datetime.now()
930 pull_request.updated_on = datetime.datetime.now()
927 Session().add(pull_request)
931 Session().add(pull_request)
928
932
929 self.notify_reviewers(pull_request, ids_to_add)
933 self.notify_reviewers(pull_request, ids_to_add)
930 return ids_to_add, ids_to_remove
934 return ids_to_add, ids_to_remove
931
935
932 def get_url(self, pull_request):
936 def get_url(self, pull_request):
933 return h.url('pullrequest_show',
937 return h.url('pullrequest_show',
934 repo_name=safe_str(pull_request.target_repo.repo_name),
938 repo_name=safe_str(pull_request.target_repo.repo_name),
935 pull_request_id=pull_request.pull_request_id,
939 pull_request_id=pull_request.pull_request_id,
936 qualified=True)
940 qualified=True)
937
941
938 def get_shadow_clone_url(self, pull_request):
942 def get_shadow_clone_url(self, pull_request):
939 """
943 """
940 Returns qualified url pointing to the shadow repository. If this pull
944 Returns qualified url pointing to the shadow repository. If this pull
941 request is closed there is no shadow repository and ``None`` will be
945 request is closed there is no shadow repository and ``None`` will be
942 returned.
946 returned.
943 """
947 """
944 if pull_request.is_closed():
948 if pull_request.is_closed():
945 return None
949 return None
946 else:
950 else:
947 pr_url = urllib.unquote(self.get_url(pull_request))
951 pr_url = urllib.unquote(self.get_url(pull_request))
948 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
952 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
949
953
950 def notify_reviewers(self, pull_request, reviewers_ids):
954 def notify_reviewers(self, pull_request, reviewers_ids):
951 # notification to reviewers
955 # notification to reviewers
952 if not reviewers_ids:
956 if not reviewers_ids:
953 return
957 return
954
958
955 pull_request_obj = pull_request
959 pull_request_obj = pull_request
956 # get the current participants of this pull request
960 # get the current participants of this pull request
957 recipients = reviewers_ids
961 recipients = reviewers_ids
958 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
962 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
959
963
960 pr_source_repo = pull_request_obj.source_repo
964 pr_source_repo = pull_request_obj.source_repo
961 pr_target_repo = pull_request_obj.target_repo
965 pr_target_repo = pull_request_obj.target_repo
962
966
963 pr_url = h.url(
967 pr_url = h.url(
964 'pullrequest_show',
968 'pullrequest_show',
965 repo_name=pr_target_repo.repo_name,
969 repo_name=pr_target_repo.repo_name,
966 pull_request_id=pull_request_obj.pull_request_id,
970 pull_request_id=pull_request_obj.pull_request_id,
967 qualified=True,)
971 qualified=True,)
968
972
969 # set some variables for email notification
973 # set some variables for email notification
970 pr_target_repo_url = h.url(
974 pr_target_repo_url = h.url(
971 'summary_home',
975 'summary_home',
972 repo_name=pr_target_repo.repo_name,
976 repo_name=pr_target_repo.repo_name,
973 qualified=True)
977 qualified=True)
974
978
975 pr_source_repo_url = h.url(
979 pr_source_repo_url = h.url(
976 'summary_home',
980 'summary_home',
977 repo_name=pr_source_repo.repo_name,
981 repo_name=pr_source_repo.repo_name,
978 qualified=True)
982 qualified=True)
979
983
980 # pull request specifics
984 # pull request specifics
981 pull_request_commits = [
985 pull_request_commits = [
982 (x.raw_id, x.message)
986 (x.raw_id, x.message)
983 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
987 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
984
988
985 kwargs = {
989 kwargs = {
986 'user': pull_request.author,
990 'user': pull_request.author,
987 'pull_request': pull_request_obj,
991 'pull_request': pull_request_obj,
988 'pull_request_commits': pull_request_commits,
992 'pull_request_commits': pull_request_commits,
989
993
990 'pull_request_target_repo': pr_target_repo,
994 'pull_request_target_repo': pr_target_repo,
991 'pull_request_target_repo_url': pr_target_repo_url,
995 'pull_request_target_repo_url': pr_target_repo_url,
992
996
993 'pull_request_source_repo': pr_source_repo,
997 'pull_request_source_repo': pr_source_repo,
994 'pull_request_source_repo_url': pr_source_repo_url,
998 'pull_request_source_repo_url': pr_source_repo_url,
995
999
996 'pull_request_url': pr_url,
1000 'pull_request_url': pr_url,
997 }
1001 }
998
1002
999 # pre-generate the subject for notification itself
1003 # pre-generate the subject for notification itself
1000 (subject,
1004 (subject,
1001 _h, _e, # we don't care about those
1005 _h, _e, # we don't care about those
1002 body_plaintext) = EmailNotificationModel().render_email(
1006 body_plaintext) = EmailNotificationModel().render_email(
1003 notification_type, **kwargs)
1007 notification_type, **kwargs)
1004
1008
1005 # create notification objects, and emails
1009 # create notification objects, and emails
1006 NotificationModel().create(
1010 NotificationModel().create(
1007 created_by=pull_request.author,
1011 created_by=pull_request.author,
1008 notification_subject=subject,
1012 notification_subject=subject,
1009 notification_body=body_plaintext,
1013 notification_body=body_plaintext,
1010 notification_type=notification_type,
1014 notification_type=notification_type,
1011 recipients=recipients,
1015 recipients=recipients,
1012 email_kwargs=kwargs,
1016 email_kwargs=kwargs,
1013 )
1017 )
1014
1018
1015 def delete(self, pull_request):
1019 def delete(self, pull_request):
1016 pull_request = self.__get_pull_request(pull_request)
1020 pull_request = self.__get_pull_request(pull_request)
1017 self._cleanup_merge_workspace(pull_request)
1021 self._cleanup_merge_workspace(pull_request)
1018 Session().delete(pull_request)
1022 Session().delete(pull_request)
1019
1023
1020 def close_pull_request(self, pull_request, user):
1024 def close_pull_request(self, pull_request, user):
1021 pull_request = self.__get_pull_request(pull_request)
1025 pull_request = self.__get_pull_request(pull_request)
1022 self._cleanup_merge_workspace(pull_request)
1026 self._cleanup_merge_workspace(pull_request)
1023 pull_request.status = PullRequest.STATUS_CLOSED
1027 pull_request.status = PullRequest.STATUS_CLOSED
1024 pull_request.updated_on = datetime.datetime.now()
1028 pull_request.updated_on = datetime.datetime.now()
1025 Session().add(pull_request)
1029 Session().add(pull_request)
1026 self._trigger_pull_request_hook(
1030 self._trigger_pull_request_hook(
1027 pull_request, pull_request.author, 'close')
1031 pull_request, pull_request.author, 'close')
1028 self._log_action('user_closed_pull_request', user, pull_request)
1032 self._log_action('user_closed_pull_request', user, pull_request)
1029
1033
1030 def close_pull_request_with_comment(self, pull_request, user, repo,
1034 def close_pull_request_with_comment(self, pull_request, user, repo,
1031 message=None):
1035 message=None):
1032 status = ChangesetStatus.STATUS_REJECTED
1036 status = ChangesetStatus.STATUS_REJECTED
1033
1037
1034 if not message:
1038 if not message:
1035 message = (
1039 message = (
1036 _('Status change %(transition_icon)s %(status)s') % {
1040 _('Status change %(transition_icon)s %(status)s') % {
1037 'transition_icon': '>',
1041 'transition_icon': '>',
1038 'status': ChangesetStatus.get_status_lbl(status)})
1042 'status': ChangesetStatus.get_status_lbl(status)})
1039
1043
1040 internal_message = _('Closing with') + ' ' + message
1044 internal_message = _('Closing with') + ' ' + message
1041
1045
1042 comm = CommentsModel().create(
1046 comm = CommentsModel().create(
1043 text=internal_message,
1047 text=internal_message,
1044 repo=repo.repo_id,
1048 repo=repo.repo_id,
1045 user=user.user_id,
1049 user=user.user_id,
1046 pull_request=pull_request.pull_request_id,
1050 pull_request=pull_request.pull_request_id,
1047 f_path=None,
1051 f_path=None,
1048 line_no=None,
1052 line_no=None,
1049 status_change=ChangesetStatus.get_status_lbl(status),
1053 status_change=ChangesetStatus.get_status_lbl(status),
1050 status_change_type=status,
1054 status_change_type=status,
1051 closing_pr=True
1055 closing_pr=True
1052 )
1056 )
1053
1057
1054 ChangesetStatusModel().set_status(
1058 ChangesetStatusModel().set_status(
1055 repo.repo_id,
1059 repo.repo_id,
1056 status,
1060 status,
1057 user.user_id,
1061 user.user_id,
1058 comm,
1062 comm,
1059 pull_request=pull_request.pull_request_id
1063 pull_request=pull_request.pull_request_id
1060 )
1064 )
1061 Session().flush()
1065 Session().flush()
1062
1066
1063 PullRequestModel().close_pull_request(
1067 PullRequestModel().close_pull_request(
1064 pull_request.pull_request_id, user)
1068 pull_request.pull_request_id, user)
1065
1069
1066 def merge_status(self, pull_request):
1070 def merge_status(self, pull_request):
1067 if not self._is_merge_enabled(pull_request):
1071 if not self._is_merge_enabled(pull_request):
1068 return False, _('Server-side pull request merging is disabled.')
1072 return False, _('Server-side pull request merging is disabled.')
1069 if pull_request.is_closed():
1073 if pull_request.is_closed():
1070 return False, _('This pull request is closed.')
1074 return False, _('This pull request is closed.')
1071 merge_possible, msg = self._check_repo_requirements(
1075 merge_possible, msg = self._check_repo_requirements(
1072 target=pull_request.target_repo, source=pull_request.source_repo)
1076 target=pull_request.target_repo, source=pull_request.source_repo)
1073 if not merge_possible:
1077 if not merge_possible:
1074 return merge_possible, msg
1078 return merge_possible, msg
1075
1079
1076 try:
1080 try:
1077 resp = self._try_merge(pull_request)
1081 resp = self._try_merge(pull_request)
1078 log.debug("Merge response: %s", resp)
1082 log.debug("Merge response: %s", resp)
1079 status = resp.possible, self.merge_status_message(
1083 status = resp.possible, self.merge_status_message(
1080 resp.failure_reason)
1084 resp.failure_reason)
1081 except NotImplementedError:
1085 except NotImplementedError:
1082 status = False, _('Pull request merging is not supported.')
1086 status = False, _('Pull request merging is not supported.')
1083
1087
1084 return status
1088 return status
1085
1089
1086 def _check_repo_requirements(self, target, source):
1090 def _check_repo_requirements(self, target, source):
1087 """
1091 """
1088 Check if `target` and `source` have compatible requirements.
1092 Check if `target` and `source` have compatible requirements.
1089
1093
1090 Currently this is just checking for largefiles.
1094 Currently this is just checking for largefiles.
1091 """
1095 """
1092 target_has_largefiles = self._has_largefiles(target)
1096 target_has_largefiles = self._has_largefiles(target)
1093 source_has_largefiles = self._has_largefiles(source)
1097 source_has_largefiles = self._has_largefiles(source)
1094 merge_possible = True
1098 merge_possible = True
1095 message = u''
1099 message = u''
1096
1100
1097 if target_has_largefiles != source_has_largefiles:
1101 if target_has_largefiles != source_has_largefiles:
1098 merge_possible = False
1102 merge_possible = False
1099 if source_has_largefiles:
1103 if source_has_largefiles:
1100 message = _(
1104 message = _(
1101 'Target repository large files support is disabled.')
1105 'Target repository large files support is disabled.')
1102 else:
1106 else:
1103 message = _(
1107 message = _(
1104 'Source repository large files support is disabled.')
1108 'Source repository large files support is disabled.')
1105
1109
1106 return merge_possible, message
1110 return merge_possible, message
1107
1111
1108 def _has_largefiles(self, repo):
1112 def _has_largefiles(self, repo):
1109 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1113 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1110 'extensions', 'largefiles')
1114 'extensions', 'largefiles')
1111 return largefiles_ui and largefiles_ui[0].active
1115 return largefiles_ui and largefiles_ui[0].active
1112
1116
1113 def _try_merge(self, pull_request):
1117 def _try_merge(self, pull_request):
1114 """
1118 """
1115 Try to merge the pull request and return the merge status.
1119 Try to merge the pull request and return the merge status.
1116 """
1120 """
1117 log.debug(
1121 log.debug(
1118 "Trying out if the pull request %s can be merged.",
1122 "Trying out if the pull request %s can be merged.",
1119 pull_request.pull_request_id)
1123 pull_request.pull_request_id)
1120 target_vcs = pull_request.target_repo.scm_instance()
1124 target_vcs = pull_request.target_repo.scm_instance()
1121
1125
1122 # Refresh the target reference.
1126 # Refresh the target reference.
1123 try:
1127 try:
1124 target_ref = self._refresh_reference(
1128 target_ref = self._refresh_reference(
1125 pull_request.target_ref_parts, target_vcs)
1129 pull_request.target_ref_parts, target_vcs)
1126 except CommitDoesNotExistError:
1130 except CommitDoesNotExistError:
1127 merge_state = MergeResponse(
1131 merge_state = MergeResponse(
1128 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1132 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1129 return merge_state
1133 return merge_state
1130
1134
1131 target_locked = pull_request.target_repo.locked
1135 target_locked = pull_request.target_repo.locked
1132 if target_locked and target_locked[0]:
1136 if target_locked and target_locked[0]:
1133 log.debug("The target repository is locked.")
1137 log.debug("The target repository is locked.")
1134 merge_state = MergeResponse(
1138 merge_state = MergeResponse(
1135 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1139 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1136 elif self._needs_merge_state_refresh(pull_request, target_ref):
1140 elif self._needs_merge_state_refresh(pull_request, target_ref):
1137 log.debug("Refreshing the merge status of the repository.")
1141 log.debug("Refreshing the merge status of the repository.")
1138 merge_state = self._refresh_merge_state(
1142 merge_state = self._refresh_merge_state(
1139 pull_request, target_vcs, target_ref)
1143 pull_request, target_vcs, target_ref)
1140 else:
1144 else:
1141 possible = pull_request.\
1145 possible = pull_request.\
1142 _last_merge_status == MergeFailureReason.NONE
1146 _last_merge_status == MergeFailureReason.NONE
1143 merge_state = MergeResponse(
1147 merge_state = MergeResponse(
1144 possible, False, None, pull_request._last_merge_status)
1148 possible, False, None, pull_request._last_merge_status)
1145
1149
1146 return merge_state
1150 return merge_state
1147
1151
1148 def _refresh_reference(self, reference, vcs_repository):
1152 def _refresh_reference(self, reference, vcs_repository):
1149 if reference.type in ('branch', 'book'):
1153 if reference.type in ('branch', 'book'):
1150 name_or_id = reference.name
1154 name_or_id = reference.name
1151 else:
1155 else:
1152 name_or_id = reference.commit_id
1156 name_or_id = reference.commit_id
1153 refreshed_commit = vcs_repository.get_commit(name_or_id)
1157 refreshed_commit = vcs_repository.get_commit(name_or_id)
1154 refreshed_reference = Reference(
1158 refreshed_reference = Reference(
1155 reference.type, reference.name, refreshed_commit.raw_id)
1159 reference.type, reference.name, refreshed_commit.raw_id)
1156 return refreshed_reference
1160 return refreshed_reference
1157
1161
1158 def _needs_merge_state_refresh(self, pull_request, target_reference):
1162 def _needs_merge_state_refresh(self, pull_request, target_reference):
1159 return not(
1163 return not(
1160 pull_request.revisions and
1164 pull_request.revisions and
1161 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1165 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1162 target_reference.commit_id == pull_request._last_merge_target_rev)
1166 target_reference.commit_id == pull_request._last_merge_target_rev)
1163
1167
1164 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1168 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1165 workspace_id = self._workspace_id(pull_request)
1169 workspace_id = self._workspace_id(pull_request)
1166 source_vcs = pull_request.source_repo.scm_instance()
1170 source_vcs = pull_request.source_repo.scm_instance()
1167 use_rebase = self._use_rebase_for_merging(pull_request)
1171 use_rebase = self._use_rebase_for_merging(pull_request)
1168 merge_state = target_vcs.merge(
1172 merge_state = target_vcs.merge(
1169 target_reference, source_vcs, pull_request.source_ref_parts,
1173 target_reference, source_vcs, pull_request.source_ref_parts,
1170 workspace_id, dry_run=True, use_rebase=use_rebase)
1174 workspace_id, dry_run=True, use_rebase=use_rebase)
1171
1175
1172 # Do not store the response if there was an unknown error.
1176 # Do not store the response if there was an unknown error.
1173 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1177 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1174 pull_request._last_merge_source_rev = \
1178 pull_request._last_merge_source_rev = \
1175 pull_request.source_ref_parts.commit_id
1179 pull_request.source_ref_parts.commit_id
1176 pull_request._last_merge_target_rev = target_reference.commit_id
1180 pull_request._last_merge_target_rev = target_reference.commit_id
1177 pull_request._last_merge_status = merge_state.failure_reason
1181 pull_request._last_merge_status = merge_state.failure_reason
1178 pull_request.shadow_merge_ref = merge_state.merge_ref
1182 pull_request.shadow_merge_ref = merge_state.merge_ref
1179 Session().add(pull_request)
1183 Session().add(pull_request)
1180 Session().commit()
1184 Session().commit()
1181
1185
1182 return merge_state
1186 return merge_state
1183
1187
1184 def _workspace_id(self, pull_request):
1188 def _workspace_id(self, pull_request):
1185 workspace_id = 'pr-%s' % pull_request.pull_request_id
1189 workspace_id = 'pr-%s' % pull_request.pull_request_id
1186 return workspace_id
1190 return workspace_id
1187
1191
1188 def merge_status_message(self, status_code):
1192 def merge_status_message(self, status_code):
1189 """
1193 """
1190 Return a human friendly error message for the given merge status code.
1194 Return a human friendly error message for the given merge status code.
1191 """
1195 """
1192 return self.MERGE_STATUS_MESSAGES[status_code]
1196 return self.MERGE_STATUS_MESSAGES[status_code]
1193
1197
1194 def generate_repo_data(self, repo, commit_id=None, branch=None,
1198 def generate_repo_data(self, repo, commit_id=None, branch=None,
1195 bookmark=None):
1199 bookmark=None):
1196 all_refs, selected_ref = \
1200 all_refs, selected_ref = \
1197 self._get_repo_pullrequest_sources(
1201 self._get_repo_pullrequest_sources(
1198 repo.scm_instance(), commit_id=commit_id,
1202 repo.scm_instance(), commit_id=commit_id,
1199 branch=branch, bookmark=bookmark)
1203 branch=branch, bookmark=bookmark)
1200
1204
1201 refs_select2 = []
1205 refs_select2 = []
1202 for element in all_refs:
1206 for element in all_refs:
1203 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1207 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1204 refs_select2.append({'text': element[1], 'children': children})
1208 refs_select2.append({'text': element[1], 'children': children})
1205
1209
1206 return {
1210 return {
1207 'user': {
1211 'user': {
1208 'user_id': repo.user.user_id,
1212 'user_id': repo.user.user_id,
1209 'username': repo.user.username,
1213 'username': repo.user.username,
1210 'firstname': repo.user.firstname,
1214 'firstname': repo.user.firstname,
1211 'lastname': repo.user.lastname,
1215 'lastname': repo.user.lastname,
1212 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1216 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1213 },
1217 },
1214 'description': h.chop_at_smart(repo.description, '\n'),
1218 'description': h.chop_at_smart(repo.description, '\n'),
1215 'refs': {
1219 'refs': {
1216 'all_refs': all_refs,
1220 'all_refs': all_refs,
1217 'selected_ref': selected_ref,
1221 'selected_ref': selected_ref,
1218 'select2_refs': refs_select2
1222 'select2_refs': refs_select2
1219 }
1223 }
1220 }
1224 }
1221
1225
1222 def generate_pullrequest_title(self, source, source_ref, target):
1226 def generate_pullrequest_title(self, source, source_ref, target):
1223 return u'{source}#{at_ref} to {target}'.format(
1227 return u'{source}#{at_ref} to {target}'.format(
1224 source=source,
1228 source=source,
1225 at_ref=source_ref,
1229 at_ref=source_ref,
1226 target=target,
1230 target=target,
1227 )
1231 )
1228
1232
1229 def _cleanup_merge_workspace(self, pull_request):
1233 def _cleanup_merge_workspace(self, pull_request):
1230 # Merging related cleanup
1234 # Merging related cleanup
1231 target_scm = pull_request.target_repo.scm_instance()
1235 target_scm = pull_request.target_repo.scm_instance()
1232 workspace_id = 'pr-%s' % pull_request.pull_request_id
1236 workspace_id = 'pr-%s' % pull_request.pull_request_id
1233
1237
1234 try:
1238 try:
1235 target_scm.cleanup_merge_workspace(workspace_id)
1239 target_scm.cleanup_merge_workspace(workspace_id)
1236 except NotImplementedError:
1240 except NotImplementedError:
1237 pass
1241 pass
1238
1242
1239 def _get_repo_pullrequest_sources(
1243 def _get_repo_pullrequest_sources(
1240 self, repo, commit_id=None, branch=None, bookmark=None):
1244 self, repo, commit_id=None, branch=None, bookmark=None):
1241 """
1245 """
1242 Return a structure with repo's interesting commits, suitable for
1246 Return a structure with repo's interesting commits, suitable for
1243 the selectors in pullrequest controller
1247 the selectors in pullrequest controller
1244
1248
1245 :param commit_id: a commit that must be in the list somehow
1249 :param commit_id: a commit that must be in the list somehow
1246 and selected by default
1250 and selected by default
1247 :param branch: a branch that must be in the list and selected
1251 :param branch: a branch that must be in the list and selected
1248 by default - even if closed
1252 by default - even if closed
1249 :param bookmark: a bookmark that must be in the list and selected
1253 :param bookmark: a bookmark that must be in the list and selected
1250 """
1254 """
1251
1255
1252 commit_id = safe_str(commit_id) if commit_id else None
1256 commit_id = safe_str(commit_id) if commit_id else None
1253 branch = safe_str(branch) if branch else None
1257 branch = safe_str(branch) if branch else None
1254 bookmark = safe_str(bookmark) if bookmark else None
1258 bookmark = safe_str(bookmark) if bookmark else None
1255
1259
1256 selected = None
1260 selected = None
1257
1261
1258 # order matters: first source that has commit_id in it will be selected
1262 # order matters: first source that has commit_id in it will be selected
1259 sources = []
1263 sources = []
1260 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1264 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1261 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1265 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1262
1266
1263 if commit_id:
1267 if commit_id:
1264 ref_commit = (h.short_id(commit_id), commit_id)
1268 ref_commit = (h.short_id(commit_id), commit_id)
1265 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1269 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1266
1270
1267 sources.append(
1271 sources.append(
1268 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1272 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1269 )
1273 )
1270
1274
1271 groups = []
1275 groups = []
1272 for group_key, ref_list, group_name, match in sources:
1276 for group_key, ref_list, group_name, match in sources:
1273 group_refs = []
1277 group_refs = []
1274 for ref_name, ref_id in ref_list:
1278 for ref_name, ref_id in ref_list:
1275 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1279 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1276 group_refs.append((ref_key, ref_name))
1280 group_refs.append((ref_key, ref_name))
1277
1281
1278 if not selected:
1282 if not selected:
1279 if set([commit_id, match]) & set([ref_id, ref_name]):
1283 if set([commit_id, match]) & set([ref_id, ref_name]):
1280 selected = ref_key
1284 selected = ref_key
1281
1285
1282 if group_refs:
1286 if group_refs:
1283 groups.append((group_refs, group_name))
1287 groups.append((group_refs, group_name))
1284
1288
1285 if not selected:
1289 if not selected:
1286 ref = commit_id or branch or bookmark
1290 ref = commit_id or branch or bookmark
1287 if ref:
1291 if ref:
1288 raise CommitDoesNotExistError(
1292 raise CommitDoesNotExistError(
1289 'No commit refs could be found matching: %s' % ref)
1293 'No commit refs could be found matching: %s' % ref)
1290 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1294 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1291 selected = 'branch:%s:%s' % (
1295 selected = 'branch:%s:%s' % (
1292 repo.DEFAULT_BRANCH_NAME,
1296 repo.DEFAULT_BRANCH_NAME,
1293 repo.branches[repo.DEFAULT_BRANCH_NAME]
1297 repo.branches[repo.DEFAULT_BRANCH_NAME]
1294 )
1298 )
1295 elif repo.commit_ids:
1299 elif repo.commit_ids:
1296 rev = repo.commit_ids[0]
1300 rev = repo.commit_ids[0]
1297 selected = 'rev:%s:%s' % (rev, rev)
1301 selected = 'rev:%s:%s' % (rev, rev)
1298 else:
1302 else:
1299 raise EmptyRepositoryError()
1303 raise EmptyRepositoryError()
1300 return groups, selected
1304 return groups, selected
1301
1305
1302 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1306 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1303 return self._get_diff_from_pr_or_version(
1307 return self._get_diff_from_pr_or_version(
1304 source_repo, source_ref_id, target_ref_id, context=context)
1308 source_repo, source_ref_id, target_ref_id, context=context)
1305
1309
1306 def _get_diff_from_pr_or_version(
1310 def _get_diff_from_pr_or_version(
1307 self, source_repo, source_ref_id, target_ref_id, context):
1311 self, source_repo, source_ref_id, target_ref_id, context):
1308 target_commit = source_repo.get_commit(
1312 target_commit = source_repo.get_commit(
1309 commit_id=safe_str(target_ref_id))
1313 commit_id=safe_str(target_ref_id))
1310 source_commit = source_repo.get_commit(
1314 source_commit = source_repo.get_commit(
1311 commit_id=safe_str(source_ref_id))
1315 commit_id=safe_str(source_ref_id))
1312 if isinstance(source_repo, Repository):
1316 if isinstance(source_repo, Repository):
1313 vcs_repo = source_repo.scm_instance()
1317 vcs_repo = source_repo.scm_instance()
1314 else:
1318 else:
1315 vcs_repo = source_repo
1319 vcs_repo = source_repo
1316
1320
1317 # TODO: johbo: In the context of an update, we cannot reach
1321 # TODO: johbo: In the context of an update, we cannot reach
1318 # the old commit anymore with our normal mechanisms. It needs
1322 # the old commit anymore with our normal mechanisms. It needs
1319 # some sort of special support in the vcs layer to avoid this
1323 # some sort of special support in the vcs layer to avoid this
1320 # workaround.
1324 # workaround.
1321 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1325 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1322 vcs_repo.alias == 'git'):
1326 vcs_repo.alias == 'git'):
1323 source_commit.raw_id = safe_str(source_ref_id)
1327 source_commit.raw_id = safe_str(source_ref_id)
1324
1328
1325 log.debug('calculating diff between '
1329 log.debug('calculating diff between '
1326 'source_ref:%s and target_ref:%s for repo `%s`',
1330 'source_ref:%s and target_ref:%s for repo `%s`',
1327 target_ref_id, source_ref_id,
1331 target_ref_id, source_ref_id,
1328 safe_unicode(vcs_repo.path))
1332 safe_unicode(vcs_repo.path))
1329
1333
1330 vcs_diff = vcs_repo.get_diff(
1334 vcs_diff = vcs_repo.get_diff(
1331 commit1=target_commit, commit2=source_commit, context=context)
1335 commit1=target_commit, commit2=source_commit, context=context)
1332 return vcs_diff
1336 return vcs_diff
1333
1337
1334 def _is_merge_enabled(self, pull_request):
1338 def _is_merge_enabled(self, pull_request):
1335 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1339 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1336 settings = settings_model.get_general_settings()
1340 settings = settings_model.get_general_settings()
1337 return settings.get('rhodecode_pr_merge_enabled', False)
1341 return settings.get('rhodecode_pr_merge_enabled', False)
1338
1342
1339 def _use_rebase_for_merging(self, pull_request):
1343 def _use_rebase_for_merging(self, pull_request):
1340 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1344 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1341 settings = settings_model.get_general_settings()
1345 settings = settings_model.get_general_settings()
1342 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1346 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1343
1347
1344 def _log_action(self, action, user, pull_request):
1348 def _log_action(self, action, user, pull_request):
1345 action_logger(
1349 action_logger(
1346 user,
1350 user,
1347 '{action}:{pr_id}'.format(
1351 '{action}:{pr_id}'.format(
1348 action=action, pr_id=pull_request.pull_request_id),
1352 action=action, pr_id=pull_request.pull_request_id),
1349 pull_request.target_repo)
1353 pull_request.target_repo)
1350
1354
1351
1355
1352 class MergeCheck(object):
1356 class MergeCheck(object):
1353 """
1357 """
1354 Perform Merge Checks and returns a check object which stores information
1358 Perform Merge Checks and returns a check object which stores information
1355 about merge errors, and merge conditions
1359 about merge errors, and merge conditions
1356 """
1360 """
1357 TODO_CHECK = 'todo'
1361 TODO_CHECK = 'todo'
1358 PERM_CHECK = 'perm'
1362 PERM_CHECK = 'perm'
1359 REVIEW_CHECK = 'review'
1363 REVIEW_CHECK = 'review'
1360 MERGE_CHECK = 'merge'
1364 MERGE_CHECK = 'merge'
1361
1365
1362 def __init__(self):
1366 def __init__(self):
1363 self.review_status = None
1367 self.review_status = None
1364 self.merge_possible = None
1368 self.merge_possible = None
1365 self.merge_msg = ''
1369 self.merge_msg = ''
1366 self.failed = None
1370 self.failed = None
1367 self.errors = []
1371 self.errors = []
1368 self.error_details = OrderedDict()
1372 self.error_details = OrderedDict()
1369
1373
1370 def push_error(self, error_type, message, error_key, details):
1374 def push_error(self, error_type, message, error_key, details):
1371 self.failed = True
1375 self.failed = True
1372 self.errors.append([error_type, message])
1376 self.errors.append([error_type, message])
1373 self.error_details[error_key] = dict(
1377 self.error_details[error_key] = dict(
1374 details=details,
1378 details=details,
1375 error_type=error_type,
1379 error_type=error_type,
1376 message=message
1380 message=message
1377 )
1381 )
1378
1382
1379 @classmethod
1383 @classmethod
1380 def validate(cls, pull_request, user, fail_early=False, translator=None):
1384 def validate(cls, pull_request, user, fail_early=False, translator=None):
1381 # if migrated to pyramid...
1385 # if migrated to pyramid...
1382 # _ = lambda: translator or _ # use passed in translator if any
1386 # _ = lambda: translator or _ # use passed in translator if any
1383
1387
1384 merge_check = cls()
1388 merge_check = cls()
1385
1389
1386 # permissions to merge
1390 # permissions to merge
1387 user_allowed_to_merge = PullRequestModel().check_user_merge(
1391 user_allowed_to_merge = PullRequestModel().check_user_merge(
1388 pull_request, user)
1392 pull_request, user)
1389 if not user_allowed_to_merge:
1393 if not user_allowed_to_merge:
1390 log.debug("MergeCheck: cannot merge, approval is pending.")
1394 log.debug("MergeCheck: cannot merge, approval is pending.")
1391
1395
1392 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1396 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1393 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1397 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1394 if fail_early:
1398 if fail_early:
1395 return merge_check
1399 return merge_check
1396
1400
1397 # review status, must be always present
1401 # review status, must be always present
1398 review_status = pull_request.calculated_review_status()
1402 review_status = pull_request.calculated_review_status()
1399 merge_check.review_status = review_status
1403 merge_check.review_status = review_status
1400
1404
1401 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1405 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1402 if not status_approved:
1406 if not status_approved:
1403 log.debug("MergeCheck: cannot merge, approval is pending.")
1407 log.debug("MergeCheck: cannot merge, approval is pending.")
1404
1408
1405 msg = _('Pull request reviewer approval is pending.')
1409 msg = _('Pull request reviewer approval is pending.')
1406
1410
1407 merge_check.push_error(
1411 merge_check.push_error(
1408 'warning', msg, cls.REVIEW_CHECK, review_status)
1412 'warning', msg, cls.REVIEW_CHECK, review_status)
1409
1413
1410 if fail_early:
1414 if fail_early:
1411 return merge_check
1415 return merge_check
1412
1416
1413 # left over TODOs
1417 # left over TODOs
1414 todos = CommentsModel().get_unresolved_todos(pull_request)
1418 todos = CommentsModel().get_unresolved_todos(pull_request)
1415 if todos:
1419 if todos:
1416 log.debug("MergeCheck: cannot merge, {} "
1420 log.debug("MergeCheck: cannot merge, {} "
1417 "unresolved todos left.".format(len(todos)))
1421 "unresolved todos left.".format(len(todos)))
1418
1422
1419 if len(todos) == 1:
1423 if len(todos) == 1:
1420 msg = _('Cannot merge, {} TODO still not resolved.').format(
1424 msg = _('Cannot merge, {} TODO still not resolved.').format(
1421 len(todos))
1425 len(todos))
1422 else:
1426 else:
1423 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1427 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1424 len(todos))
1428 len(todos))
1425
1429
1426 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1430 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1427
1431
1428 if fail_early:
1432 if fail_early:
1429 return merge_check
1433 return merge_check
1430
1434
1431 # merge possible
1435 # merge possible
1432 merge_status, msg = PullRequestModel().merge_status(pull_request)
1436 merge_status, msg = PullRequestModel().merge_status(pull_request)
1433 merge_check.merge_possible = merge_status
1437 merge_check.merge_possible = merge_status
1434 merge_check.merge_msg = msg
1438 merge_check.merge_msg = msg
1435 if not merge_status:
1439 if not merge_status:
1436 log.debug(
1440 log.debug(
1437 "MergeCheck: cannot merge, pull request merge not possible.")
1441 "MergeCheck: cannot merge, pull request merge not possible.")
1438 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1442 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1439
1443
1440 if fail_early:
1444 if fail_early:
1441 return merge_check
1445 return merge_check
1442
1446
1443 return merge_check
1447 return merge_check
1444
1448
1445
1449
1446 ChangeTuple = namedtuple('ChangeTuple',
1450 ChangeTuple = namedtuple('ChangeTuple',
1447 ['added', 'common', 'removed', 'total'])
1451 ['added', 'common', 'removed', 'total'])
1448
1452
1449 FileChangeTuple = namedtuple('FileChangeTuple',
1453 FileChangeTuple = namedtuple('FileChangeTuple',
1450 ['added', 'modified', 'removed'])
1454 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now