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