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