##// END OF EJS Templates
settings: Read 'rebase-merge' config option and pass it to the VCS insntacen on merge.
Martin Bornhold -
r361:813d7f2f default
parent child Browse files
Show More
@@ -1,1141 +1,1148 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 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
401 if merge_state.executed:
401 if merge_state.executed:
402 log.debug(
402 log.debug(
403 "Merge was successful, updating the pull request comments.")
403 "Merge was successful, updating the pull request comments.")
404 self._comment_and_close_pr(pull_request, user, merge_state)
404 self._comment_and_close_pr(pull_request, user, merge_state)
405 self._log_action('user_merged_pull_request', user, pull_request)
405 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
406 else:
407 log.warn("Merge failed, not updating the pull request.")
407 log.warn("Merge failed, not updating the pull request.")
408 return merge_state
408 return merge_state
409
409
410 def _merge_pull_request(self, pull_request, user, extras):
410 def _merge_pull_request(self, pull_request, user, extras):
411 target_vcs = pull_request.target_repo.scm_instance()
411 target_vcs = pull_request.target_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
413 target_ref = self._refresh_reference(
413 target_ref = self._refresh_reference(
414 pull_request.target_ref_parts, target_vcs)
414 pull_request.target_ref_parts, target_vcs)
415
415
416 message = _(
416 message = _(
417 'Merge pull request #%(pr_id)s from '
417 'Merge pull request #%(pr_id)s from '
418 '%(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') % {
419 'pr_id': pull_request.pull_request_id,
419 'pr_id': pull_request.pull_request_id,
420 'source_repo': source_vcs.name,
420 'source_repo': source_vcs.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
422 'pr_title': pull_request.title
422 'pr_title': pull_request.title
423 }
423 }
424
424
425 workspace_id = self._workspace_id(pull_request)
425 workspace_id = self._workspace_id(pull_request)
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
428 use_rebase = self._use_rebase_for_merging(pull_request)
428
429
429 callback_daemon, extras = prepare_callback_daemon(
430 callback_daemon, extras = prepare_callback_daemon(
430 extras, protocol=protocol, use_direct_calls=use_direct_calls)
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
431
432
432 with callback_daemon:
433 with callback_daemon:
433 # TODO: johbo: Implement a clean way to run a config_override
434 # TODO: johbo: Implement a clean way to run a config_override
434 # for a single call.
435 # for a single call.
435 target_vcs.config.set(
436 target_vcs.config.set(
436 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 merge_state = target_vcs.merge(
438 merge_state = target_vcs.merge(
438 target_ref, source_vcs, pull_request.source_ref_parts,
439 target_ref, source_vcs, pull_request.source_ref_parts,
439 workspace_id, user_name=user.username,
440 workspace_id, user_name=user.username,
440 user_email=user.email, message=message)
441 user_email=user.email, message=message, use_rebase=use_rebase)
441 return merge_state
442 return merge_state
442
443
443 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 pull_request.merge_rev = merge_state.merge_commit_id
445 pull_request.merge_rev = merge_state.merge_commit_id
445 pull_request.updated_on = datetime.datetime.now()
446 pull_request.updated_on = datetime.datetime.now()
446
447
447 ChangesetCommentsModel().create(
448 ChangesetCommentsModel().create(
448 text=unicode(_('Pull request merged and closed')),
449 text=unicode(_('Pull request merged and closed')),
449 repo=pull_request.target_repo.repo_id,
450 repo=pull_request.target_repo.repo_id,
450 user=user.user_id,
451 user=user.user_id,
451 pull_request=pull_request.pull_request_id,
452 pull_request=pull_request.pull_request_id,
452 f_path=None,
453 f_path=None,
453 line_no=None,
454 line_no=None,
454 closing_pr=True
455 closing_pr=True
455 )
456 )
456
457
457 Session().add(pull_request)
458 Session().add(pull_request)
458 Session().flush()
459 Session().flush()
459 # TODO: paris: replace invalidation with less radical solution
460 # TODO: paris: replace invalidation with less radical solution
460 ScmModel().mark_for_invalidation(
461 ScmModel().mark_for_invalidation(
461 pull_request.target_repo.repo_name)
462 pull_request.target_repo.repo_name)
462 self._trigger_pull_request_hook(pull_request, user, 'merge')
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
463
464
464 def has_valid_update_type(self, pull_request):
465 def has_valid_update_type(self, pull_request):
465 source_ref_type = pull_request.source_ref_parts.type
466 source_ref_type = pull_request.source_ref_parts.type
466 return source_ref_type in ['book', 'branch', 'tag']
467 return source_ref_type in ['book', 'branch', 'tag']
467
468
468 def update_commits(self, pull_request):
469 def update_commits(self, pull_request):
469 """
470 """
470 Get the updated list of commits for the pull request
471 Get the updated list of commits for the pull request
471 and return the new pull request version and the list
472 and return the new pull request version and the list
472 of commits processed by this update action
473 of commits processed by this update action
473 """
474 """
474
475
475 pull_request = self.__get_pull_request(pull_request)
476 pull_request = self.__get_pull_request(pull_request)
476 source_ref_type = pull_request.source_ref_parts.type
477 source_ref_type = pull_request.source_ref_parts.type
477 source_ref_name = pull_request.source_ref_parts.name
478 source_ref_name = pull_request.source_ref_parts.name
478 source_ref_id = pull_request.source_ref_parts.commit_id
479 source_ref_id = pull_request.source_ref_parts.commit_id
479
480
480 if not self.has_valid_update_type(pull_request):
481 if not self.has_valid_update_type(pull_request):
481 log.debug(
482 log.debug(
482 "Skipping update of pull request %s due to ref type: %s",
483 "Skipping update of pull request %s due to ref type: %s",
483 pull_request, source_ref_type)
484 pull_request, source_ref_type)
484 return (None, None)
485 return (None, None)
485
486
486 source_repo = pull_request.source_repo.scm_instance()
487 source_repo = pull_request.source_repo.scm_instance()
487 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 if source_ref_id == source_commit.raw_id:
489 if source_ref_id == source_commit.raw_id:
489 log.debug("Nothing changed in pull request %s", pull_request)
490 log.debug("Nothing changed in pull request %s", pull_request)
490 return (None, None)
491 return (None, None)
491
492
492 # Finally there is a need for an update
493 # Finally there is a need for an update
493 pull_request_version = self._create_version_from_snapshot(pull_request)
494 pull_request_version = self._create_version_from_snapshot(pull_request)
494 self._link_comments_to_version(pull_request_version)
495 self._link_comments_to_version(pull_request_version)
495
496
496 target_ref_type = pull_request.target_ref_parts.type
497 target_ref_type = pull_request.target_ref_parts.type
497 target_ref_name = pull_request.target_ref_parts.name
498 target_ref_name = pull_request.target_ref_parts.name
498 target_ref_id = pull_request.target_ref_parts.commit_id
499 target_ref_id = pull_request.target_ref_parts.commit_id
499 target_repo = pull_request.target_repo.scm_instance()
500 target_repo = pull_request.target_repo.scm_instance()
500
501
501 if target_ref_type in ('tag', 'branch', 'book'):
502 if target_ref_type in ('tag', 'branch', 'book'):
502 target_commit = target_repo.get_commit(target_ref_name)
503 target_commit = target_repo.get_commit(target_ref_name)
503 else:
504 else:
504 target_commit = target_repo.get_commit(target_ref_id)
505 target_commit = target_repo.get_commit(target_ref_id)
505
506
506 # re-compute commit ids
507 # re-compute commit ids
507 old_commit_ids = set(pull_request.revisions)
508 old_commit_ids = set(pull_request.revisions)
508 pre_load = ["author", "branch", "date", "message"]
509 pre_load = ["author", "branch", "date", "message"]
509 commit_ranges = target_repo.compare(
510 commit_ranges = target_repo.compare(
510 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 pre_load=pre_load)
512 pre_load=pre_load)
512
513
513 ancestor = target_repo.get_common_ancestor(
514 ancestor = target_repo.get_common_ancestor(
514 target_commit.raw_id, source_commit.raw_id, source_repo)
515 target_commit.raw_id, source_commit.raw_id, source_repo)
515
516
516 pull_request.source_ref = '%s:%s:%s' % (
517 pull_request.source_ref = '%s:%s:%s' % (
517 source_ref_type, source_ref_name, source_commit.raw_id)
518 source_ref_type, source_ref_name, source_commit.raw_id)
518 pull_request.target_ref = '%s:%s:%s' % (
519 pull_request.target_ref = '%s:%s:%s' % (
519 target_ref_type, target_ref_name, ancestor)
520 target_ref_type, target_ref_name, ancestor)
520 pull_request.revisions = [
521 pull_request.revisions = [
521 commit.raw_id for commit in reversed(commit_ranges)]
522 commit.raw_id for commit in reversed(commit_ranges)]
522 pull_request.updated_on = datetime.datetime.now()
523 pull_request.updated_on = datetime.datetime.now()
523 Session().add(pull_request)
524 Session().add(pull_request)
524 new_commit_ids = set(pull_request.revisions)
525 new_commit_ids = set(pull_request.revisions)
525
526
526 changes = self._calculate_commit_id_changes(
527 changes = self._calculate_commit_id_changes(
527 old_commit_ids, new_commit_ids)
528 old_commit_ids, new_commit_ids)
528
529
529 old_diff_data, new_diff_data = self._generate_update_diffs(
530 old_diff_data, new_diff_data = self._generate_update_diffs(
530 pull_request, pull_request_version)
531 pull_request, pull_request_version)
531
532
532 ChangesetCommentsModel().outdate_comments(
533 ChangesetCommentsModel().outdate_comments(
533 pull_request, old_diff_data=old_diff_data,
534 pull_request, old_diff_data=old_diff_data,
534 new_diff_data=new_diff_data)
535 new_diff_data=new_diff_data)
535
536
536 file_changes = self._calculate_file_changes(
537 file_changes = self._calculate_file_changes(
537 old_diff_data, new_diff_data)
538 old_diff_data, new_diff_data)
538
539
539 # Add an automatic comment to the pull request
540 # Add an automatic comment to the pull request
540 update_comment = ChangesetCommentsModel().create(
541 update_comment = ChangesetCommentsModel().create(
541 text=self._render_update_message(changes, file_changes),
542 text=self._render_update_message(changes, file_changes),
542 repo=pull_request.target_repo,
543 repo=pull_request.target_repo,
543 user=pull_request.author,
544 user=pull_request.author,
544 pull_request=pull_request,
545 pull_request=pull_request,
545 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546
547
547 # Update status to "Under Review" for added commits
548 # Update status to "Under Review" for added commits
548 for commit_id in changes.added:
549 for commit_id in changes.added:
549 ChangesetStatusModel().set_status(
550 ChangesetStatusModel().set_status(
550 repo=pull_request.source_repo,
551 repo=pull_request.source_repo,
551 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 comment=update_comment,
553 comment=update_comment,
553 user=pull_request.author,
554 user=pull_request.author,
554 pull_request=pull_request,
555 pull_request=pull_request,
555 revision=commit_id)
556 revision=commit_id)
556
557
557 log.debug(
558 log.debug(
558 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 'removed_ids: %s', pull_request.pull_request_id,
560 'removed_ids: %s', pull_request.pull_request_id,
560 changes.added, changes.common, changes.removed)
561 changes.added, changes.common, changes.removed)
561 log.debug('Updated pull request with the following file changes: %s',
562 log.debug('Updated pull request with the following file changes: %s',
562 file_changes)
563 file_changes)
563
564
564 log.info(
565 log.info(
565 "Updated pull request %s from commit %s to commit %s, "
566 "Updated pull request %s from commit %s to commit %s, "
566 "stored new version %s of this pull request.",
567 "stored new version %s of this pull request.",
567 pull_request.pull_request_id, source_ref_id,
568 pull_request.pull_request_id, source_ref_id,
568 pull_request.source_ref_parts.commit_id,
569 pull_request.source_ref_parts.commit_id,
569 pull_request_version.pull_request_version_id)
570 pull_request_version.pull_request_version_id)
570 Session().commit()
571 Session().commit()
571 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 'update')
573 'update')
573 return (pull_request_version, changes)
574 return (pull_request_version, changes)
574
575
575 def _create_version_from_snapshot(self, pull_request):
576 def _create_version_from_snapshot(self, pull_request):
576 version = PullRequestVersion()
577 version = PullRequestVersion()
577 version.title = pull_request.title
578 version.title = pull_request.title
578 version.description = pull_request.description
579 version.description = pull_request.description
579 version.status = pull_request.status
580 version.status = pull_request.status
580 version.created_on = pull_request.created_on
581 version.created_on = pull_request.created_on
581 version.updated_on = pull_request.updated_on
582 version.updated_on = pull_request.updated_on
582 version.user_id = pull_request.user_id
583 version.user_id = pull_request.user_id
583 version.source_repo = pull_request.source_repo
584 version.source_repo = pull_request.source_repo
584 version.source_ref = pull_request.source_ref
585 version.source_ref = pull_request.source_ref
585 version.target_repo = pull_request.target_repo
586 version.target_repo = pull_request.target_repo
586 version.target_ref = pull_request.target_ref
587 version.target_ref = pull_request.target_ref
587
588
588 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 version._last_merge_status = pull_request._last_merge_status
591 version._last_merge_status = pull_request._last_merge_status
591 version.merge_rev = pull_request.merge_rev
592 version.merge_rev = pull_request.merge_rev
592
593
593 version.revisions = pull_request.revisions
594 version.revisions = pull_request.revisions
594 version.pull_request = pull_request
595 version.pull_request = pull_request
595 Session().add(version)
596 Session().add(version)
596 Session().flush()
597 Session().flush()
597
598
598 return version
599 return version
599
600
600 def _generate_update_diffs(self, pull_request, pull_request_version):
601 def _generate_update_diffs(self, pull_request, pull_request_version):
601 diff_context = (
602 diff_context = (
602 self.DIFF_CONTEXT +
603 self.DIFF_CONTEXT +
603 ChangesetCommentsModel.needed_extra_diff_context())
604 ChangesetCommentsModel.needed_extra_diff_context())
604 old_diff = self._get_diff_from_pr_or_version(
605 old_diff = self._get_diff_from_pr_or_version(
605 pull_request_version, context=diff_context)
606 pull_request_version, context=diff_context)
606 new_diff = self._get_diff_from_pr_or_version(
607 new_diff = self._get_diff_from_pr_or_version(
607 pull_request, context=diff_context)
608 pull_request, context=diff_context)
608
609
609 old_diff_data = diffs.DiffProcessor(old_diff)
610 old_diff_data = diffs.DiffProcessor(old_diff)
610 old_diff_data.prepare()
611 old_diff_data.prepare()
611 new_diff_data = diffs.DiffProcessor(new_diff)
612 new_diff_data = diffs.DiffProcessor(new_diff)
612 new_diff_data.prepare()
613 new_diff_data.prepare()
613
614
614 return old_diff_data, new_diff_data
615 return old_diff_data, new_diff_data
615
616
616 def _link_comments_to_version(self, pull_request_version):
617 def _link_comments_to_version(self, pull_request_version):
617 """
618 """
618 Link all unlinked comments of this pull request to the given version.
619 Link all unlinked comments of this pull request to the given version.
619
620
620 :param pull_request_version: The `PullRequestVersion` to which
621 :param pull_request_version: The `PullRequestVersion` to which
621 the comments shall be linked.
622 the comments shall be linked.
622
623
623 """
624 """
624 pull_request = pull_request_version.pull_request
625 pull_request = pull_request_version.pull_request
625 comments = ChangesetComment.query().filter(
626 comments = ChangesetComment.query().filter(
626 # TODO: johbo: Should we query for the repo at all here?
627 # TODO: johbo: Should we query for the repo at all here?
627 # Pending decision on how comments of PRs are to be related
628 # Pending decision on how comments of PRs are to be related
628 # to either the source repo, the target repo or no repo at all.
629 # to either the source repo, the target repo or no repo at all.
629 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 ChangesetComment.pull_request == pull_request,
631 ChangesetComment.pull_request == pull_request,
631 ChangesetComment.pull_request_version == None)
632 ChangesetComment.pull_request_version == None)
632
633
633 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 # operation.
635 # operation.
635 for comment in comments:
636 for comment in comments:
636 comment.pull_request_version_id = (
637 comment.pull_request_version_id = (
637 pull_request_version.pull_request_version_id)
638 pull_request_version.pull_request_version_id)
638 Session().add(comment)
639 Session().add(comment)
639
640
640 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 added = new_ids.difference(old_ids)
642 added = new_ids.difference(old_ids)
642 common = old_ids.intersection(new_ids)
643 common = old_ids.intersection(new_ids)
643 removed = old_ids.difference(new_ids)
644 removed = old_ids.difference(new_ids)
644 return ChangeTuple(added, common, removed)
645 return ChangeTuple(added, common, removed)
645
646
646 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647
648
648 old_files = OrderedDict()
649 old_files = OrderedDict()
649 for diff_data in old_diff_data.parsed_diff:
650 for diff_data in old_diff_data.parsed_diff:
650 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651
652
652 added_files = []
653 added_files = []
653 modified_files = []
654 modified_files = []
654 removed_files = []
655 removed_files = []
655 for diff_data in new_diff_data.parsed_diff:
656 for diff_data in new_diff_data.parsed_diff:
656 new_filename = diff_data['filename']
657 new_filename = diff_data['filename']
657 new_hash = md5_safe(diff_data['raw_diff'])
658 new_hash = md5_safe(diff_data['raw_diff'])
658
659
659 old_hash = old_files.get(new_filename)
660 old_hash = old_files.get(new_filename)
660 if not old_hash:
661 if not old_hash:
661 # file is not present in old diff, means it's added
662 # file is not present in old diff, means it's added
662 added_files.append(new_filename)
663 added_files.append(new_filename)
663 else:
664 else:
664 if new_hash != old_hash:
665 if new_hash != old_hash:
665 modified_files.append(new_filename)
666 modified_files.append(new_filename)
666 # now remove a file from old, since we have seen it already
667 # now remove a file from old, since we have seen it already
667 del old_files[new_filename]
668 del old_files[new_filename]
668
669
669 # removed files is when there are present in old, but not in NEW,
670 # removed files is when there are present in old, but not in NEW,
670 # since we remove old files that are present in new diff, left-overs
671 # since we remove old files that are present in new diff, left-overs
671 # if any should be the removed files
672 # if any should be the removed files
672 removed_files.extend(old_files.keys())
673 removed_files.extend(old_files.keys())
673
674
674 return FileChangeTuple(added_files, modified_files, removed_files)
675 return FileChangeTuple(added_files, modified_files, removed_files)
675
676
676 def _render_update_message(self, changes, file_changes):
677 def _render_update_message(self, changes, file_changes):
677 """
678 """
678 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 so it's always looking the same disregarding on which default
680 so it's always looking the same disregarding on which default
680 renderer system is using.
681 renderer system is using.
681
682
682 :param changes: changes named tuple
683 :param changes: changes named tuple
683 :param file_changes: file changes named tuple
684 :param file_changes: file changes named tuple
684
685
685 """
686 """
686 new_status = ChangesetStatus.get_status_lbl(
687 new_status = ChangesetStatus.get_status_lbl(
687 ChangesetStatus.STATUS_UNDER_REVIEW)
688 ChangesetStatus.STATUS_UNDER_REVIEW)
688
689
689 changed_files = (
690 changed_files = (
690 file_changes.added + file_changes.modified + file_changes.removed)
691 file_changes.added + file_changes.modified + file_changes.removed)
691
692
692 params = {
693 params = {
693 'under_review_label': new_status,
694 'under_review_label': new_status,
694 'added_commits': changes.added,
695 'added_commits': changes.added,
695 'removed_commits': changes.removed,
696 'removed_commits': changes.removed,
696 'changed_files': changed_files,
697 'changed_files': changed_files,
697 'added_files': file_changes.added,
698 'added_files': file_changes.added,
698 'modified_files': file_changes.modified,
699 'modified_files': file_changes.modified,
699 'removed_files': file_changes.removed,
700 'removed_files': file_changes.removed,
700 }
701 }
701 renderer = RstTemplateRenderer()
702 renderer = RstTemplateRenderer()
702 return renderer.render('pull_request_update.mako', **params)
703 return renderer.render('pull_request_update.mako', **params)
703
704
704 def edit(self, pull_request, title, description):
705 def edit(self, pull_request, title, description):
705 pull_request = self.__get_pull_request(pull_request)
706 pull_request = self.__get_pull_request(pull_request)
706 if pull_request.is_closed():
707 if pull_request.is_closed():
707 raise ValueError('This pull request is closed')
708 raise ValueError('This pull request is closed')
708 if title:
709 if title:
709 pull_request.title = title
710 pull_request.title = title
710 pull_request.description = description
711 pull_request.description = description
711 pull_request.updated_on = datetime.datetime.now()
712 pull_request.updated_on = datetime.datetime.now()
712 Session().add(pull_request)
713 Session().add(pull_request)
713
714
714 def update_reviewers(self, pull_request, reviewers_ids):
715 def update_reviewers(self, pull_request, reviewers_ids):
715 reviewers_ids = set(reviewers_ids)
716 reviewers_ids = set(reviewers_ids)
716 pull_request = self.__get_pull_request(pull_request)
717 pull_request = self.__get_pull_request(pull_request)
717 current_reviewers = PullRequestReviewers.query()\
718 current_reviewers = PullRequestReviewers.query()\
718 .filter(PullRequestReviewers.pull_request ==
719 .filter(PullRequestReviewers.pull_request ==
719 pull_request).all()
720 pull_request).all()
720 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721
722
722 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724
725
725 log.debug("Adding %s reviewers", ids_to_add)
726 log.debug("Adding %s reviewers", ids_to_add)
726 log.debug("Removing %s reviewers", ids_to_remove)
727 log.debug("Removing %s reviewers", ids_to_remove)
727 changed = False
728 changed = False
728 for uid in ids_to_add:
729 for uid in ids_to_add:
729 changed = True
730 changed = True
730 _usr = self._get_user(uid)
731 _usr = self._get_user(uid)
731 reviewer = PullRequestReviewers(_usr, pull_request)
732 reviewer = PullRequestReviewers(_usr, pull_request)
732 Session().add(reviewer)
733 Session().add(reviewer)
733
734
734 self.notify_reviewers(pull_request, ids_to_add)
735 self.notify_reviewers(pull_request, ids_to_add)
735
736
736 for uid in ids_to_remove:
737 for uid in ids_to_remove:
737 changed = True
738 changed = True
738 reviewer = PullRequestReviewers.query()\
739 reviewer = PullRequestReviewers.query()\
739 .filter(PullRequestReviewers.user_id == uid,
740 .filter(PullRequestReviewers.user_id == uid,
740 PullRequestReviewers.pull_request == pull_request)\
741 PullRequestReviewers.pull_request == pull_request)\
741 .scalar()
742 .scalar()
742 if reviewer:
743 if reviewer:
743 Session().delete(reviewer)
744 Session().delete(reviewer)
744 if changed:
745 if changed:
745 pull_request.updated_on = datetime.datetime.now()
746 pull_request.updated_on = datetime.datetime.now()
746 Session().add(pull_request)
747 Session().add(pull_request)
747
748
748 return ids_to_add, ids_to_remove
749 return ids_to_add, ids_to_remove
749
750
750 def notify_reviewers(self, pull_request, reviewers_ids):
751 def notify_reviewers(self, pull_request, reviewers_ids):
751 # notification to reviewers
752 # notification to reviewers
752 if not reviewers_ids:
753 if not reviewers_ids:
753 return
754 return
754
755
755 pull_request_obj = pull_request
756 pull_request_obj = pull_request
756 # get the current participants of this pull request
757 # get the current participants of this pull request
757 recipients = reviewers_ids
758 recipients = reviewers_ids
758 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
759 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
759
760
760 pr_source_repo = pull_request_obj.source_repo
761 pr_source_repo = pull_request_obj.source_repo
761 pr_target_repo = pull_request_obj.target_repo
762 pr_target_repo = pull_request_obj.target_repo
762
763
763 pr_url = h.url(
764 pr_url = h.url(
764 'pullrequest_show',
765 'pullrequest_show',
765 repo_name=pr_target_repo.repo_name,
766 repo_name=pr_target_repo.repo_name,
766 pull_request_id=pull_request_obj.pull_request_id,
767 pull_request_id=pull_request_obj.pull_request_id,
767 qualified=True,)
768 qualified=True,)
768
769
769 # set some variables for email notification
770 # set some variables for email notification
770 pr_target_repo_url = h.url(
771 pr_target_repo_url = h.url(
771 'summary_home',
772 'summary_home',
772 repo_name=pr_target_repo.repo_name,
773 repo_name=pr_target_repo.repo_name,
773 qualified=True)
774 qualified=True)
774
775
775 pr_source_repo_url = h.url(
776 pr_source_repo_url = h.url(
776 'summary_home',
777 'summary_home',
777 repo_name=pr_source_repo.repo_name,
778 repo_name=pr_source_repo.repo_name,
778 qualified=True)
779 qualified=True)
779
780
780 # pull request specifics
781 # pull request specifics
781 pull_request_commits = [
782 pull_request_commits = [
782 (x.raw_id, x.message)
783 (x.raw_id, x.message)
783 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
784 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
784
785
785 kwargs = {
786 kwargs = {
786 'user': pull_request.author,
787 'user': pull_request.author,
787 'pull_request': pull_request_obj,
788 'pull_request': pull_request_obj,
788 'pull_request_commits': pull_request_commits,
789 'pull_request_commits': pull_request_commits,
789
790
790 'pull_request_target_repo': pr_target_repo,
791 'pull_request_target_repo': pr_target_repo,
791 'pull_request_target_repo_url': pr_target_repo_url,
792 'pull_request_target_repo_url': pr_target_repo_url,
792
793
793 'pull_request_source_repo': pr_source_repo,
794 'pull_request_source_repo': pr_source_repo,
794 'pull_request_source_repo_url': pr_source_repo_url,
795 'pull_request_source_repo_url': pr_source_repo_url,
795
796
796 'pull_request_url': pr_url,
797 'pull_request_url': pr_url,
797 }
798 }
798
799
799 # pre-generate the subject for notification itself
800 # pre-generate the subject for notification itself
800 (subject,
801 (subject,
801 _h, _e, # we don't care about those
802 _h, _e, # we don't care about those
802 body_plaintext) = EmailNotificationModel().render_email(
803 body_plaintext) = EmailNotificationModel().render_email(
803 notification_type, **kwargs)
804 notification_type, **kwargs)
804
805
805 # create notification objects, and emails
806 # create notification objects, and emails
806 NotificationModel().create(
807 NotificationModel().create(
807 created_by=pull_request.author,
808 created_by=pull_request.author,
808 notification_subject=subject,
809 notification_subject=subject,
809 notification_body=body_plaintext,
810 notification_body=body_plaintext,
810 notification_type=notification_type,
811 notification_type=notification_type,
811 recipients=recipients,
812 recipients=recipients,
812 email_kwargs=kwargs,
813 email_kwargs=kwargs,
813 )
814 )
814
815
815 def delete(self, pull_request):
816 def delete(self, pull_request):
816 pull_request = self.__get_pull_request(pull_request)
817 pull_request = self.__get_pull_request(pull_request)
817 self._cleanup_merge_workspace(pull_request)
818 self._cleanup_merge_workspace(pull_request)
818 Session().delete(pull_request)
819 Session().delete(pull_request)
819
820
820 def close_pull_request(self, pull_request, user):
821 def close_pull_request(self, pull_request, user):
821 pull_request = self.__get_pull_request(pull_request)
822 pull_request = self.__get_pull_request(pull_request)
822 self._cleanup_merge_workspace(pull_request)
823 self._cleanup_merge_workspace(pull_request)
823 pull_request.status = PullRequest.STATUS_CLOSED
824 pull_request.status = PullRequest.STATUS_CLOSED
824 pull_request.updated_on = datetime.datetime.now()
825 pull_request.updated_on = datetime.datetime.now()
825 Session().add(pull_request)
826 Session().add(pull_request)
826 self._trigger_pull_request_hook(
827 self._trigger_pull_request_hook(
827 pull_request, pull_request.author, 'close')
828 pull_request, pull_request.author, 'close')
828 self._log_action('user_closed_pull_request', user, pull_request)
829 self._log_action('user_closed_pull_request', user, pull_request)
829
830
830 def close_pull_request_with_comment(self, pull_request, user, repo,
831 def close_pull_request_with_comment(self, pull_request, user, repo,
831 message=None):
832 message=None):
832 status = ChangesetStatus.STATUS_REJECTED
833 status = ChangesetStatus.STATUS_REJECTED
833
834
834 if not message:
835 if not message:
835 message = (
836 message = (
836 _('Status change %(transition_icon)s %(status)s') % {
837 _('Status change %(transition_icon)s %(status)s') % {
837 'transition_icon': '>',
838 'transition_icon': '>',
838 'status': ChangesetStatus.get_status_lbl(status)})
839 'status': ChangesetStatus.get_status_lbl(status)})
839
840
840 internal_message = _('Closing with') + ' ' + message
841 internal_message = _('Closing with') + ' ' + message
841
842
842 comm = ChangesetCommentsModel().create(
843 comm = ChangesetCommentsModel().create(
843 text=internal_message,
844 text=internal_message,
844 repo=repo.repo_id,
845 repo=repo.repo_id,
845 user=user.user_id,
846 user=user.user_id,
846 pull_request=pull_request.pull_request_id,
847 pull_request=pull_request.pull_request_id,
847 f_path=None,
848 f_path=None,
848 line_no=None,
849 line_no=None,
849 status_change=ChangesetStatus.get_status_lbl(status),
850 status_change=ChangesetStatus.get_status_lbl(status),
850 closing_pr=True
851 closing_pr=True
851 )
852 )
852
853
853 ChangesetStatusModel().set_status(
854 ChangesetStatusModel().set_status(
854 repo.repo_id,
855 repo.repo_id,
855 status,
856 status,
856 user.user_id,
857 user.user_id,
857 comm,
858 comm,
858 pull_request=pull_request.pull_request_id
859 pull_request=pull_request.pull_request_id
859 )
860 )
860 Session().flush()
861 Session().flush()
861
862
862 PullRequestModel().close_pull_request(
863 PullRequestModel().close_pull_request(
863 pull_request.pull_request_id, user)
864 pull_request.pull_request_id, user)
864
865
865 def merge_status(self, pull_request):
866 def merge_status(self, pull_request):
866 if not self._is_merge_enabled(pull_request):
867 if not self._is_merge_enabled(pull_request):
867 return False, _('Server-side pull request merging is disabled.')
868 return False, _('Server-side pull request merging is disabled.')
868 if pull_request.is_closed():
869 if pull_request.is_closed():
869 return False, _('This pull request is closed.')
870 return False, _('This pull request is closed.')
870 merge_possible, msg = self._check_repo_requirements(
871 merge_possible, msg = self._check_repo_requirements(
871 target=pull_request.target_repo, source=pull_request.source_repo)
872 target=pull_request.target_repo, source=pull_request.source_repo)
872 if not merge_possible:
873 if not merge_possible:
873 return merge_possible, msg
874 return merge_possible, msg
874
875
875 try:
876 try:
876 resp = self._try_merge(pull_request)
877 resp = self._try_merge(pull_request)
877 status = resp.possible, self.merge_status_message(
878 status = resp.possible, self.merge_status_message(
878 resp.failure_reason)
879 resp.failure_reason)
879 except NotImplementedError:
880 except NotImplementedError:
880 status = False, _('Pull request merging is not supported.')
881 status = False, _('Pull request merging is not supported.')
881
882
882 return status
883 return status
883
884
884 def _check_repo_requirements(self, target, source):
885 def _check_repo_requirements(self, target, source):
885 """
886 """
886 Check if `target` and `source` have compatible requirements.
887 Check if `target` and `source` have compatible requirements.
887
888
888 Currently this is just checking for largefiles.
889 Currently this is just checking for largefiles.
889 """
890 """
890 target_has_largefiles = self._has_largefiles(target)
891 target_has_largefiles = self._has_largefiles(target)
891 source_has_largefiles = self._has_largefiles(source)
892 source_has_largefiles = self._has_largefiles(source)
892 merge_possible = True
893 merge_possible = True
893 message = u''
894 message = u''
894
895
895 if target_has_largefiles != source_has_largefiles:
896 if target_has_largefiles != source_has_largefiles:
896 merge_possible = False
897 merge_possible = False
897 if source_has_largefiles:
898 if source_has_largefiles:
898 message = _(
899 message = _(
899 'Target repository large files support is disabled.')
900 'Target repository large files support is disabled.')
900 else:
901 else:
901 message = _(
902 message = _(
902 'Source repository large files support is disabled.')
903 'Source repository large files support is disabled.')
903
904
904 return merge_possible, message
905 return merge_possible, message
905
906
906 def _has_largefiles(self, repo):
907 def _has_largefiles(self, repo):
907 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
908 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
908 'extensions', 'largefiles')
909 'extensions', 'largefiles')
909 return largefiles_ui and largefiles_ui[0].active
910 return largefiles_ui and largefiles_ui[0].active
910
911
911 def _try_merge(self, pull_request):
912 def _try_merge(self, pull_request):
912 """
913 """
913 Try to merge the pull request and return the merge status.
914 Try to merge the pull request and return the merge status.
914 """
915 """
915 log.debug(
916 log.debug(
916 "Trying out if the pull request %s can be merged.",
917 "Trying out if the pull request %s can be merged.",
917 pull_request.pull_request_id)
918 pull_request.pull_request_id)
918 target_vcs = pull_request.target_repo.scm_instance()
919 target_vcs = pull_request.target_repo.scm_instance()
919 target_ref = self._refresh_reference(
920 target_ref = self._refresh_reference(
920 pull_request.target_ref_parts, target_vcs)
921 pull_request.target_ref_parts, target_vcs)
921
922
922 target_locked = pull_request.target_repo.locked
923 target_locked = pull_request.target_repo.locked
923 if target_locked and target_locked[0]:
924 if target_locked and target_locked[0]:
924 log.debug("The target repository is locked.")
925 log.debug("The target repository is locked.")
925 merge_state = MergeResponse(
926 merge_state = MergeResponse(
926 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
927 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
927 elif self._needs_merge_state_refresh(pull_request, target_ref):
928 elif self._needs_merge_state_refresh(pull_request, target_ref):
928 log.debug("Refreshing the merge status of the repository.")
929 log.debug("Refreshing the merge status of the repository.")
929 merge_state = self._refresh_merge_state(
930 merge_state = self._refresh_merge_state(
930 pull_request, target_vcs, target_ref)
931 pull_request, target_vcs, target_ref)
931 else:
932 else:
932 possible = pull_request.\
933 possible = pull_request.\
933 _last_merge_status == MergeFailureReason.NONE
934 _last_merge_status == MergeFailureReason.NONE
934 merge_state = MergeResponse(
935 merge_state = MergeResponse(
935 possible, False, None, pull_request._last_merge_status)
936 possible, False, None, pull_request._last_merge_status)
936 log.debug("Merge response: %s", merge_state)
937 log.debug("Merge response: %s", merge_state)
937 return merge_state
938 return merge_state
938
939
939 def _refresh_reference(self, reference, vcs_repository):
940 def _refresh_reference(self, reference, vcs_repository):
940 if reference.type in ('branch', 'book'):
941 if reference.type in ('branch', 'book'):
941 name_or_id = reference.name
942 name_or_id = reference.name
942 else:
943 else:
943 name_or_id = reference.commit_id
944 name_or_id = reference.commit_id
944 refreshed_commit = vcs_repository.get_commit(name_or_id)
945 refreshed_commit = vcs_repository.get_commit(name_or_id)
945 refreshed_reference = Reference(
946 refreshed_reference = Reference(
946 reference.type, reference.name, refreshed_commit.raw_id)
947 reference.type, reference.name, refreshed_commit.raw_id)
947 return refreshed_reference
948 return refreshed_reference
948
949
949 def _needs_merge_state_refresh(self, pull_request, target_reference):
950 def _needs_merge_state_refresh(self, pull_request, target_reference):
950 return not(
951 return not(
951 pull_request.revisions and
952 pull_request.revisions and
952 pull_request.revisions[0] == pull_request._last_merge_source_rev and
953 pull_request.revisions[0] == pull_request._last_merge_source_rev and
953 target_reference.commit_id == pull_request._last_merge_target_rev)
954 target_reference.commit_id == pull_request._last_merge_target_rev)
954
955
955 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
956 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
956 workspace_id = self._workspace_id(pull_request)
957 workspace_id = self._workspace_id(pull_request)
957 source_vcs = pull_request.source_repo.scm_instance()
958 source_vcs = pull_request.source_repo.scm_instance()
959 use_rebase = self._use_rebase_for_merging(pull_request)
958 merge_state = target_vcs.merge(
960 merge_state = target_vcs.merge(
959 target_reference, source_vcs, pull_request.source_ref_parts,
961 target_reference, source_vcs, pull_request.source_ref_parts,
960 workspace_id, dry_run=True)
962 workspace_id, dry_run=True, use_rebase=use_rebase)
961
963
962 # Do not store the response if there was an unknown error.
964 # Do not store the response if there was an unknown error.
963 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
965 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
964 pull_request._last_merge_source_rev = pull_request.\
966 pull_request._last_merge_source_rev = pull_request.\
965 source_ref_parts.commit_id
967 source_ref_parts.commit_id
966 pull_request._last_merge_target_rev = target_reference.commit_id
968 pull_request._last_merge_target_rev = target_reference.commit_id
967 pull_request._last_merge_status = (
969 pull_request._last_merge_status = (
968 merge_state.failure_reason)
970 merge_state.failure_reason)
969 Session().add(pull_request)
971 Session().add(pull_request)
970 Session().flush()
972 Session().flush()
971
973
972 return merge_state
974 return merge_state
973
975
974 def _workspace_id(self, pull_request):
976 def _workspace_id(self, pull_request):
975 workspace_id = 'pr-%s' % pull_request.pull_request_id
977 workspace_id = 'pr-%s' % pull_request.pull_request_id
976 return workspace_id
978 return workspace_id
977
979
978 def merge_status_message(self, status_code):
980 def merge_status_message(self, status_code):
979 """
981 """
980 Return a human friendly error message for the given merge status code.
982 Return a human friendly error message for the given merge status code.
981 """
983 """
982 return self.MERGE_STATUS_MESSAGES[status_code]
984 return self.MERGE_STATUS_MESSAGES[status_code]
983
985
984 def generate_repo_data(self, repo, commit_id=None, branch=None,
986 def generate_repo_data(self, repo, commit_id=None, branch=None,
985 bookmark=None):
987 bookmark=None):
986 all_refs, selected_ref = \
988 all_refs, selected_ref = \
987 self._get_repo_pullrequest_sources(
989 self._get_repo_pullrequest_sources(
988 repo.scm_instance(), commit_id=commit_id,
990 repo.scm_instance(), commit_id=commit_id,
989 branch=branch, bookmark=bookmark)
991 branch=branch, bookmark=bookmark)
990
992
991 refs_select2 = []
993 refs_select2 = []
992 for element in all_refs:
994 for element in all_refs:
993 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
995 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
994 refs_select2.append({'text': element[1], 'children': children})
996 refs_select2.append({'text': element[1], 'children': children})
995
997
996 return {
998 return {
997 'user': {
999 'user': {
998 'user_id': repo.user.user_id,
1000 'user_id': repo.user.user_id,
999 'username': repo.user.username,
1001 'username': repo.user.username,
1000 'firstname': repo.user.firstname,
1002 'firstname': repo.user.firstname,
1001 'lastname': repo.user.lastname,
1003 'lastname': repo.user.lastname,
1002 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1004 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1003 },
1005 },
1004 'description': h.chop_at_smart(repo.description, '\n'),
1006 'description': h.chop_at_smart(repo.description, '\n'),
1005 'refs': {
1007 'refs': {
1006 'all_refs': all_refs,
1008 'all_refs': all_refs,
1007 'selected_ref': selected_ref,
1009 'selected_ref': selected_ref,
1008 'select2_refs': refs_select2
1010 'select2_refs': refs_select2
1009 }
1011 }
1010 }
1012 }
1011
1013
1012 def generate_pullrequest_title(self, source, source_ref, target):
1014 def generate_pullrequest_title(self, source, source_ref, target):
1013 return '{source}#{at_ref} to {target}'.format(
1015 return '{source}#{at_ref} to {target}'.format(
1014 source=source,
1016 source=source,
1015 at_ref=source_ref,
1017 at_ref=source_ref,
1016 target=target,
1018 target=target,
1017 )
1019 )
1018
1020
1019 def _cleanup_merge_workspace(self, pull_request):
1021 def _cleanup_merge_workspace(self, pull_request):
1020 # Merging related cleanup
1022 # Merging related cleanup
1021 target_scm = pull_request.target_repo.scm_instance()
1023 target_scm = pull_request.target_repo.scm_instance()
1022 workspace_id = 'pr-%s' % pull_request.pull_request_id
1024 workspace_id = 'pr-%s' % pull_request.pull_request_id
1023
1025
1024 try:
1026 try:
1025 target_scm.cleanup_merge_workspace(workspace_id)
1027 target_scm.cleanup_merge_workspace(workspace_id)
1026 except NotImplementedError:
1028 except NotImplementedError:
1027 pass
1029 pass
1028
1030
1029 def _get_repo_pullrequest_sources(
1031 def _get_repo_pullrequest_sources(
1030 self, repo, commit_id=None, branch=None, bookmark=None):
1032 self, repo, commit_id=None, branch=None, bookmark=None):
1031 """
1033 """
1032 Return a structure with repo's interesting commits, suitable for
1034 Return a structure with repo's interesting commits, suitable for
1033 the selectors in pullrequest controller
1035 the selectors in pullrequest controller
1034
1036
1035 :param commit_id: a commit that must be in the list somehow
1037 :param commit_id: a commit that must be in the list somehow
1036 and selected by default
1038 and selected by default
1037 :param branch: a branch that must be in the list and selected
1039 :param branch: a branch that must be in the list and selected
1038 by default - even if closed
1040 by default - even if closed
1039 :param bookmark: a bookmark that must be in the list and selected
1041 :param bookmark: a bookmark that must be in the list and selected
1040 """
1042 """
1041
1043
1042 commit_id = safe_str(commit_id) if commit_id else None
1044 commit_id = safe_str(commit_id) if commit_id else None
1043 branch = safe_str(branch) if branch else None
1045 branch = safe_str(branch) if branch else None
1044 bookmark = safe_str(bookmark) if bookmark else None
1046 bookmark = safe_str(bookmark) if bookmark else None
1045
1047
1046 selected = None
1048 selected = None
1047
1049
1048 # order matters: first source that has commit_id in it will be selected
1050 # order matters: first source that has commit_id in it will be selected
1049 sources = []
1051 sources = []
1050 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1052 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1051 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1053 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1052
1054
1053 if commit_id:
1055 if commit_id:
1054 ref_commit = (h.short_id(commit_id), commit_id)
1056 ref_commit = (h.short_id(commit_id), commit_id)
1055 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1057 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1056
1058
1057 sources.append(
1059 sources.append(
1058 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1060 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1059 )
1061 )
1060
1062
1061 groups = []
1063 groups = []
1062 for group_key, ref_list, group_name, match in sources:
1064 for group_key, ref_list, group_name, match in sources:
1063 group_refs = []
1065 group_refs = []
1064 for ref_name, ref_id in ref_list:
1066 for ref_name, ref_id in ref_list:
1065 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1067 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1066 group_refs.append((ref_key, ref_name))
1068 group_refs.append((ref_key, ref_name))
1067
1069
1068 if not selected:
1070 if not selected:
1069 if set([commit_id, match]) & set([ref_id, ref_name]):
1071 if set([commit_id, match]) & set([ref_id, ref_name]):
1070 selected = ref_key
1072 selected = ref_key
1071
1073
1072 if group_refs:
1074 if group_refs:
1073 groups.append((group_refs, group_name))
1075 groups.append((group_refs, group_name))
1074
1076
1075 if not selected:
1077 if not selected:
1076 ref = commit_id or branch or bookmark
1078 ref = commit_id or branch or bookmark
1077 if ref:
1079 if ref:
1078 raise CommitDoesNotExistError(
1080 raise CommitDoesNotExistError(
1079 'No commit refs could be found matching: %s' % ref)
1081 'No commit refs could be found matching: %s' % ref)
1080 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1082 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1081 selected = 'branch:%s:%s' % (
1083 selected = 'branch:%s:%s' % (
1082 repo.DEFAULT_BRANCH_NAME,
1084 repo.DEFAULT_BRANCH_NAME,
1083 repo.branches[repo.DEFAULT_BRANCH_NAME]
1085 repo.branches[repo.DEFAULT_BRANCH_NAME]
1084 )
1086 )
1085 elif repo.commit_ids:
1087 elif repo.commit_ids:
1086 rev = repo.commit_ids[0]
1088 rev = repo.commit_ids[0]
1087 selected = 'rev:%s:%s' % (rev, rev)
1089 selected = 'rev:%s:%s' % (rev, rev)
1088 else:
1090 else:
1089 raise EmptyRepositoryError()
1091 raise EmptyRepositoryError()
1090 return groups, selected
1092 return groups, selected
1091
1093
1092 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1094 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1093 pull_request = self.__get_pull_request(pull_request)
1095 pull_request = self.__get_pull_request(pull_request)
1094 return self._get_diff_from_pr_or_version(pull_request, context=context)
1096 return self._get_diff_from_pr_or_version(pull_request, context=context)
1095
1097
1096 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1098 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1097 source_repo = pr_or_version.source_repo
1099 source_repo = pr_or_version.source_repo
1098
1100
1099 # we swap org/other ref since we run a simple diff on one repo
1101 # we swap org/other ref since we run a simple diff on one repo
1100 target_ref_id = pr_or_version.target_ref_parts.commit_id
1102 target_ref_id = pr_or_version.target_ref_parts.commit_id
1101 source_ref_id = pr_or_version.source_ref_parts.commit_id
1103 source_ref_id = pr_or_version.source_ref_parts.commit_id
1102 target_commit = source_repo.get_commit(
1104 target_commit = source_repo.get_commit(
1103 commit_id=safe_str(target_ref_id))
1105 commit_id=safe_str(target_ref_id))
1104 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1106 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1105 vcs_repo = source_repo.scm_instance()
1107 vcs_repo = source_repo.scm_instance()
1106
1108
1107 # TODO: johbo: In the context of an update, we cannot reach
1109 # TODO: johbo: In the context of an update, we cannot reach
1108 # the old commit anymore with our normal mechanisms. It needs
1110 # the old commit anymore with our normal mechanisms. It needs
1109 # some sort of special support in the vcs layer to avoid this
1111 # some sort of special support in the vcs layer to avoid this
1110 # workaround.
1112 # workaround.
1111 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1113 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1112 vcs_repo.alias == 'git'):
1114 vcs_repo.alias == 'git'):
1113 source_commit.raw_id = safe_str(source_ref_id)
1115 source_commit.raw_id = safe_str(source_ref_id)
1114
1116
1115 log.debug('calculating diff between '
1117 log.debug('calculating diff between '
1116 'source_ref:%s and target_ref:%s for repo `%s`',
1118 'source_ref:%s and target_ref:%s for repo `%s`',
1117 target_ref_id, source_ref_id,
1119 target_ref_id, source_ref_id,
1118 safe_unicode(vcs_repo.path))
1120 safe_unicode(vcs_repo.path))
1119
1121
1120 vcs_diff = vcs_repo.get_diff(
1122 vcs_diff = vcs_repo.get_diff(
1121 commit1=target_commit, commit2=source_commit, context=context)
1123 commit1=target_commit, commit2=source_commit, context=context)
1122 return vcs_diff
1124 return vcs_diff
1123
1125
1124 def _is_merge_enabled(self, pull_request):
1126 def _is_merge_enabled(self, pull_request):
1125 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1127 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1126 settings = settings_model.get_general_settings()
1128 settings = settings_model.get_general_settings()
1127 return settings.get('rhodecode_pr_merge_enabled', False)
1129 return settings.get('rhodecode_pr_merge_enabled', False)
1128
1130
1131 def _use_rebase_for_merging(self, pull_request):
1132 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1133 settings = settings_model.get_general_settings()
1134 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1135
1129 def _log_action(self, action, user, pull_request):
1136 def _log_action(self, action, user, pull_request):
1130 action_logger(
1137 action_logger(
1131 user,
1138 user,
1132 '{action}:{pr_id}'.format(
1139 '{action}:{pr_id}'.format(
1133 action=action, pr_id=pull_request.pull_request_id),
1140 action=action, pr_id=pull_request.pull_request_id),
1134 pull_request.target_repo)
1141 pull_request.target_repo)
1135
1142
1136
1143
1137 ChangeTuple = namedtuple('ChangeTuple',
1144 ChangeTuple = namedtuple('ChangeTuple',
1138 ['added', 'common', 'removed'])
1145 ['added', 'common', 'removed'])
1139
1146
1140 FileChangeTuple = namedtuple('FileChangeTuple',
1147 FileChangeTuple = namedtuple('FileChangeTuple',
1141 ['added', 'modified', 'removed'])
1148 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now