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