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