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