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