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