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