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