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