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