##// END OF EJS Templates
pull-requests: moved get_pr_version into PullRequestModel.
marcink -
r2393:e9838883 default
parent child Browse files
Show More
@@ -1,1259 +1,1236 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63
63
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'rhodecode:templates/data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176 self.load_default_context()
176 self.load_default_context()
177
177
178 # additional filters
178 # additional filters
179 req_get = self.request.GET
179 req_get = self.request.GET
180 source = str2bool(req_get.get('source'))
180 source = str2bool(req_get.get('source'))
181 closed = str2bool(req_get.get('closed'))
181 closed = str2bool(req_get.get('closed'))
182 my = str2bool(req_get.get('my'))
182 my = str2bool(req_get.get('my'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185
185
186 filter_type = 'awaiting_review' if awaiting_review \
186 filter_type = 'awaiting_review' if awaiting_review \
187 else 'awaiting_my_review' if awaiting_my_review \
187 else 'awaiting_my_review' if awaiting_my_review \
188 else None
188 else None
189
189
190 opened_by = None
190 opened_by = None
191 if my:
191 if my:
192 opened_by = [self._rhodecode_user.user_id]
192 opened_by = [self._rhodecode_user.user_id]
193
193
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 if closed:
195 if closed:
196 statuses = [PullRequest.STATUS_CLOSED]
196 statuses = [PullRequest.STATUS_CLOSED]
197
197
198 data = self._get_pull_requests_list(
198 data = self._get_pull_requests_list(
199 repo_name=self.db_repo_name, source=source,
199 repo_name=self.db_repo_name, source=source,
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201
201
202 return data
202 return data
203
203
204 def _get_pr_version(self, pull_request_id, version=None):
205 at_version = None
206
207 if version and version == 'latest':
208 pull_request_ver = PullRequest.get(pull_request_id)
209 pull_request_obj = pull_request_ver
210 _org_pull_request_obj = pull_request_obj
211 at_version = 'latest'
212 elif version:
213 pull_request_ver = PullRequestVersion.get_or_404(version)
214 pull_request_obj = pull_request_ver
215 _org_pull_request_obj = pull_request_ver.pull_request
216 at_version = pull_request_ver.pull_request_version_id
217 else:
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
219 pull_request_id)
220
221 pull_request_display_obj = PullRequest.get_pr_display_object(
222 pull_request_obj, _org_pull_request_obj)
223
224 return _org_pull_request_obj, pull_request_obj, \
225 pull_request_display_obj, at_version
226
227 def _get_diffset(self, source_repo_name, source_repo,
204 def _get_diffset(self, source_repo_name, source_repo,
228 source_ref_id, target_ref_id,
205 source_ref_id, target_ref_id,
229 target_commit, source_commit, diff_limit, fulldiff,
206 target_commit, source_commit, diff_limit, fulldiff,
230 file_limit, display_inline_comments):
207 file_limit, display_inline_comments):
231
208
232 vcs_diff = PullRequestModel().get_diff(
209 vcs_diff = PullRequestModel().get_diff(
233 source_repo, source_ref_id, target_ref_id)
210 source_repo, source_ref_id, target_ref_id)
234
211
235 diff_processor = diffs.DiffProcessor(
212 diff_processor = diffs.DiffProcessor(
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
213 vcs_diff, format='newdiff', diff_limit=diff_limit,
237 file_limit=file_limit, show_full_diff=fulldiff)
214 file_limit=file_limit, show_full_diff=fulldiff)
238
215
239 _parsed = diff_processor.prepare()
216 _parsed = diff_processor.prepare()
240
217
241 def _node_getter(commit):
218 def _node_getter(commit):
242 def get_node(fname):
219 def get_node(fname):
243 try:
220 try:
244 return commit.get_node(fname)
221 return commit.get_node(fname)
245 except NodeDoesNotExistError:
222 except NodeDoesNotExistError:
246 return None
223 return None
247
224
248 return get_node
225 return get_node
249
226
250 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
251 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
252 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
253 source_node_getter=_node_getter(target_commit),
230 source_node_getter=_node_getter(target_commit),
254 target_node_getter=_node_getter(source_commit),
231 target_node_getter=_node_getter(source_commit),
255 comments=display_inline_comments
232 comments=display_inline_comments
256 )
233 )
257 diffset = diffset.render_patchset(
234 diffset = diffset.render_patchset(
258 _parsed, target_commit.raw_id, source_commit.raw_id)
235 _parsed, target_commit.raw_id, source_commit.raw_id)
259
236
260 return diffset
237 return diffset
261
238
262 @LoginRequired()
239 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
240 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
241 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
242 @view_config(
266 route_name='pullrequest_show', request_method='GET',
243 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
244 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
245 def pull_request_show(self):
269 pull_request_id = self.request.matchdict['pull_request_id']
246 pull_request_id = self.request.matchdict['pull_request_id']
270
247
271 c = self.load_default_context()
248 c = self.load_default_context()
272
249
273 version = self.request.GET.get('version')
250 version = self.request.GET.get('version')
274 from_version = self.request.GET.get('from_version') or version
251 from_version = self.request.GET.get('from_version') or version
275 merge_checks = self.request.GET.get('merge_checks')
252 merge_checks = self.request.GET.get('merge_checks')
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
253 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
277
254
278 (pull_request_latest,
255 (pull_request_latest,
279 pull_request_at_ver,
256 pull_request_at_ver,
280 pull_request_display_obj,
257 pull_request_display_obj,
281 at_version) = self._get_pr_version(
258 at_version) = PullRequestModel().get_pr_version(
282 pull_request_id, version=version)
259 pull_request_id, version=version)
283 pr_closed = pull_request_latest.is_closed()
260 pr_closed = pull_request_latest.is_closed()
284
261
285 if pr_closed and (version or from_version):
262 if pr_closed and (version or from_version):
286 # not allow to browse versions
263 # not allow to browse versions
287 raise HTTPFound(h.route_path(
264 raise HTTPFound(h.route_path(
288 'pullrequest_show', repo_name=self.db_repo_name,
265 'pullrequest_show', repo_name=self.db_repo_name,
289 pull_request_id=pull_request_id))
266 pull_request_id=pull_request_id))
290
267
291 versions = pull_request_display_obj.versions()
268 versions = pull_request_display_obj.versions()
292
269
293 c.at_version = at_version
270 c.at_version = at_version
294 c.at_version_num = (at_version
271 c.at_version_num = (at_version
295 if at_version and at_version != 'latest'
272 if at_version and at_version != 'latest'
296 else None)
273 else None)
297 c.at_version_pos = ChangesetComment.get_index_from_version(
274 c.at_version_pos = ChangesetComment.get_index_from_version(
298 c.at_version_num, versions)
275 c.at_version_num, versions)
299
276
300 (prev_pull_request_latest,
277 (prev_pull_request_latest,
301 prev_pull_request_at_ver,
278 prev_pull_request_at_ver,
302 prev_pull_request_display_obj,
279 prev_pull_request_display_obj,
303 prev_at_version) = self._get_pr_version(
280 prev_at_version) = PullRequestModel().get_pr_version(
304 pull_request_id, version=from_version)
281 pull_request_id, version=from_version)
305
282
306 c.from_version = prev_at_version
283 c.from_version = prev_at_version
307 c.from_version_num = (prev_at_version
284 c.from_version_num = (prev_at_version
308 if prev_at_version and prev_at_version != 'latest'
285 if prev_at_version and prev_at_version != 'latest'
309 else None)
286 else None)
310 c.from_version_pos = ChangesetComment.get_index_from_version(
287 c.from_version_pos = ChangesetComment.get_index_from_version(
311 c.from_version_num, versions)
288 c.from_version_num, versions)
312
289
313 # define if we're in COMPARE mode or VIEW at version mode
290 # define if we're in COMPARE mode or VIEW at version mode
314 compare = at_version != prev_at_version
291 compare = at_version != prev_at_version
315
292
316 # pull_requests repo_name we opened it against
293 # pull_requests repo_name we opened it against
317 # ie. target_repo must match
294 # ie. target_repo must match
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
295 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
319 raise HTTPNotFound()
296 raise HTTPNotFound()
320
297
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
298 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
322 pull_request_at_ver)
299 pull_request_at_ver)
323
300
324 c.pull_request = pull_request_display_obj
301 c.pull_request = pull_request_display_obj
325 c.pull_request_latest = pull_request_latest
302 c.pull_request_latest = pull_request_latest
326
303
327 if compare or (at_version and not at_version == 'latest'):
304 if compare or (at_version and not at_version == 'latest'):
328 c.allowed_to_change_status = False
305 c.allowed_to_change_status = False
329 c.allowed_to_update = False
306 c.allowed_to_update = False
330 c.allowed_to_merge = False
307 c.allowed_to_merge = False
331 c.allowed_to_delete = False
308 c.allowed_to_delete = False
332 c.allowed_to_comment = False
309 c.allowed_to_comment = False
333 c.allowed_to_close = False
310 c.allowed_to_close = False
334 else:
311 else:
335 can_change_status = PullRequestModel().check_user_change_status(
312 can_change_status = PullRequestModel().check_user_change_status(
336 pull_request_at_ver, self._rhodecode_user)
313 pull_request_at_ver, self._rhodecode_user)
337 c.allowed_to_change_status = can_change_status and not pr_closed
314 c.allowed_to_change_status = can_change_status and not pr_closed
338
315
339 c.allowed_to_update = PullRequestModel().check_user_update(
316 c.allowed_to_update = PullRequestModel().check_user_update(
340 pull_request_latest, self._rhodecode_user) and not pr_closed
317 pull_request_latest, self._rhodecode_user) and not pr_closed
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
318 c.allowed_to_merge = PullRequestModel().check_user_merge(
342 pull_request_latest, self._rhodecode_user) and not pr_closed
319 pull_request_latest, self._rhodecode_user) and not pr_closed
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
320 c.allowed_to_delete = PullRequestModel().check_user_delete(
344 pull_request_latest, self._rhodecode_user) and not pr_closed
321 pull_request_latest, self._rhodecode_user) and not pr_closed
345 c.allowed_to_comment = not pr_closed
322 c.allowed_to_comment = not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
323 c.allowed_to_close = c.allowed_to_merge and not pr_closed
347
324
348 c.forbid_adding_reviewers = False
325 c.forbid_adding_reviewers = False
349 c.forbid_author_to_review = False
326 c.forbid_author_to_review = False
350 c.forbid_commit_author_to_review = False
327 c.forbid_commit_author_to_review = False
351
328
352 if pull_request_latest.reviewer_data and \
329 if pull_request_latest.reviewer_data and \
353 'rules' in pull_request_latest.reviewer_data:
330 'rules' in pull_request_latest.reviewer_data:
354 rules = pull_request_latest.reviewer_data['rules'] or {}
331 rules = pull_request_latest.reviewer_data['rules'] or {}
355 try:
332 try:
356 c.forbid_adding_reviewers = rules.get(
333 c.forbid_adding_reviewers = rules.get(
357 'forbid_adding_reviewers')
334 'forbid_adding_reviewers')
358 c.forbid_author_to_review = rules.get(
335 c.forbid_author_to_review = rules.get(
359 'forbid_author_to_review')
336 'forbid_author_to_review')
360 c.forbid_commit_author_to_review = rules.get(
337 c.forbid_commit_author_to_review = rules.get(
361 'forbid_commit_author_to_review')
338 'forbid_commit_author_to_review')
362 except Exception:
339 except Exception:
363 pass
340 pass
364
341
365 # check merge capabilities
342 # check merge capabilities
366 _merge_check = MergeCheck.validate(
343 _merge_check = MergeCheck.validate(
367 pull_request_latest, user=self._rhodecode_user,
344 pull_request_latest, user=self._rhodecode_user,
368 translator=self.request.translate)
345 translator=self.request.translate)
369 c.pr_merge_errors = _merge_check.error_details
346 c.pr_merge_errors = _merge_check.error_details
370 c.pr_merge_possible = not _merge_check.failed
347 c.pr_merge_possible = not _merge_check.failed
371 c.pr_merge_message = _merge_check.merge_msg
348 c.pr_merge_message = _merge_check.merge_msg
372
349
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
350 c.pr_merge_info = MergeCheck.get_merge_conditions(
374 pull_request_latest, translator=self.request.translate)
351 pull_request_latest, translator=self.request.translate)
375
352
376 c.pull_request_review_status = _merge_check.review_status
353 c.pull_request_review_status = _merge_check.review_status
377 if merge_checks:
354 if merge_checks:
378 self.request.override_renderer = \
355 self.request.override_renderer = \
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
356 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
380 return self._get_template_context(c)
357 return self._get_template_context(c)
381
358
382 comments_model = CommentsModel()
359 comments_model = CommentsModel()
383
360
384 # reviewers and statuses
361 # reviewers and statuses
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
362 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
363 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
387
364
388 # GENERAL COMMENTS with versions #
365 # GENERAL COMMENTS with versions #
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
366 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
390 q = q.order_by(ChangesetComment.comment_id.asc())
367 q = q.order_by(ChangesetComment.comment_id.asc())
391 general_comments = q
368 general_comments = q
392
369
393 # pick comments we want to render at current version
370 # pick comments we want to render at current version
394 c.comment_versions = comments_model.aggregate_comments(
371 c.comment_versions = comments_model.aggregate_comments(
395 general_comments, versions, c.at_version_num)
372 general_comments, versions, c.at_version_num)
396 c.comments = c.comment_versions[c.at_version_num]['until']
373 c.comments = c.comment_versions[c.at_version_num]['until']
397
374
398 # INLINE COMMENTS with versions #
375 # INLINE COMMENTS with versions #
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
376 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
400 q = q.order_by(ChangesetComment.comment_id.asc())
377 q = q.order_by(ChangesetComment.comment_id.asc())
401 inline_comments = q
378 inline_comments = q
402
379
403 c.inline_versions = comments_model.aggregate_comments(
380 c.inline_versions = comments_model.aggregate_comments(
404 inline_comments, versions, c.at_version_num, inline=True)
381 inline_comments, versions, c.at_version_num, inline=True)
405
382
406 # inject latest version
383 # inject latest version
407 latest_ver = PullRequest.get_pr_display_object(
384 latest_ver = PullRequest.get_pr_display_object(
408 pull_request_latest, pull_request_latest)
385 pull_request_latest, pull_request_latest)
409
386
410 c.versions = versions + [latest_ver]
387 c.versions = versions + [latest_ver]
411
388
412 # if we use version, then do not show later comments
389 # if we use version, then do not show later comments
413 # than current version
390 # than current version
414 display_inline_comments = collections.defaultdict(
391 display_inline_comments = collections.defaultdict(
415 lambda: collections.defaultdict(list))
392 lambda: collections.defaultdict(list))
416 for co in inline_comments:
393 for co in inline_comments:
417 if c.at_version_num:
394 if c.at_version_num:
418 # pick comments that are at least UPTO given version, so we
395 # pick comments that are at least UPTO given version, so we
419 # don't render comments for higher version
396 # don't render comments for higher version
420 should_render = co.pull_request_version_id and \
397 should_render = co.pull_request_version_id and \
421 co.pull_request_version_id <= c.at_version_num
398 co.pull_request_version_id <= c.at_version_num
422 else:
399 else:
423 # showing all, for 'latest'
400 # showing all, for 'latest'
424 should_render = True
401 should_render = True
425
402
426 if should_render:
403 if should_render:
427 display_inline_comments[co.f_path][co.line_no].append(co)
404 display_inline_comments[co.f_path][co.line_no].append(co)
428
405
429 # load diff data into template context, if we use compare mode then
406 # load diff data into template context, if we use compare mode then
430 # diff is calculated based on changes between versions of PR
407 # diff is calculated based on changes between versions of PR
431
408
432 source_repo = pull_request_at_ver.source_repo
409 source_repo = pull_request_at_ver.source_repo
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
410 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
434
411
435 target_repo = pull_request_at_ver.target_repo
412 target_repo = pull_request_at_ver.target_repo
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
413 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
437
414
438 if compare:
415 if compare:
439 # in compare switch the diff base to latest commit from prev version
416 # in compare switch the diff base to latest commit from prev version
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
417 target_ref_id = prev_pull_request_display_obj.revisions[0]
441
418
442 # despite opening commits for bookmarks/branches/tags, we always
419 # despite opening commits for bookmarks/branches/tags, we always
443 # convert this to rev to prevent changes after bookmark or branch change
420 # convert this to rev to prevent changes after bookmark or branch change
444 c.source_ref_type = 'rev'
421 c.source_ref_type = 'rev'
445 c.source_ref = source_ref_id
422 c.source_ref = source_ref_id
446
423
447 c.target_ref_type = 'rev'
424 c.target_ref_type = 'rev'
448 c.target_ref = target_ref_id
425 c.target_ref = target_ref_id
449
426
450 c.source_repo = source_repo
427 c.source_repo = source_repo
451 c.target_repo = target_repo
428 c.target_repo = target_repo
452
429
453 c.commit_ranges = []
430 c.commit_ranges = []
454 source_commit = EmptyCommit()
431 source_commit = EmptyCommit()
455 target_commit = EmptyCommit()
432 target_commit = EmptyCommit()
456 c.missing_requirements = False
433 c.missing_requirements = False
457
434
458 source_scm = source_repo.scm_instance()
435 source_scm = source_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
436 target_scm = target_repo.scm_instance()
460
437
461 # try first shadow repo, fallback to regular repo
438 # try first shadow repo, fallback to regular repo
462 try:
439 try:
463 commits_source_repo = pull_request_latest.get_shadow_repo()
440 commits_source_repo = pull_request_latest.get_shadow_repo()
464 except Exception:
441 except Exception:
465 log.debug('Failed to get shadow repo', exc_info=True)
442 log.debug('Failed to get shadow repo', exc_info=True)
466 commits_source_repo = source_scm
443 commits_source_repo = source_scm
467
444
468 c.commits_source_repo = commits_source_repo
445 c.commits_source_repo = commits_source_repo
469 commit_cache = {}
446 commit_cache = {}
470 try:
447 try:
471 pre_load = ["author", "branch", "date", "message"]
448 pre_load = ["author", "branch", "date", "message"]
472 show_revs = pull_request_at_ver.revisions
449 show_revs = pull_request_at_ver.revisions
473 for rev in show_revs:
450 for rev in show_revs:
474 comm = commits_source_repo.get_commit(
451 comm = commits_source_repo.get_commit(
475 commit_id=rev, pre_load=pre_load)
452 commit_id=rev, pre_load=pre_load)
476 c.commit_ranges.append(comm)
453 c.commit_ranges.append(comm)
477 commit_cache[comm.raw_id] = comm
454 commit_cache[comm.raw_id] = comm
478
455
479 # Order here matters, we first need to get target, and then
456 # Order here matters, we first need to get target, and then
480 # the source
457 # the source
481 target_commit = commits_source_repo.get_commit(
458 target_commit = commits_source_repo.get_commit(
482 commit_id=safe_str(target_ref_id))
459 commit_id=safe_str(target_ref_id))
483
460
484 source_commit = commits_source_repo.get_commit(
461 source_commit = commits_source_repo.get_commit(
485 commit_id=safe_str(source_ref_id))
462 commit_id=safe_str(source_ref_id))
486
463
487 except CommitDoesNotExistError:
464 except CommitDoesNotExistError:
488 log.warning(
465 log.warning(
489 'Failed to get commit from `{}` repo'.format(
466 'Failed to get commit from `{}` repo'.format(
490 commits_source_repo), exc_info=True)
467 commits_source_repo), exc_info=True)
491 except RepositoryRequirementError:
468 except RepositoryRequirementError:
492 log.warning(
469 log.warning(
493 'Failed to get all required data from repo', exc_info=True)
470 'Failed to get all required data from repo', exc_info=True)
494 c.missing_requirements = True
471 c.missing_requirements = True
495
472
496 c.ancestor = None # set it to None, to hide it from PR view
473 c.ancestor = None # set it to None, to hide it from PR view
497
474
498 try:
475 try:
499 ancestor_id = source_scm.get_common_ancestor(
476 ancestor_id = source_scm.get_common_ancestor(
500 source_commit.raw_id, target_commit.raw_id, target_scm)
477 source_commit.raw_id, target_commit.raw_id, target_scm)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
478 c.ancestor_commit = source_scm.get_commit(ancestor_id)
502 except Exception:
479 except Exception:
503 c.ancestor_commit = None
480 c.ancestor_commit = None
504
481
505 c.statuses = source_repo.statuses(
482 c.statuses = source_repo.statuses(
506 [x.raw_id for x in c.commit_ranges])
483 [x.raw_id for x in c.commit_ranges])
507
484
508 # auto collapse if we have more than limit
485 # auto collapse if we have more than limit
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
486 collapse_limit = diffs.DiffProcessor._collapse_commits_over
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
487 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
511 c.compare_mode = compare
488 c.compare_mode = compare
512
489
513 # diff_limit is the old behavior, will cut off the whole diff
490 # diff_limit is the old behavior, will cut off the whole diff
514 # if the limit is applied otherwise will just hide the
491 # if the limit is applied otherwise will just hide the
515 # big files from the front-end
492 # big files from the front-end
516 diff_limit = c.visual.cut_off_limit_diff
493 diff_limit = c.visual.cut_off_limit_diff
517 file_limit = c.visual.cut_off_limit_file
494 file_limit = c.visual.cut_off_limit_file
518
495
519 c.missing_commits = False
496 c.missing_commits = False
520 if (c.missing_requirements
497 if (c.missing_requirements
521 or isinstance(source_commit, EmptyCommit)
498 or isinstance(source_commit, EmptyCommit)
522 or source_commit == target_commit):
499 or source_commit == target_commit):
523
500
524 c.missing_commits = True
501 c.missing_commits = True
525 else:
502 else:
526
503
527 c.diffset = self._get_diffset(
504 c.diffset = self._get_diffset(
528 c.source_repo.repo_name, commits_source_repo,
505 c.source_repo.repo_name, commits_source_repo,
529 source_ref_id, target_ref_id,
506 source_ref_id, target_ref_id,
530 target_commit, source_commit,
507 target_commit, source_commit,
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
508 diff_limit, c.fulldiff, file_limit, display_inline_comments)
532
509
533 c.limited_diff = c.diffset.limited_diff
510 c.limited_diff = c.diffset.limited_diff
534
511
535 # calculate removed files that are bound to comments
512 # calculate removed files that are bound to comments
536 comment_deleted_files = [
513 comment_deleted_files = [
537 fname for fname in display_inline_comments
514 fname for fname in display_inline_comments
538 if fname not in c.diffset.file_stats]
515 if fname not in c.diffset.file_stats]
539
516
540 c.deleted_files_comments = collections.defaultdict(dict)
517 c.deleted_files_comments = collections.defaultdict(dict)
541 for fname, per_line_comments in display_inline_comments.items():
518 for fname, per_line_comments in display_inline_comments.items():
542 if fname in comment_deleted_files:
519 if fname in comment_deleted_files:
543 c.deleted_files_comments[fname]['stats'] = 0
520 c.deleted_files_comments[fname]['stats'] = 0
544 c.deleted_files_comments[fname]['comments'] = list()
521 c.deleted_files_comments[fname]['comments'] = list()
545 for lno, comments in per_line_comments.items():
522 for lno, comments in per_line_comments.items():
546 c.deleted_files_comments[fname]['comments'].extend(
523 c.deleted_files_comments[fname]['comments'].extend(
547 comments)
524 comments)
548
525
549 # this is a hack to properly display links, when creating PR, the
526 # this is a hack to properly display links, when creating PR, the
550 # compare view and others uses different notation, and
527 # compare view and others uses different notation, and
551 # compare_commits.mako renders links based on the target_repo.
528 # compare_commits.mako renders links based on the target_repo.
552 # We need to swap that here to generate it properly on the html side
529 # We need to swap that here to generate it properly on the html side
553 c.target_repo = c.source_repo
530 c.target_repo = c.source_repo
554
531
555 c.commit_statuses = ChangesetStatus.STATUSES
532 c.commit_statuses = ChangesetStatus.STATUSES
556
533
557 c.show_version_changes = not pr_closed
534 c.show_version_changes = not pr_closed
558 if c.show_version_changes:
535 if c.show_version_changes:
559 cur_obj = pull_request_at_ver
536 cur_obj = pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
537 prev_obj = prev_pull_request_at_ver
561
538
562 old_commit_ids = prev_obj.revisions
539 old_commit_ids = prev_obj.revisions
563 new_commit_ids = cur_obj.revisions
540 new_commit_ids = cur_obj.revisions
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
541 commit_changes = PullRequestModel()._calculate_commit_id_changes(
565 old_commit_ids, new_commit_ids)
542 old_commit_ids, new_commit_ids)
566 c.commit_changes_summary = commit_changes
543 c.commit_changes_summary = commit_changes
567
544
568 # calculate the diff for commits between versions
545 # calculate the diff for commits between versions
569 c.commit_changes = []
546 c.commit_changes = []
570 mark = lambda cs, fw: list(
547 mark = lambda cs, fw: list(
571 h.itertools.izip_longest([], cs, fillvalue=fw))
548 h.itertools.izip_longest([], cs, fillvalue=fw))
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
549 for c_type, raw_id in mark(commit_changes.added, 'a') \
573 + mark(commit_changes.removed, 'r') \
550 + mark(commit_changes.removed, 'r') \
574 + mark(commit_changes.common, 'c'):
551 + mark(commit_changes.common, 'c'):
575
552
576 if raw_id in commit_cache:
553 if raw_id in commit_cache:
577 commit = commit_cache[raw_id]
554 commit = commit_cache[raw_id]
578 else:
555 else:
579 try:
556 try:
580 commit = commits_source_repo.get_commit(raw_id)
557 commit = commits_source_repo.get_commit(raw_id)
581 except CommitDoesNotExistError:
558 except CommitDoesNotExistError:
582 # in case we fail extracting still use "dummy" commit
559 # in case we fail extracting still use "dummy" commit
583 # for display in commit diff
560 # for display in commit diff
584 commit = h.AttributeDict(
561 commit = h.AttributeDict(
585 {'raw_id': raw_id,
562 {'raw_id': raw_id,
586 'message': 'EMPTY or MISSING COMMIT'})
563 'message': 'EMPTY or MISSING COMMIT'})
587 c.commit_changes.append([c_type, commit])
564 c.commit_changes.append([c_type, commit])
588
565
589 # current user review statuses for each version
566 # current user review statuses for each version
590 c.review_versions = {}
567 c.review_versions = {}
591 if self._rhodecode_user.user_id in allowed_reviewers:
568 if self._rhodecode_user.user_id in allowed_reviewers:
592 for co in general_comments:
569 for co in general_comments:
593 if co.author.user_id == self._rhodecode_user.user_id:
570 if co.author.user_id == self._rhodecode_user.user_id:
594 # each comment has a status change
571 # each comment has a status change
595 status = co.status_change
572 status = co.status_change
596 if status:
573 if status:
597 _ver_pr = status[0].comment.pull_request_version_id
574 _ver_pr = status[0].comment.pull_request_version_id
598 c.review_versions[_ver_pr] = status[0]
575 c.review_versions[_ver_pr] = status[0]
599
576
600 return self._get_template_context(c)
577 return self._get_template_context(c)
601
578
602 def assure_not_empty_repo(self):
579 def assure_not_empty_repo(self):
603 _ = self.request.translate
580 _ = self.request.translate
604
581
605 try:
582 try:
606 self.db_repo.scm_instance().get_commit()
583 self.db_repo.scm_instance().get_commit()
607 except EmptyRepositoryError:
584 except EmptyRepositoryError:
608 h.flash(h.literal(_('There are no commits yet')),
585 h.flash(h.literal(_('There are no commits yet')),
609 category='warning')
586 category='warning')
610 raise HTTPFound(
587 raise HTTPFound(
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
588 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
612
589
613 @LoginRequired()
590 @LoginRequired()
614 @NotAnonymous()
591 @NotAnonymous()
615 @HasRepoPermissionAnyDecorator(
592 @HasRepoPermissionAnyDecorator(
616 'repository.read', 'repository.write', 'repository.admin')
593 'repository.read', 'repository.write', 'repository.admin')
617 @view_config(
594 @view_config(
618 route_name='pullrequest_new', request_method='GET',
595 route_name='pullrequest_new', request_method='GET',
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
596 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
620 def pull_request_new(self):
597 def pull_request_new(self):
621 _ = self.request.translate
598 _ = self.request.translate
622 c = self.load_default_context()
599 c = self.load_default_context()
623
600
624 self.assure_not_empty_repo()
601 self.assure_not_empty_repo()
625 source_repo = self.db_repo
602 source_repo = self.db_repo
626
603
627 commit_id = self.request.GET.get('commit')
604 commit_id = self.request.GET.get('commit')
628 branch_ref = self.request.GET.get('branch')
605 branch_ref = self.request.GET.get('branch')
629 bookmark_ref = self.request.GET.get('bookmark')
606 bookmark_ref = self.request.GET.get('bookmark')
630
607
631 try:
608 try:
632 source_repo_data = PullRequestModel().generate_repo_data(
609 source_repo_data = PullRequestModel().generate_repo_data(
633 source_repo, commit_id=commit_id,
610 source_repo, commit_id=commit_id,
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
611 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
635 except CommitDoesNotExistError as e:
612 except CommitDoesNotExistError as e:
636 log.exception(e)
613 log.exception(e)
637 h.flash(_('Commit does not exist'), 'error')
614 h.flash(_('Commit does not exist'), 'error')
638 raise HTTPFound(
615 raise HTTPFound(
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
616 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
640
617
641 default_target_repo = source_repo
618 default_target_repo = source_repo
642
619
643 if source_repo.parent:
620 if source_repo.parent:
644 parent_vcs_obj = source_repo.parent.scm_instance()
621 parent_vcs_obj = source_repo.parent.scm_instance()
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
622 if parent_vcs_obj and not parent_vcs_obj.is_empty():
646 # change default if we have a parent repo
623 # change default if we have a parent repo
647 default_target_repo = source_repo.parent
624 default_target_repo = source_repo.parent
648
625
649 target_repo_data = PullRequestModel().generate_repo_data(
626 target_repo_data = PullRequestModel().generate_repo_data(
650 default_target_repo, translator=self.request.translate)
627 default_target_repo, translator=self.request.translate)
651
628
652 selected_source_ref = source_repo_data['refs']['selected_ref']
629 selected_source_ref = source_repo_data['refs']['selected_ref']
653
630
654 title_source_ref = selected_source_ref.split(':', 2)[1]
631 title_source_ref = selected_source_ref.split(':', 2)[1]
655 c.default_title = PullRequestModel().generate_pullrequest_title(
632 c.default_title = PullRequestModel().generate_pullrequest_title(
656 source=source_repo.repo_name,
633 source=source_repo.repo_name,
657 source_ref=title_source_ref,
634 source_ref=title_source_ref,
658 target=default_target_repo.repo_name
635 target=default_target_repo.repo_name
659 )
636 )
660
637
661 c.default_repo_data = {
638 c.default_repo_data = {
662 'source_repo_name': source_repo.repo_name,
639 'source_repo_name': source_repo.repo_name,
663 'source_refs_json': json.dumps(source_repo_data),
640 'source_refs_json': json.dumps(source_repo_data),
664 'target_repo_name': default_target_repo.repo_name,
641 'target_repo_name': default_target_repo.repo_name,
665 'target_refs_json': json.dumps(target_repo_data),
642 'target_refs_json': json.dumps(target_repo_data),
666 }
643 }
667 c.default_source_ref = selected_source_ref
644 c.default_source_ref = selected_source_ref
668
645
669 return self._get_template_context(c)
646 return self._get_template_context(c)
670
647
671 @LoginRequired()
648 @LoginRequired()
672 @NotAnonymous()
649 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator(
650 @HasRepoPermissionAnyDecorator(
674 'repository.read', 'repository.write', 'repository.admin')
651 'repository.read', 'repository.write', 'repository.admin')
675 @view_config(
652 @view_config(
676 route_name='pullrequest_repo_refs', request_method='GET',
653 route_name='pullrequest_repo_refs', request_method='GET',
677 renderer='json_ext', xhr=True)
654 renderer='json_ext', xhr=True)
678 def pull_request_repo_refs(self):
655 def pull_request_repo_refs(self):
679 self.load_default_context()
656 self.load_default_context()
680 target_repo_name = self.request.matchdict['target_repo_name']
657 target_repo_name = self.request.matchdict['target_repo_name']
681 repo = Repository.get_by_repo_name(target_repo_name)
658 repo = Repository.get_by_repo_name(target_repo_name)
682 if not repo:
659 if not repo:
683 raise HTTPNotFound()
660 raise HTTPNotFound()
684
661
685 target_perm = HasRepoPermissionAny(
662 target_perm = HasRepoPermissionAny(
686 'repository.read', 'repository.write', 'repository.admin')(
663 'repository.read', 'repository.write', 'repository.admin')(
687 target_repo_name)
664 target_repo_name)
688 if not target_perm:
665 if not target_perm:
689 raise HTTPNotFound()
666 raise HTTPNotFound()
690
667
691 return PullRequestModel().generate_repo_data(
668 return PullRequestModel().generate_repo_data(
692 repo, translator=self.request.translate)
669 repo, translator=self.request.translate)
693
670
694 @LoginRequired()
671 @LoginRequired()
695 @NotAnonymous()
672 @NotAnonymous()
696 @HasRepoPermissionAnyDecorator(
673 @HasRepoPermissionAnyDecorator(
697 'repository.read', 'repository.write', 'repository.admin')
674 'repository.read', 'repository.write', 'repository.admin')
698 @view_config(
675 @view_config(
699 route_name='pullrequest_repo_destinations', request_method='GET',
676 route_name='pullrequest_repo_destinations', request_method='GET',
700 renderer='json_ext', xhr=True)
677 renderer='json_ext', xhr=True)
701 def pull_request_repo_destinations(self):
678 def pull_request_repo_destinations(self):
702 _ = self.request.translate
679 _ = self.request.translate
703 filter_query = self.request.GET.get('query')
680 filter_query = self.request.GET.get('query')
704
681
705 query = Repository.query() \
682 query = Repository.query() \
706 .order_by(func.length(Repository.repo_name)) \
683 .order_by(func.length(Repository.repo_name)) \
707 .filter(
684 .filter(
708 or_(Repository.repo_name == self.db_repo.repo_name,
685 or_(Repository.repo_name == self.db_repo.repo_name,
709 Repository.fork_id == self.db_repo.repo_id))
686 Repository.fork_id == self.db_repo.repo_id))
710
687
711 if filter_query:
688 if filter_query:
712 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
689 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
713 query = query.filter(
690 query = query.filter(
714 Repository.repo_name.ilike(ilike_expression))
691 Repository.repo_name.ilike(ilike_expression))
715
692
716 add_parent = False
693 add_parent = False
717 if self.db_repo.parent:
694 if self.db_repo.parent:
718 if filter_query in self.db_repo.parent.repo_name:
695 if filter_query in self.db_repo.parent.repo_name:
719 parent_vcs_obj = self.db_repo.parent.scm_instance()
696 parent_vcs_obj = self.db_repo.parent.scm_instance()
720 if parent_vcs_obj and not parent_vcs_obj.is_empty():
697 if parent_vcs_obj and not parent_vcs_obj.is_empty():
721 add_parent = True
698 add_parent = True
722
699
723 limit = 20 - 1 if add_parent else 20
700 limit = 20 - 1 if add_parent else 20
724 all_repos = query.limit(limit).all()
701 all_repos = query.limit(limit).all()
725 if add_parent:
702 if add_parent:
726 all_repos += [self.db_repo.parent]
703 all_repos += [self.db_repo.parent]
727
704
728 repos = []
705 repos = []
729 for obj in ScmModel().get_repos(all_repos):
706 for obj in ScmModel().get_repos(all_repos):
730 repos.append({
707 repos.append({
731 'id': obj['name'],
708 'id': obj['name'],
732 'text': obj['name'],
709 'text': obj['name'],
733 'type': 'repo',
710 'type': 'repo',
734 'obj': obj['dbrepo']
711 'obj': obj['dbrepo']
735 })
712 })
736
713
737 data = {
714 data = {
738 'more': False,
715 'more': False,
739 'results': [{
716 'results': [{
740 'text': _('Repositories'),
717 'text': _('Repositories'),
741 'children': repos
718 'children': repos
742 }] if repos else []
719 }] if repos else []
743 }
720 }
744 return data
721 return data
745
722
746 @LoginRequired()
723 @LoginRequired()
747 @NotAnonymous()
724 @NotAnonymous()
748 @HasRepoPermissionAnyDecorator(
725 @HasRepoPermissionAnyDecorator(
749 'repository.read', 'repository.write', 'repository.admin')
726 'repository.read', 'repository.write', 'repository.admin')
750 @CSRFRequired()
727 @CSRFRequired()
751 @view_config(
728 @view_config(
752 route_name='pullrequest_create', request_method='POST',
729 route_name='pullrequest_create', request_method='POST',
753 renderer=None)
730 renderer=None)
754 def pull_request_create(self):
731 def pull_request_create(self):
755 _ = self.request.translate
732 _ = self.request.translate
756 self.assure_not_empty_repo()
733 self.assure_not_empty_repo()
757 self.load_default_context()
734 self.load_default_context()
758
735
759 controls = peppercorn.parse(self.request.POST.items())
736 controls = peppercorn.parse(self.request.POST.items())
760
737
761 try:
738 try:
762 form = PullRequestForm(
739 form = PullRequestForm(
763 self.request.translate, self.db_repo.repo_id)()
740 self.request.translate, self.db_repo.repo_id)()
764 _form = form.to_python(controls)
741 _form = form.to_python(controls)
765 except formencode.Invalid as errors:
742 except formencode.Invalid as errors:
766 if errors.error_dict.get('revisions'):
743 if errors.error_dict.get('revisions'):
767 msg = 'Revisions: %s' % errors.error_dict['revisions']
744 msg = 'Revisions: %s' % errors.error_dict['revisions']
768 elif errors.error_dict.get('pullrequest_title'):
745 elif errors.error_dict.get('pullrequest_title'):
769 msg = _('Pull request requires a title with min. 3 chars')
746 msg = _('Pull request requires a title with min. 3 chars')
770 else:
747 else:
771 msg = _('Error creating pull request: {}').format(errors)
748 msg = _('Error creating pull request: {}').format(errors)
772 log.exception(msg)
749 log.exception(msg)
773 h.flash(msg, 'error')
750 h.flash(msg, 'error')
774
751
775 # would rather just go back to form ...
752 # would rather just go back to form ...
776 raise HTTPFound(
753 raise HTTPFound(
777 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
754 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
778
755
779 source_repo = _form['source_repo']
756 source_repo = _form['source_repo']
780 source_ref = _form['source_ref']
757 source_ref = _form['source_ref']
781 target_repo = _form['target_repo']
758 target_repo = _form['target_repo']
782 target_ref = _form['target_ref']
759 target_ref = _form['target_ref']
783 commit_ids = _form['revisions'][::-1]
760 commit_ids = _form['revisions'][::-1]
784
761
785 # find the ancestor for this pr
762 # find the ancestor for this pr
786 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
763 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
787 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
764 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
788
765
789 # re-check permissions again here
766 # re-check permissions again here
790 # source_repo we must have read permissions
767 # source_repo we must have read permissions
791
768
792 source_perm = HasRepoPermissionAny(
769 source_perm = HasRepoPermissionAny(
793 'repository.read',
770 'repository.read',
794 'repository.write', 'repository.admin')(source_db_repo.repo_name)
771 'repository.write', 'repository.admin')(source_db_repo.repo_name)
795 if not source_perm:
772 if not source_perm:
796 msg = _('Not Enough permissions to source repo `{}`.'.format(
773 msg = _('Not Enough permissions to source repo `{}`.'.format(
797 source_db_repo.repo_name))
774 source_db_repo.repo_name))
798 h.flash(msg, category='error')
775 h.flash(msg, category='error')
799 # copy the args back to redirect
776 # copy the args back to redirect
800 org_query = self.request.GET.mixed()
777 org_query = self.request.GET.mixed()
801 raise HTTPFound(
778 raise HTTPFound(
802 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
779 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
803 _query=org_query))
780 _query=org_query))
804
781
805 # target repo we must have read permissions, and also later on
782 # target repo we must have read permissions, and also later on
806 # we want to check branch permissions here
783 # we want to check branch permissions here
807 target_perm = HasRepoPermissionAny(
784 target_perm = HasRepoPermissionAny(
808 'repository.read',
785 'repository.read',
809 'repository.write', 'repository.admin')(target_db_repo.repo_name)
786 'repository.write', 'repository.admin')(target_db_repo.repo_name)
810 if not target_perm:
787 if not target_perm:
811 msg = _('Not Enough permissions to target repo `{}`.'.format(
788 msg = _('Not Enough permissions to target repo `{}`.'.format(
812 target_db_repo.repo_name))
789 target_db_repo.repo_name))
813 h.flash(msg, category='error')
790 h.flash(msg, category='error')
814 # copy the args back to redirect
791 # copy the args back to redirect
815 org_query = self.request.GET.mixed()
792 org_query = self.request.GET.mixed()
816 raise HTTPFound(
793 raise HTTPFound(
817 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
794 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
818 _query=org_query))
795 _query=org_query))
819
796
820 source_scm = source_db_repo.scm_instance()
797 source_scm = source_db_repo.scm_instance()
821 target_scm = target_db_repo.scm_instance()
798 target_scm = target_db_repo.scm_instance()
822
799
823 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
800 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
824 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
801 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
825
802
826 ancestor = source_scm.get_common_ancestor(
803 ancestor = source_scm.get_common_ancestor(
827 source_commit.raw_id, target_commit.raw_id, target_scm)
804 source_commit.raw_id, target_commit.raw_id, target_scm)
828
805
829 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
806 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
830 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
807 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
831
808
832 pullrequest_title = _form['pullrequest_title']
809 pullrequest_title = _form['pullrequest_title']
833 title_source_ref = source_ref.split(':', 2)[1]
810 title_source_ref = source_ref.split(':', 2)[1]
834 if not pullrequest_title:
811 if not pullrequest_title:
835 pullrequest_title = PullRequestModel().generate_pullrequest_title(
812 pullrequest_title = PullRequestModel().generate_pullrequest_title(
836 source=source_repo,
813 source=source_repo,
837 source_ref=title_source_ref,
814 source_ref=title_source_ref,
838 target=target_repo
815 target=target_repo
839 )
816 )
840
817
841 description = _form['pullrequest_desc']
818 description = _form['pullrequest_desc']
842
819
843 get_default_reviewers_data, validate_default_reviewers = \
820 get_default_reviewers_data, validate_default_reviewers = \
844 PullRequestModel().get_reviewer_functions()
821 PullRequestModel().get_reviewer_functions()
845
822
846 # recalculate reviewers logic, to make sure we can validate this
823 # recalculate reviewers logic, to make sure we can validate this
847 reviewer_rules = get_default_reviewers_data(
824 reviewer_rules = get_default_reviewers_data(
848 self._rhodecode_db_user, source_db_repo,
825 self._rhodecode_db_user, source_db_repo,
849 source_commit, target_db_repo, target_commit)
826 source_commit, target_db_repo, target_commit)
850
827
851 given_reviewers = _form['review_members']
828 given_reviewers = _form['review_members']
852 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
829 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
853
830
854 try:
831 try:
855 pull_request = PullRequestModel().create(
832 pull_request = PullRequestModel().create(
856 self._rhodecode_user.user_id, source_repo, source_ref,
833 self._rhodecode_user.user_id, source_repo, source_ref,
857 target_repo, target_ref, commit_ids, reviewers,
834 target_repo, target_ref, commit_ids, reviewers,
858 pullrequest_title, description, reviewer_rules
835 pullrequest_title, description, reviewer_rules
859 )
836 )
860 Session().commit()
837 Session().commit()
861
838
862 h.flash(_('Successfully opened new pull request'),
839 h.flash(_('Successfully opened new pull request'),
863 category='success')
840 category='success')
864 except Exception:
841 except Exception:
865 msg = _('Error occurred during creation of this pull request.')
842 msg = _('Error occurred during creation of this pull request.')
866 log.exception(msg)
843 log.exception(msg)
867 h.flash(msg, category='error')
844 h.flash(msg, category='error')
868
845
869 # copy the args back to redirect
846 # copy the args back to redirect
870 org_query = self.request.GET.mixed()
847 org_query = self.request.GET.mixed()
871 raise HTTPFound(
848 raise HTTPFound(
872 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
849 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
873 _query=org_query))
850 _query=org_query))
874
851
875 raise HTTPFound(
852 raise HTTPFound(
876 h.route_path('pullrequest_show', repo_name=target_repo,
853 h.route_path('pullrequest_show', repo_name=target_repo,
877 pull_request_id=pull_request.pull_request_id))
854 pull_request_id=pull_request.pull_request_id))
878
855
879 @LoginRequired()
856 @LoginRequired()
880 @NotAnonymous()
857 @NotAnonymous()
881 @HasRepoPermissionAnyDecorator(
858 @HasRepoPermissionAnyDecorator(
882 'repository.read', 'repository.write', 'repository.admin')
859 'repository.read', 'repository.write', 'repository.admin')
883 @CSRFRequired()
860 @CSRFRequired()
884 @view_config(
861 @view_config(
885 route_name='pullrequest_update', request_method='POST',
862 route_name='pullrequest_update', request_method='POST',
886 renderer='json_ext')
863 renderer='json_ext')
887 def pull_request_update(self):
864 def pull_request_update(self):
888 pull_request = PullRequest.get_or_404(
865 pull_request = PullRequest.get_or_404(
889 self.request.matchdict['pull_request_id'])
866 self.request.matchdict['pull_request_id'])
890 _ = self.request.translate
867 _ = self.request.translate
891
868
892 self.load_default_context()
869 self.load_default_context()
893
870
894 if pull_request.is_closed():
871 if pull_request.is_closed():
895 log.debug('update: forbidden because pull request is closed')
872 log.debug('update: forbidden because pull request is closed')
896 msg = _(u'Cannot update closed pull requests.')
873 msg = _(u'Cannot update closed pull requests.')
897 h.flash(msg, category='error')
874 h.flash(msg, category='error')
898 return True
875 return True
899
876
900 # only owner or admin can update it
877 # only owner or admin can update it
901 allowed_to_update = PullRequestModel().check_user_update(
878 allowed_to_update = PullRequestModel().check_user_update(
902 pull_request, self._rhodecode_user)
879 pull_request, self._rhodecode_user)
903 if allowed_to_update:
880 if allowed_to_update:
904 controls = peppercorn.parse(self.request.POST.items())
881 controls = peppercorn.parse(self.request.POST.items())
905
882
906 if 'review_members' in controls:
883 if 'review_members' in controls:
907 self._update_reviewers(
884 self._update_reviewers(
908 pull_request, controls['review_members'],
885 pull_request, controls['review_members'],
909 pull_request.reviewer_data)
886 pull_request.reviewer_data)
910 elif str2bool(self.request.POST.get('update_commits', 'false')):
887 elif str2bool(self.request.POST.get('update_commits', 'false')):
911 self._update_commits(pull_request)
888 self._update_commits(pull_request)
912 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
889 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
913 self._edit_pull_request(pull_request)
890 self._edit_pull_request(pull_request)
914 else:
891 else:
915 raise HTTPBadRequest()
892 raise HTTPBadRequest()
916 return True
893 return True
917 raise HTTPForbidden()
894 raise HTTPForbidden()
918
895
919 def _edit_pull_request(self, pull_request):
896 def _edit_pull_request(self, pull_request):
920 _ = self.request.translate
897 _ = self.request.translate
921 try:
898 try:
922 PullRequestModel().edit(
899 PullRequestModel().edit(
923 pull_request, self.request.POST.get('title'),
900 pull_request, self.request.POST.get('title'),
924 self.request.POST.get('description'), self._rhodecode_user)
901 self.request.POST.get('description'), self._rhodecode_user)
925 except ValueError:
902 except ValueError:
926 msg = _(u'Cannot update closed pull requests.')
903 msg = _(u'Cannot update closed pull requests.')
927 h.flash(msg, category='error')
904 h.flash(msg, category='error')
928 return
905 return
929 else:
906 else:
930 Session().commit()
907 Session().commit()
931
908
932 msg = _(u'Pull request title & description updated.')
909 msg = _(u'Pull request title & description updated.')
933 h.flash(msg, category='success')
910 h.flash(msg, category='success')
934 return
911 return
935
912
936 def _update_commits(self, pull_request):
913 def _update_commits(self, pull_request):
937 _ = self.request.translate
914 _ = self.request.translate
938 resp = PullRequestModel().update_commits(pull_request)
915 resp = PullRequestModel().update_commits(pull_request)
939
916
940 if resp.executed:
917 if resp.executed:
941
918
942 if resp.target_changed and resp.source_changed:
919 if resp.target_changed and resp.source_changed:
943 changed = 'target and source repositories'
920 changed = 'target and source repositories'
944 elif resp.target_changed and not resp.source_changed:
921 elif resp.target_changed and not resp.source_changed:
945 changed = 'target repository'
922 changed = 'target repository'
946 elif not resp.target_changed and resp.source_changed:
923 elif not resp.target_changed and resp.source_changed:
947 changed = 'source repository'
924 changed = 'source repository'
948 else:
925 else:
949 changed = 'nothing'
926 changed = 'nothing'
950
927
951 msg = _(
928 msg = _(
952 u'Pull request updated to "{source_commit_id}" with '
929 u'Pull request updated to "{source_commit_id}" with '
953 u'{count_added} added, {count_removed} removed commits. '
930 u'{count_added} added, {count_removed} removed commits. '
954 u'Source of changes: {change_source}')
931 u'Source of changes: {change_source}')
955 msg = msg.format(
932 msg = msg.format(
956 source_commit_id=pull_request.source_ref_parts.commit_id,
933 source_commit_id=pull_request.source_ref_parts.commit_id,
957 count_added=len(resp.changes.added),
934 count_added=len(resp.changes.added),
958 count_removed=len(resp.changes.removed),
935 count_removed=len(resp.changes.removed),
959 change_source=changed)
936 change_source=changed)
960 h.flash(msg, category='success')
937 h.flash(msg, category='success')
961
938
962 channel = '/repo${}$/pr/{}'.format(
939 channel = '/repo${}$/pr/{}'.format(
963 pull_request.target_repo.repo_name,
940 pull_request.target_repo.repo_name,
964 pull_request.pull_request_id)
941 pull_request.pull_request_id)
965 message = msg + (
942 message = msg + (
966 ' - <a onclick="window.location.reload()">'
943 ' - <a onclick="window.location.reload()">'
967 '<strong>{}</strong></a>'.format(_('Reload page')))
944 '<strong>{}</strong></a>'.format(_('Reload page')))
968 channelstream.post_message(
945 channelstream.post_message(
969 channel, message, self._rhodecode_user.username,
946 channel, message, self._rhodecode_user.username,
970 registry=self.request.registry)
947 registry=self.request.registry)
971 else:
948 else:
972 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
949 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
973 warning_reasons = [
950 warning_reasons = [
974 UpdateFailureReason.NO_CHANGE,
951 UpdateFailureReason.NO_CHANGE,
975 UpdateFailureReason.WRONG_REF_TYPE,
952 UpdateFailureReason.WRONG_REF_TYPE,
976 ]
953 ]
977 category = 'warning' if resp.reason in warning_reasons else 'error'
954 category = 'warning' if resp.reason in warning_reasons else 'error'
978 h.flash(msg, category=category)
955 h.flash(msg, category=category)
979
956
980 @LoginRequired()
957 @LoginRequired()
981 @NotAnonymous()
958 @NotAnonymous()
982 @HasRepoPermissionAnyDecorator(
959 @HasRepoPermissionAnyDecorator(
983 'repository.read', 'repository.write', 'repository.admin')
960 'repository.read', 'repository.write', 'repository.admin')
984 @CSRFRequired()
961 @CSRFRequired()
985 @view_config(
962 @view_config(
986 route_name='pullrequest_merge', request_method='POST',
963 route_name='pullrequest_merge', request_method='POST',
987 renderer='json_ext')
964 renderer='json_ext')
988 def pull_request_merge(self):
965 def pull_request_merge(self):
989 """
966 """
990 Merge will perform a server-side merge of the specified
967 Merge will perform a server-side merge of the specified
991 pull request, if the pull request is approved and mergeable.
968 pull request, if the pull request is approved and mergeable.
992 After successful merging, the pull request is automatically
969 After successful merging, the pull request is automatically
993 closed, with a relevant comment.
970 closed, with a relevant comment.
994 """
971 """
995 pull_request = PullRequest.get_or_404(
972 pull_request = PullRequest.get_or_404(
996 self.request.matchdict['pull_request_id'])
973 self.request.matchdict['pull_request_id'])
997
974
998 self.load_default_context()
975 self.load_default_context()
999 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1000 translator=self.request.translate)
977 translator=self.request.translate)
1001 merge_possible = not check.failed
978 merge_possible = not check.failed
1002
979
1003 for err_type, error_msg in check.errors:
980 for err_type, error_msg in check.errors:
1004 h.flash(error_msg, category=err_type)
981 h.flash(error_msg, category=err_type)
1005
982
1006 if merge_possible:
983 if merge_possible:
1007 log.debug("Pre-conditions checked, trying to merge.")
984 log.debug("Pre-conditions checked, trying to merge.")
1008 extras = vcs_operation_context(
985 extras = vcs_operation_context(
1009 self.request.environ, repo_name=pull_request.target_repo.repo_name,
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1010 username=self._rhodecode_db_user.username, action='push',
987 username=self._rhodecode_db_user.username, action='push',
1011 scm=pull_request.target_repo.repo_type)
988 scm=pull_request.target_repo.repo_type)
1012 self._merge_pull_request(
989 self._merge_pull_request(
1013 pull_request, self._rhodecode_db_user, extras)
990 pull_request, self._rhodecode_db_user, extras)
1014 else:
991 else:
1015 log.debug("Pre-conditions failed, NOT merging.")
992 log.debug("Pre-conditions failed, NOT merging.")
1016
993
1017 raise HTTPFound(
994 raise HTTPFound(
1018 h.route_path('pullrequest_show',
995 h.route_path('pullrequest_show',
1019 repo_name=pull_request.target_repo.repo_name,
996 repo_name=pull_request.target_repo.repo_name,
1020 pull_request_id=pull_request.pull_request_id))
997 pull_request_id=pull_request.pull_request_id))
1021
998
1022 def _merge_pull_request(self, pull_request, user, extras):
999 def _merge_pull_request(self, pull_request, user, extras):
1023 _ = self.request.translate
1000 _ = self.request.translate
1024 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1025
1002
1026 if merge_resp.executed:
1003 if merge_resp.executed:
1027 log.debug("The merge was successful, closing the pull request.")
1004 log.debug("The merge was successful, closing the pull request.")
1028 PullRequestModel().close_pull_request(
1005 PullRequestModel().close_pull_request(
1029 pull_request.pull_request_id, user)
1006 pull_request.pull_request_id, user)
1030 Session().commit()
1007 Session().commit()
1031 msg = _('Pull request was successfully merged and closed.')
1008 msg = _('Pull request was successfully merged and closed.')
1032 h.flash(msg, category='success')
1009 h.flash(msg, category='success')
1033 else:
1010 else:
1034 log.debug(
1011 log.debug(
1035 "The merge was not successful. Merge response: %s",
1012 "The merge was not successful. Merge response: %s",
1036 merge_resp)
1013 merge_resp)
1037 msg = PullRequestModel().merge_status_message(
1014 msg = PullRequestModel().merge_status_message(
1038 merge_resp.failure_reason)
1015 merge_resp.failure_reason)
1039 h.flash(msg, category='error')
1016 h.flash(msg, category='error')
1040
1017
1041 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1042 _ = self.request.translate
1019 _ = self.request.translate
1043 get_default_reviewers_data, validate_default_reviewers = \
1020 get_default_reviewers_data, validate_default_reviewers = \
1044 PullRequestModel().get_reviewer_functions()
1021 PullRequestModel().get_reviewer_functions()
1045
1022
1046 try:
1023 try:
1047 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1048 except ValueError as e:
1025 except ValueError as e:
1049 log.error('Reviewers Validation: {}'.format(e))
1026 log.error('Reviewers Validation: {}'.format(e))
1050 h.flash(e, category='error')
1027 h.flash(e, category='error')
1051 return
1028 return
1052
1029
1053 PullRequestModel().update_reviewers(
1030 PullRequestModel().update_reviewers(
1054 pull_request, reviewers, self._rhodecode_user)
1031 pull_request, reviewers, self._rhodecode_user)
1055 h.flash(_('Pull request reviewers updated.'), category='success')
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1056 Session().commit()
1033 Session().commit()
1057
1034
1058 @LoginRequired()
1035 @LoginRequired()
1059 @NotAnonymous()
1036 @NotAnonymous()
1060 @HasRepoPermissionAnyDecorator(
1037 @HasRepoPermissionAnyDecorator(
1061 'repository.read', 'repository.write', 'repository.admin')
1038 'repository.read', 'repository.write', 'repository.admin')
1062 @CSRFRequired()
1039 @CSRFRequired()
1063 @view_config(
1040 @view_config(
1064 route_name='pullrequest_delete', request_method='POST',
1041 route_name='pullrequest_delete', request_method='POST',
1065 renderer='json_ext')
1042 renderer='json_ext')
1066 def pull_request_delete(self):
1043 def pull_request_delete(self):
1067 _ = self.request.translate
1044 _ = self.request.translate
1068
1045
1069 pull_request = PullRequest.get_or_404(
1046 pull_request = PullRequest.get_or_404(
1070 self.request.matchdict['pull_request_id'])
1047 self.request.matchdict['pull_request_id'])
1071 self.load_default_context()
1048 self.load_default_context()
1072
1049
1073 pr_closed = pull_request.is_closed()
1050 pr_closed = pull_request.is_closed()
1074 allowed_to_delete = PullRequestModel().check_user_delete(
1051 allowed_to_delete = PullRequestModel().check_user_delete(
1075 pull_request, self._rhodecode_user) and not pr_closed
1052 pull_request, self._rhodecode_user) and not pr_closed
1076
1053
1077 # only owner can delete it !
1054 # only owner can delete it !
1078 if allowed_to_delete:
1055 if allowed_to_delete:
1079 PullRequestModel().delete(pull_request, self._rhodecode_user)
1056 PullRequestModel().delete(pull_request, self._rhodecode_user)
1080 Session().commit()
1057 Session().commit()
1081 h.flash(_('Successfully deleted pull request'),
1058 h.flash(_('Successfully deleted pull request'),
1082 category='success')
1059 category='success')
1083 raise HTTPFound(h.route_path('pullrequest_show_all',
1060 raise HTTPFound(h.route_path('pullrequest_show_all',
1084 repo_name=self.db_repo_name))
1061 repo_name=self.db_repo_name))
1085
1062
1086 log.warning('user %s tried to delete pull request without access',
1063 log.warning('user %s tried to delete pull request without access',
1087 self._rhodecode_user)
1064 self._rhodecode_user)
1088 raise HTTPNotFound()
1065 raise HTTPNotFound()
1089
1066
1090 @LoginRequired()
1067 @LoginRequired()
1091 @NotAnonymous()
1068 @NotAnonymous()
1092 @HasRepoPermissionAnyDecorator(
1069 @HasRepoPermissionAnyDecorator(
1093 'repository.read', 'repository.write', 'repository.admin')
1070 'repository.read', 'repository.write', 'repository.admin')
1094 @CSRFRequired()
1071 @CSRFRequired()
1095 @view_config(
1072 @view_config(
1096 route_name='pullrequest_comment_create', request_method='POST',
1073 route_name='pullrequest_comment_create', request_method='POST',
1097 renderer='json_ext')
1074 renderer='json_ext')
1098 def pull_request_comment_create(self):
1075 def pull_request_comment_create(self):
1099 _ = self.request.translate
1076 _ = self.request.translate
1100
1077
1101 pull_request = PullRequest.get_or_404(
1078 pull_request = PullRequest.get_or_404(
1102 self.request.matchdict['pull_request_id'])
1079 self.request.matchdict['pull_request_id'])
1103 pull_request_id = pull_request.pull_request_id
1080 pull_request_id = pull_request.pull_request_id
1104
1081
1105 if pull_request.is_closed():
1082 if pull_request.is_closed():
1106 log.debug('comment: forbidden because pull request is closed')
1083 log.debug('comment: forbidden because pull request is closed')
1107 raise HTTPForbidden()
1084 raise HTTPForbidden()
1108
1085
1109 allowed_to_comment = PullRequestModel().check_user_comment(
1086 allowed_to_comment = PullRequestModel().check_user_comment(
1110 pull_request, self._rhodecode_user)
1087 pull_request, self._rhodecode_user)
1111 if not allowed_to_comment:
1088 if not allowed_to_comment:
1112 log.debug(
1089 log.debug(
1113 'comment: forbidden because pull request is from forbidden repo')
1090 'comment: forbidden because pull request is from forbidden repo')
1114 raise HTTPForbidden()
1091 raise HTTPForbidden()
1115
1092
1116 c = self.load_default_context()
1093 c = self.load_default_context()
1117
1094
1118 status = self.request.POST.get('changeset_status', None)
1095 status = self.request.POST.get('changeset_status', None)
1119 text = self.request.POST.get('text')
1096 text = self.request.POST.get('text')
1120 comment_type = self.request.POST.get('comment_type')
1097 comment_type = self.request.POST.get('comment_type')
1121 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1098 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1122 close_pull_request = self.request.POST.get('close_pull_request')
1099 close_pull_request = self.request.POST.get('close_pull_request')
1123
1100
1124 # the logic here should work like following, if we submit close
1101 # the logic here should work like following, if we submit close
1125 # pr comment, use `close_pull_request_with_comment` function
1102 # pr comment, use `close_pull_request_with_comment` function
1126 # else handle regular comment logic
1103 # else handle regular comment logic
1127
1104
1128 if close_pull_request:
1105 if close_pull_request:
1129 # only owner or admin or person with write permissions
1106 # only owner or admin or person with write permissions
1130 allowed_to_close = PullRequestModel().check_user_update(
1107 allowed_to_close = PullRequestModel().check_user_update(
1131 pull_request, self._rhodecode_user)
1108 pull_request, self._rhodecode_user)
1132 if not allowed_to_close:
1109 if not allowed_to_close:
1133 log.debug('comment: forbidden because not allowed to close '
1110 log.debug('comment: forbidden because not allowed to close '
1134 'pull request %s', pull_request_id)
1111 'pull request %s', pull_request_id)
1135 raise HTTPForbidden()
1112 raise HTTPForbidden()
1136 comment, status = PullRequestModel().close_pull_request_with_comment(
1113 comment, status = PullRequestModel().close_pull_request_with_comment(
1137 pull_request, self._rhodecode_user, self.db_repo, message=text)
1114 pull_request, self._rhodecode_user, self.db_repo, message=text)
1138 Session().flush()
1115 Session().flush()
1139 events.trigger(
1116 events.trigger(
1140 events.PullRequestCommentEvent(pull_request, comment))
1117 events.PullRequestCommentEvent(pull_request, comment))
1141
1118
1142 else:
1119 else:
1143 # regular comment case, could be inline, or one with status.
1120 # regular comment case, could be inline, or one with status.
1144 # for that one we check also permissions
1121 # for that one we check also permissions
1145
1122
1146 allowed_to_change_status = PullRequestModel().check_user_change_status(
1123 allowed_to_change_status = PullRequestModel().check_user_change_status(
1147 pull_request, self._rhodecode_user)
1124 pull_request, self._rhodecode_user)
1148
1125
1149 if status and allowed_to_change_status:
1126 if status and allowed_to_change_status:
1150 message = (_('Status change %(transition_icon)s %(status)s')
1127 message = (_('Status change %(transition_icon)s %(status)s')
1151 % {'transition_icon': '>',
1128 % {'transition_icon': '>',
1152 'status': ChangesetStatus.get_status_lbl(status)})
1129 'status': ChangesetStatus.get_status_lbl(status)})
1153 text = text or message
1130 text = text or message
1154
1131
1155 comment = CommentsModel().create(
1132 comment = CommentsModel().create(
1156 text=text,
1133 text=text,
1157 repo=self.db_repo.repo_id,
1134 repo=self.db_repo.repo_id,
1158 user=self._rhodecode_user.user_id,
1135 user=self._rhodecode_user.user_id,
1159 pull_request=pull_request,
1136 pull_request=pull_request,
1160 f_path=self.request.POST.get('f_path'),
1137 f_path=self.request.POST.get('f_path'),
1161 line_no=self.request.POST.get('line'),
1138 line_no=self.request.POST.get('line'),
1162 status_change=(ChangesetStatus.get_status_lbl(status)
1139 status_change=(ChangesetStatus.get_status_lbl(status)
1163 if status and allowed_to_change_status else None),
1140 if status and allowed_to_change_status else None),
1164 status_change_type=(status
1141 status_change_type=(status
1165 if status and allowed_to_change_status else None),
1142 if status and allowed_to_change_status else None),
1166 comment_type=comment_type,
1143 comment_type=comment_type,
1167 resolves_comment_id=resolves_comment_id
1144 resolves_comment_id=resolves_comment_id
1168 )
1145 )
1169
1146
1170 if allowed_to_change_status:
1147 if allowed_to_change_status:
1171 # calculate old status before we change it
1148 # calculate old status before we change it
1172 old_calculated_status = pull_request.calculated_review_status()
1149 old_calculated_status = pull_request.calculated_review_status()
1173
1150
1174 # get status if set !
1151 # get status if set !
1175 if status:
1152 if status:
1176 ChangesetStatusModel().set_status(
1153 ChangesetStatusModel().set_status(
1177 self.db_repo.repo_id,
1154 self.db_repo.repo_id,
1178 status,
1155 status,
1179 self._rhodecode_user.user_id,
1156 self._rhodecode_user.user_id,
1180 comment,
1157 comment,
1181 pull_request=pull_request
1158 pull_request=pull_request
1182 )
1159 )
1183
1160
1184 Session().flush()
1161 Session().flush()
1185 events.trigger(
1162 events.trigger(
1186 events.PullRequestCommentEvent(pull_request, comment))
1163 events.PullRequestCommentEvent(pull_request, comment))
1187
1164
1188 # we now calculate the status of pull request, and based on that
1165 # we now calculate the status of pull request, and based on that
1189 # calculation we set the commits status
1166 # calculation we set the commits status
1190 calculated_status = pull_request.calculated_review_status()
1167 calculated_status = pull_request.calculated_review_status()
1191 if old_calculated_status != calculated_status:
1168 if old_calculated_status != calculated_status:
1192 PullRequestModel()._trigger_pull_request_hook(
1169 PullRequestModel()._trigger_pull_request_hook(
1193 pull_request, self._rhodecode_user, 'review_status_change')
1170 pull_request, self._rhodecode_user, 'review_status_change')
1194
1171
1195 Session().commit()
1172 Session().commit()
1196
1173
1197 data = {
1174 data = {
1198 'target_id': h.safeid(h.safe_unicode(
1175 'target_id': h.safeid(h.safe_unicode(
1199 self.request.POST.get('f_path'))),
1176 self.request.POST.get('f_path'))),
1200 }
1177 }
1201 if comment:
1178 if comment:
1202 c.co = comment
1179 c.co = comment
1203 rendered_comment = render(
1180 rendered_comment = render(
1204 'rhodecode:templates/changeset/changeset_comment_block.mako',
1181 'rhodecode:templates/changeset/changeset_comment_block.mako',
1205 self._get_template_context(c), self.request)
1182 self._get_template_context(c), self.request)
1206
1183
1207 data.update(comment.get_dict())
1184 data.update(comment.get_dict())
1208 data.update({'rendered_text': rendered_comment})
1185 data.update({'rendered_text': rendered_comment})
1209
1186
1210 return data
1187 return data
1211
1188
1212 @LoginRequired()
1189 @LoginRequired()
1213 @NotAnonymous()
1190 @NotAnonymous()
1214 @HasRepoPermissionAnyDecorator(
1191 @HasRepoPermissionAnyDecorator(
1215 'repository.read', 'repository.write', 'repository.admin')
1192 'repository.read', 'repository.write', 'repository.admin')
1216 @CSRFRequired()
1193 @CSRFRequired()
1217 @view_config(
1194 @view_config(
1218 route_name='pullrequest_comment_delete', request_method='POST',
1195 route_name='pullrequest_comment_delete', request_method='POST',
1219 renderer='json_ext')
1196 renderer='json_ext')
1220 def pull_request_comment_delete(self):
1197 def pull_request_comment_delete(self):
1221 pull_request = PullRequest.get_or_404(
1198 pull_request = PullRequest.get_or_404(
1222 self.request.matchdict['pull_request_id'])
1199 self.request.matchdict['pull_request_id'])
1223
1200
1224 comment = ChangesetComment.get_or_404(
1201 comment = ChangesetComment.get_or_404(
1225 self.request.matchdict['comment_id'])
1202 self.request.matchdict['comment_id'])
1226 comment_id = comment.comment_id
1203 comment_id = comment.comment_id
1227
1204
1228 if pull_request.is_closed():
1205 if pull_request.is_closed():
1229 log.debug('comment: forbidden because pull request is closed')
1206 log.debug('comment: forbidden because pull request is closed')
1230 raise HTTPForbidden()
1207 raise HTTPForbidden()
1231
1208
1232 if not comment:
1209 if not comment:
1233 log.debug('Comment with id:%s not found, skipping', comment_id)
1210 log.debug('Comment with id:%s not found, skipping', comment_id)
1234 # comment already deleted in another call probably
1211 # comment already deleted in another call probably
1235 return True
1212 return True
1236
1213
1237 if comment.pull_request.is_closed():
1214 if comment.pull_request.is_closed():
1238 # don't allow deleting comments on closed pull request
1215 # don't allow deleting comments on closed pull request
1239 raise HTTPForbidden()
1216 raise HTTPForbidden()
1240
1217
1241 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1218 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1242 super_admin = h.HasPermissionAny('hg.admin')()
1219 super_admin = h.HasPermissionAny('hg.admin')()
1243 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1220 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1244 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1221 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1245 comment_repo_admin = is_repo_admin and is_repo_comment
1222 comment_repo_admin = is_repo_admin and is_repo_comment
1246
1223
1247 if super_admin or comment_owner or comment_repo_admin:
1224 if super_admin or comment_owner or comment_repo_admin:
1248 old_calculated_status = comment.pull_request.calculated_review_status()
1225 old_calculated_status = comment.pull_request.calculated_review_status()
1249 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1226 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1250 Session().commit()
1227 Session().commit()
1251 calculated_status = comment.pull_request.calculated_review_status()
1228 calculated_status = comment.pull_request.calculated_review_status()
1252 if old_calculated_status != calculated_status:
1229 if old_calculated_status != calculated_status:
1253 PullRequestModel()._trigger_pull_request_hook(
1230 PullRequestModel()._trigger_pull_request_hook(
1254 comment.pull_request, self._rhodecode_user, 'review_status_change')
1231 comment.pull_request, self._rhodecode_user, 'review_status_change')
1255 return True
1232 return True
1256 else:
1233 else:
1257 log.warning('No permissions for user %s to delete comment_id: %s',
1234 log.warning('No permissions for user %s to delete comment_id: %s',
1258 self._rhodecode_db_user, comment_id)
1235 self._rhodecode_db_user, comment_id)
1259 raise HTTPNotFound()
1236 raise HTTPNotFound()
@@ -1,1628 +1,1651 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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
26
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 import collections
31 import collections
32
32
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext#, _
36 from rhodecode.translation import lazy_ugettext#, _
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository)
54 PullRequestVersion, ChangesetComment, Repository)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = 3
76 DIFF_CONTEXT = 3
77
77
78 MERGE_STATUS_MESSAGES = {
78 MERGE_STATUS_MESSAGES = {
79 MergeFailureReason.NONE: lazy_ugettext(
79 MergeFailureReason.NONE: lazy_ugettext(
80 'This pull request can be automatically merged.'),
80 'This pull request can be automatically merged.'),
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
81 MergeFailureReason.UNKNOWN: lazy_ugettext(
82 'This pull request cannot be merged because of an unhandled'
82 'This pull request cannot be merged because of an unhandled'
83 ' exception.'),
83 ' exception.'),
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
84 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
85 'This pull request cannot be merged because of merge conflicts.'),
85 'This pull request cannot be merged because of merge conflicts.'),
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
86 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
87 'This pull request could not be merged because push to target'
87 'This pull request could not be merged because push to target'
88 ' failed.'),
88 ' failed.'),
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
89 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
90 'This pull request cannot be merged because the target is not a'
90 'This pull request cannot be merged because the target is not a'
91 ' head.'),
91 ' head.'),
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
92 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
93 'This pull request cannot be merged because the source contains'
93 'This pull request cannot be merged because the source contains'
94 ' more branches than the target.'),
94 ' more branches than the target.'),
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
95 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
96 'This pull request cannot be merged because the target has'
96 'This pull request cannot be merged because the target has'
97 ' multiple heads.'),
97 ' multiple heads.'),
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
98 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
99 'This pull request cannot be merged because the target repository'
99 'This pull request cannot be merged because the target repository'
100 ' is locked.'),
100 ' is locked.'),
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
101 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
102 'This pull request cannot be merged because the target or the '
102 'This pull request cannot be merged because the target or the '
103 'source reference is missing.'),
103 'source reference is missing.'),
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
104 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
105 'This pull request cannot be merged because the target '
105 'This pull request cannot be merged because the target '
106 'reference is missing.'),
106 'reference is missing.'),
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
107 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
108 'This pull request cannot be merged because the source '
108 'This pull request cannot be merged because the source '
109 'reference is missing.'),
109 'reference is missing.'),
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
110 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
111 'This pull request cannot be merged because of conflicts related '
111 'This pull request cannot be merged because of conflicts related '
112 'to sub repositories.'),
112 'to sub repositories.'),
113 }
113 }
114
114
115 UPDATE_STATUS_MESSAGES = {
115 UPDATE_STATUS_MESSAGES = {
116 UpdateFailureReason.NONE: lazy_ugettext(
116 UpdateFailureReason.NONE: lazy_ugettext(
117 'Pull request update successful.'),
117 'Pull request update successful.'),
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
118 UpdateFailureReason.UNKNOWN: lazy_ugettext(
119 'Pull request update failed because of an unknown error.'),
119 'Pull request update failed because of an unknown error.'),
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
120 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
121 'No update needed because the source and target have not changed.'),
121 'No update needed because the source and target have not changed.'),
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
122 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
123 'Pull request cannot be updated because the reference type is '
123 'Pull request cannot be updated because the reference type is '
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
124 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
125 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
126 'This pull request cannot be updated because the target '
126 'This pull request cannot be updated because the target '
127 'reference is missing.'),
127 'reference is missing.'),
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
128 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
129 'This pull request cannot be updated because the source '
129 'This pull request cannot be updated because the source '
130 'reference is missing.'),
130 'reference is missing.'),
131 }
131 }
132
132
133 def __get_pull_request(self, pull_request):
133 def __get_pull_request(self, pull_request):
134 return self._get_instance((
134 return self._get_instance((
135 PullRequest, PullRequestVersion), pull_request)
135 PullRequest, PullRequestVersion), pull_request)
136
136
137 def _check_perms(self, perms, pull_request, user, api=False):
137 def _check_perms(self, perms, pull_request, user, api=False):
138 if not api:
138 if not api:
139 return h.HasRepoPermissionAny(*perms)(
139 return h.HasRepoPermissionAny(*perms)(
140 user=user, repo_name=pull_request.target_repo.repo_name)
140 user=user, repo_name=pull_request.target_repo.repo_name)
141 else:
141 else:
142 return h.HasRepoPermissionAnyApi(*perms)(
142 return h.HasRepoPermissionAnyApi(*perms)(
143 user=user, repo_name=pull_request.target_repo.repo_name)
143 user=user, repo_name=pull_request.target_repo.repo_name)
144
144
145 def check_user_read(self, pull_request, user, api=False):
145 def check_user_read(self, pull_request, user, api=False):
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
146 _perms = ('repository.admin', 'repository.write', 'repository.read',)
147 return self._check_perms(_perms, pull_request, user, api)
147 return self._check_perms(_perms, pull_request, user, api)
148
148
149 def check_user_merge(self, pull_request, user, api=False):
149 def check_user_merge(self, pull_request, user, api=False):
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
150 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
151 return self._check_perms(_perms, pull_request, user, api)
151 return self._check_perms(_perms, pull_request, user, api)
152
152
153 def check_user_update(self, pull_request, user, api=False):
153 def check_user_update(self, pull_request, user, api=False):
154 owner = user.user_id == pull_request.user_id
154 owner = user.user_id == pull_request.user_id
155 return self.check_user_merge(pull_request, user, api) or owner
155 return self.check_user_merge(pull_request, user, api) or owner
156
156
157 def check_user_delete(self, pull_request, user):
157 def check_user_delete(self, pull_request, user):
158 owner = user.user_id == pull_request.user_id
158 owner = user.user_id == pull_request.user_id
159 _perms = ('repository.admin',)
159 _perms = ('repository.admin',)
160 return self._check_perms(_perms, pull_request, user) or owner
160 return self._check_perms(_perms, pull_request, user) or owner
161
161
162 def check_user_change_status(self, pull_request, user, api=False):
162 def check_user_change_status(self, pull_request, user, api=False):
163 reviewer = user.user_id in [x.user_id for x in
163 reviewer = user.user_id in [x.user_id for x in
164 pull_request.reviewers]
164 pull_request.reviewers]
165 return self.check_user_update(pull_request, user, api) or reviewer
165 return self.check_user_update(pull_request, user, api) or reviewer
166
166
167 def check_user_comment(self, pull_request, user):
167 def check_user_comment(self, pull_request, user):
168 owner = user.user_id == pull_request.user_id
168 owner = user.user_id == pull_request.user_id
169 return self.check_user_read(pull_request, user) or owner
169 return self.check_user_read(pull_request, user) or owner
170
170
171 def get(self, pull_request):
171 def get(self, pull_request):
172 return self.__get_pull_request(pull_request)
172 return self.__get_pull_request(pull_request)
173
173
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
174 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
175 opened_by=None, order_by=None,
175 opened_by=None, order_by=None,
176 order_dir='desc'):
176 order_dir='desc'):
177 repo = None
177 repo = None
178 if repo_name:
178 if repo_name:
179 repo = self._get_repo(repo_name)
179 repo = self._get_repo(repo_name)
180
180
181 q = PullRequest.query()
181 q = PullRequest.query()
182
182
183 # source or target
183 # source or target
184 if repo and source:
184 if repo and source:
185 q = q.filter(PullRequest.source_repo == repo)
185 q = q.filter(PullRequest.source_repo == repo)
186 elif repo:
186 elif repo:
187 q = q.filter(PullRequest.target_repo == repo)
187 q = q.filter(PullRequest.target_repo == repo)
188
188
189 # closed,opened
189 # closed,opened
190 if statuses:
190 if statuses:
191 q = q.filter(PullRequest.status.in_(statuses))
191 q = q.filter(PullRequest.status.in_(statuses))
192
192
193 # opened by filter
193 # opened by filter
194 if opened_by:
194 if opened_by:
195 q = q.filter(PullRequest.user_id.in_(opened_by))
195 q = q.filter(PullRequest.user_id.in_(opened_by))
196
196
197 if order_by:
197 if order_by:
198 order_map = {
198 order_map = {
199 'name_raw': PullRequest.pull_request_id,
199 'name_raw': PullRequest.pull_request_id,
200 'title': PullRequest.title,
200 'title': PullRequest.title,
201 'updated_on_raw': PullRequest.updated_on,
201 'updated_on_raw': PullRequest.updated_on,
202 'target_repo': PullRequest.target_repo_id
202 'target_repo': PullRequest.target_repo_id
203 }
203 }
204 if order_dir == 'asc':
204 if order_dir == 'asc':
205 q = q.order_by(order_map[order_by].asc())
205 q = q.order_by(order_map[order_by].asc())
206 else:
206 else:
207 q = q.order_by(order_map[order_by].desc())
207 q = q.order_by(order_map[order_by].desc())
208
208
209 return q
209 return q
210
210
211 def count_all(self, repo_name, source=False, statuses=None,
211 def count_all(self, repo_name, source=False, statuses=None,
212 opened_by=None):
212 opened_by=None):
213 """
213 """
214 Count the number of pull requests for a specific repository.
214 Count the number of pull requests for a specific repository.
215
215
216 :param repo_name: target or source repo
216 :param repo_name: target or source repo
217 :param source: boolean flag to specify if repo_name refers to source
217 :param source: boolean flag to specify if repo_name refers to source
218 :param statuses: list of pull request statuses
218 :param statuses: list of pull request statuses
219 :param opened_by: author user of the pull request
219 :param opened_by: author user of the pull request
220 :returns: int number of pull requests
220 :returns: int number 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
224
225 return q.count()
225 return q.count()
226
226
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
227 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
228 offset=0, length=None, order_by=None, order_dir='desc'):
228 offset=0, length=None, order_by=None, order_dir='desc'):
229 """
229 """
230 Get all pull requests for a specific repository.
230 Get all pull requests for a specific repository.
231
231
232 :param repo_name: target or source repo
232 :param repo_name: target or source repo
233 :param source: boolean flag to specify if repo_name refers to source
233 :param source: boolean flag to specify if repo_name refers to source
234 :param statuses: list of pull request statuses
234 :param statuses: list of pull request statuses
235 :param opened_by: author user of the pull request
235 :param opened_by: author user of the pull request
236 :param offset: pagination offset
236 :param offset: pagination offset
237 :param length: length of returned list
237 :param length: length of returned list
238 :param order_by: order of the returned list
238 :param order_by: order of the returned list
239 :param order_dir: 'asc' or 'desc' ordering direction
239 :param order_dir: 'asc' or 'desc' ordering direction
240 :returns: list of pull requests
240 :returns: list of pull requests
241 """
241 """
242 q = self._prepare_get_all_query(
242 q = self._prepare_get_all_query(
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
243 repo_name, source=source, statuses=statuses, opened_by=opened_by,
244 order_by=order_by, order_dir=order_dir)
244 order_by=order_by, order_dir=order_dir)
245
245
246 if length:
246 if length:
247 pull_requests = q.limit(length).offset(offset).all()
247 pull_requests = q.limit(length).offset(offset).all()
248 else:
248 else:
249 pull_requests = q.all()
249 pull_requests = q.all()
250
250
251 return pull_requests
251 return pull_requests
252
252
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
253 def count_awaiting_review(self, repo_name, source=False, statuses=None,
254 opened_by=None):
254 opened_by=None):
255 """
255 """
256 Count the number of pull requests for a specific repository that are
256 Count the number of pull requests for a specific repository that are
257 awaiting review.
257 awaiting review.
258
258
259 :param repo_name: target or source repo
259 :param repo_name: target or source repo
260 :param source: boolean flag to specify if repo_name refers to source
260 :param source: boolean flag to specify if repo_name refers to source
261 :param statuses: list of pull request statuses
261 :param statuses: list of pull request statuses
262 :param opened_by: author user of the pull request
262 :param opened_by: author user of the pull request
263 :returns: int number of pull requests
263 :returns: int number of pull requests
264 """
264 """
265 pull_requests = self.get_awaiting_review(
265 pull_requests = self.get_awaiting_review(
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
266 repo_name, source=source, statuses=statuses, opened_by=opened_by)
267
267
268 return len(pull_requests)
268 return len(pull_requests)
269
269
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
270 def get_awaiting_review(self, repo_name, source=False, statuses=None,
271 opened_by=None, offset=0, length=None,
271 opened_by=None, offset=0, length=None,
272 order_by=None, order_dir='desc'):
272 order_by=None, order_dir='desc'):
273 """
273 """
274 Get all pull requests for a specific repository that are awaiting
274 Get all pull requests for a specific repository that are awaiting
275 review.
275 review.
276
276
277 :param repo_name: target or source repo
277 :param repo_name: target or source repo
278 :param source: boolean flag to specify if repo_name refers to source
278 :param source: boolean flag to specify if repo_name refers to source
279 :param statuses: list of pull request statuses
279 :param statuses: list of pull request statuses
280 :param opened_by: author user of the pull request
280 :param opened_by: author user of the pull request
281 :param 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 _filtered_pull_requests = []
291 _filtered_pull_requests = []
292 for pr in pull_requests:
292 for pr in pull_requests:
293 status = pr.calculated_review_status()
293 status = pr.calculated_review_status()
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
294 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
295 ChangesetStatus.STATUS_UNDER_REVIEW]:
296 _filtered_pull_requests.append(pr)
296 _filtered_pull_requests.append(pr)
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 count_awaiting_my_review(self, repo_name, source=False, statuses=None,
302 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
303 opened_by=None, user_id=None):
303 opened_by=None, user_id=None):
304 """
304 """
305 Count the number of pull requests for a specific repository that are
305 Count the number of pull requests for a specific repository that are
306 awaiting review from a specific user.
306 awaiting 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 :returns: int number of pull requests
313 :returns: int number of pull requests
314 """
314 """
315 pull_requests = self.get_awaiting_my_review(
315 pull_requests = self.get_awaiting_my_review(
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
316 repo_name, source=source, statuses=statuses, opened_by=opened_by,
317 user_id=user_id)
317 user_id=user_id)
318
318
319 return len(pull_requests)
319 return len(pull_requests)
320
320
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
321 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
322 opened_by=None, user_id=None, offset=0,
322 opened_by=None, user_id=None, offset=0,
323 length=None, order_by=None, order_dir='desc'):
323 length=None, order_by=None, order_dir='desc'):
324 """
324 """
325 Get all pull requests for a specific repository that are awaiting
325 Get all pull requests for a specific repository that are awaiting
326 review from a specific user.
326 review from a specific user.
327
327
328 :param repo_name: target or source repo
328 :param repo_name: target or source repo
329 :param source: boolean flag to specify if repo_name refers to source
329 :param source: boolean flag to specify if repo_name refers to source
330 :param statuses: list of pull request statuses
330 :param statuses: list of pull request statuses
331 :param opened_by: author user of the pull request
331 :param opened_by: author user of the pull request
332 :param user_id: reviewer user of the pull request
332 :param user_id: reviewer user of the pull request
333 :param offset: pagination offset
333 :param offset: pagination offset
334 :param length: length of returned list
334 :param length: length of returned list
335 :param order_by: order of the returned list
335 :param order_by: order of the returned list
336 :param order_dir: 'asc' or 'desc' ordering direction
336 :param order_dir: 'asc' or 'desc' ordering direction
337 :returns: list of pull requests
337 :returns: list of pull requests
338 """
338 """
339 pull_requests = self.get_all(
339 pull_requests = self.get_all(
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
340 repo_name, source=source, statuses=statuses, opened_by=opened_by,
341 order_by=order_by, order_dir=order_dir)
341 order_by=order_by, order_dir=order_dir)
342
342
343 _my = PullRequestModel().get_not_reviewed(user_id)
343 _my = PullRequestModel().get_not_reviewed(user_id)
344 my_participation = []
344 my_participation = []
345 for pr in pull_requests:
345 for pr in pull_requests:
346 if pr in _my:
346 if pr in _my:
347 my_participation.append(pr)
347 my_participation.append(pr)
348 _filtered_pull_requests = my_participation
348 _filtered_pull_requests = my_participation
349 if length:
349 if length:
350 return _filtered_pull_requests[offset:offset+length]
350 return _filtered_pull_requests[offset:offset+length]
351 else:
351 else:
352 return _filtered_pull_requests
352 return _filtered_pull_requests
353
353
354 def get_not_reviewed(self, user_id):
354 def get_not_reviewed(self, user_id):
355 return [
355 return [
356 x.pull_request for x in PullRequestReviewers.query().filter(
356 x.pull_request for x in PullRequestReviewers.query().filter(
357 PullRequestReviewers.user_id == user_id).all()
357 PullRequestReviewers.user_id == user_id).all()
358 ]
358 ]
359
359
360 def _prepare_participating_query(self, user_id=None, statuses=None,
360 def _prepare_participating_query(self, user_id=None, statuses=None,
361 order_by=None, order_dir='desc'):
361 order_by=None, order_dir='desc'):
362 q = PullRequest.query()
362 q = PullRequest.query()
363 if user_id:
363 if user_id:
364 reviewers_subquery = Session().query(
364 reviewers_subquery = Session().query(
365 PullRequestReviewers.pull_request_id).filter(
365 PullRequestReviewers.pull_request_id).filter(
366 PullRequestReviewers.user_id == user_id).subquery()
366 PullRequestReviewers.user_id == user_id).subquery()
367 user_filter = or_(
367 user_filter = or_(
368 PullRequest.user_id == user_id,
368 PullRequest.user_id == user_id,
369 PullRequest.pull_request_id.in_(reviewers_subquery)
369 PullRequest.pull_request_id.in_(reviewers_subquery)
370 )
370 )
371 q = PullRequest.query().filter(user_filter)
371 q = PullRequest.query().filter(user_filter)
372
372
373 # closed,opened
373 # closed,opened
374 if statuses:
374 if statuses:
375 q = q.filter(PullRequest.status.in_(statuses))
375 q = q.filter(PullRequest.status.in_(statuses))
376
376
377 if order_by:
377 if order_by:
378 order_map = {
378 order_map = {
379 'name_raw': PullRequest.pull_request_id,
379 'name_raw': PullRequest.pull_request_id,
380 'title': PullRequest.title,
380 'title': PullRequest.title,
381 'updated_on_raw': PullRequest.updated_on,
381 'updated_on_raw': PullRequest.updated_on,
382 'target_repo': PullRequest.target_repo_id
382 'target_repo': PullRequest.target_repo_id
383 }
383 }
384 if order_dir == 'asc':
384 if order_dir == 'asc':
385 q = q.order_by(order_map[order_by].asc())
385 q = q.order_by(order_map[order_by].asc())
386 else:
386 else:
387 q = q.order_by(order_map[order_by].desc())
387 q = q.order_by(order_map[order_by].desc())
388
388
389 return q
389 return q
390
390
391 def count_im_participating_in(self, user_id=None, statuses=None):
391 def count_im_participating_in(self, user_id=None, statuses=None):
392 q = self._prepare_participating_query(user_id, statuses=statuses)
392 q = self._prepare_participating_query(user_id, statuses=statuses)
393 return q.count()
393 return q.count()
394
394
395 def get_im_participating_in(
395 def get_im_participating_in(
396 self, user_id=None, statuses=None, offset=0,
396 self, user_id=None, statuses=None, offset=0,
397 length=None, order_by=None, order_dir='desc'):
397 length=None, order_by=None, order_dir='desc'):
398 """
398 """
399 Get all Pull requests that i'm participating in, or i have opened
399 Get all Pull requests that i'm participating in, or i have opened
400 """
400 """
401
401
402 q = self._prepare_participating_query(
402 q = self._prepare_participating_query(
403 user_id, statuses=statuses, order_by=order_by,
403 user_id, statuses=statuses, order_by=order_by,
404 order_dir=order_dir)
404 order_dir=order_dir)
405
405
406 if length:
406 if length:
407 pull_requests = q.limit(length).offset(offset).all()
407 pull_requests = q.limit(length).offset(offset).all()
408 else:
408 else:
409 pull_requests = q.all()
409 pull_requests = q.all()
410
410
411 return pull_requests
411 return pull_requests
412
412
413 def get_versions(self, pull_request):
413 def get_versions(self, pull_request):
414 """
414 """
415 returns version of pull request sorted by ID descending
415 returns version of pull request sorted by ID descending
416 """
416 """
417 return PullRequestVersion.query()\
417 return PullRequestVersion.query()\
418 .filter(PullRequestVersion.pull_request == pull_request)\
418 .filter(PullRequestVersion.pull_request == pull_request)\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
419 .order_by(PullRequestVersion.pull_request_version_id.asc())\
420 .all()
420 .all()
421
421
422 def get_pr_version(self, pull_request_id, version=None):
423 at_version = None
424
425 if version and version == 'latest':
426 pull_request_ver = PullRequest.get(pull_request_id)
427 pull_request_obj = pull_request_ver
428 _org_pull_request_obj = pull_request_obj
429 at_version = 'latest'
430 elif version:
431 pull_request_ver = PullRequestVersion.get_or_404(version)
432 pull_request_obj = pull_request_ver
433 _org_pull_request_obj = pull_request_ver.pull_request
434 at_version = pull_request_ver.pull_request_version_id
435 else:
436 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
437 pull_request_id)
438
439 pull_request_display_obj = PullRequest.get_pr_display_object(
440 pull_request_obj, _org_pull_request_obj)
441
442 return _org_pull_request_obj, pull_request_obj, \
443 pull_request_display_obj, at_version
444
422 def create(self, created_by, source_repo, source_ref, target_repo,
445 def create(self, created_by, source_repo, source_ref, target_repo,
423 target_ref, revisions, reviewers, title, description=None,
446 target_ref, revisions, reviewers, title, description=None,
424 reviewer_data=None, translator=None):
447 reviewer_data=None, translator=None):
425 translator = translator or get_current_request().translate
448 translator = translator or get_current_request().translate
426
449
427 created_by_user = self._get_user(created_by)
450 created_by_user = self._get_user(created_by)
428 source_repo = self._get_repo(source_repo)
451 source_repo = self._get_repo(source_repo)
429 target_repo = self._get_repo(target_repo)
452 target_repo = self._get_repo(target_repo)
430
453
431 pull_request = PullRequest()
454 pull_request = PullRequest()
432 pull_request.source_repo = source_repo
455 pull_request.source_repo = source_repo
433 pull_request.source_ref = source_ref
456 pull_request.source_ref = source_ref
434 pull_request.target_repo = target_repo
457 pull_request.target_repo = target_repo
435 pull_request.target_ref = target_ref
458 pull_request.target_ref = target_ref
436 pull_request.revisions = revisions
459 pull_request.revisions = revisions
437 pull_request.title = title
460 pull_request.title = title
438 pull_request.description = description
461 pull_request.description = description
439 pull_request.author = created_by_user
462 pull_request.author = created_by_user
440 pull_request.reviewer_data = reviewer_data
463 pull_request.reviewer_data = reviewer_data
441
464
442 Session().add(pull_request)
465 Session().add(pull_request)
443 Session().flush()
466 Session().flush()
444
467
445 reviewer_ids = set()
468 reviewer_ids = set()
446 # members / reviewers
469 # members / reviewers
447 for reviewer_object in reviewers:
470 for reviewer_object in reviewers:
448 user_id, reasons, mandatory = reviewer_object
471 user_id, reasons, mandatory = reviewer_object
449 user = self._get_user(user_id)
472 user = self._get_user(user_id)
450
473
451 # skip duplicates
474 # skip duplicates
452 if user.user_id in reviewer_ids:
475 if user.user_id in reviewer_ids:
453 continue
476 continue
454
477
455 reviewer_ids.add(user.user_id)
478 reviewer_ids.add(user.user_id)
456
479
457 reviewer = PullRequestReviewers()
480 reviewer = PullRequestReviewers()
458 reviewer.user = user
481 reviewer.user = user
459 reviewer.pull_request = pull_request
482 reviewer.pull_request = pull_request
460 reviewer.reasons = reasons
483 reviewer.reasons = reasons
461 reviewer.mandatory = mandatory
484 reviewer.mandatory = mandatory
462 Session().add(reviewer)
485 Session().add(reviewer)
463
486
464 # Set approval status to "Under Review" for all commits which are
487 # Set approval status to "Under Review" for all commits which are
465 # part of this pull request.
488 # part of this pull request.
466 ChangesetStatusModel().set_status(
489 ChangesetStatusModel().set_status(
467 repo=target_repo,
490 repo=target_repo,
468 status=ChangesetStatus.STATUS_UNDER_REVIEW,
491 status=ChangesetStatus.STATUS_UNDER_REVIEW,
469 user=created_by_user,
492 user=created_by_user,
470 pull_request=pull_request
493 pull_request=pull_request
471 )
494 )
472
495
473 MergeCheck.validate(
496 MergeCheck.validate(
474 pull_request, user=created_by_user, translator=translator)
497 pull_request, user=created_by_user, translator=translator)
475
498
476 self.notify_reviewers(pull_request, reviewer_ids)
499 self.notify_reviewers(pull_request, reviewer_ids)
477 self._trigger_pull_request_hook(
500 self._trigger_pull_request_hook(
478 pull_request, created_by_user, 'create')
501 pull_request, created_by_user, 'create')
479
502
480 creation_data = pull_request.get_api_data(with_merge_state=False)
503 creation_data = pull_request.get_api_data(with_merge_state=False)
481 self._log_audit_action(
504 self._log_audit_action(
482 'repo.pull_request.create', {'data': creation_data},
505 'repo.pull_request.create', {'data': creation_data},
483 created_by_user, pull_request)
506 created_by_user, pull_request)
484
507
485 return pull_request
508 return pull_request
486
509
487 def _trigger_pull_request_hook(self, pull_request, user, action):
510 def _trigger_pull_request_hook(self, pull_request, user, action):
488 pull_request = self.__get_pull_request(pull_request)
511 pull_request = self.__get_pull_request(pull_request)
489 target_scm = pull_request.target_repo.scm_instance()
512 target_scm = pull_request.target_repo.scm_instance()
490 if action == 'create':
513 if action == 'create':
491 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
514 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
492 elif action == 'merge':
515 elif action == 'merge':
493 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
516 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
494 elif action == 'close':
517 elif action == 'close':
495 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
518 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
496 elif action == 'review_status_change':
519 elif action == 'review_status_change':
497 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
520 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
498 elif action == 'update':
521 elif action == 'update':
499 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
522 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
500 else:
523 else:
501 return
524 return
502
525
503 trigger_hook(
526 trigger_hook(
504 username=user.username,
527 username=user.username,
505 repo_name=pull_request.target_repo.repo_name,
528 repo_name=pull_request.target_repo.repo_name,
506 repo_alias=target_scm.alias,
529 repo_alias=target_scm.alias,
507 pull_request=pull_request)
530 pull_request=pull_request)
508
531
509 def _get_commit_ids(self, pull_request):
532 def _get_commit_ids(self, pull_request):
510 """
533 """
511 Return the commit ids of the merged pull request.
534 Return the commit ids of the merged pull request.
512
535
513 This method is not dealing correctly yet with the lack of autoupdates
536 This method is not dealing correctly yet with the lack of autoupdates
514 nor with the implicit target updates.
537 nor with the implicit target updates.
515 For example: if a commit in the source repo is already in the target it
538 For example: if a commit in the source repo is already in the target it
516 will be reported anyways.
539 will be reported anyways.
517 """
540 """
518 merge_rev = pull_request.merge_rev
541 merge_rev = pull_request.merge_rev
519 if merge_rev is None:
542 if merge_rev is None:
520 raise ValueError('This pull request was not merged yet')
543 raise ValueError('This pull request was not merged yet')
521
544
522 commit_ids = list(pull_request.revisions)
545 commit_ids = list(pull_request.revisions)
523 if merge_rev not in commit_ids:
546 if merge_rev not in commit_ids:
524 commit_ids.append(merge_rev)
547 commit_ids.append(merge_rev)
525
548
526 return commit_ids
549 return commit_ids
527
550
528 def merge(self, pull_request, user, extras):
551 def merge(self, pull_request, user, extras):
529 log.debug("Merging pull request %s", pull_request.pull_request_id)
552 log.debug("Merging pull request %s", pull_request.pull_request_id)
530 merge_state = self._merge_pull_request(pull_request, user, extras)
553 merge_state = self._merge_pull_request(pull_request, user, extras)
531 if merge_state.executed:
554 if merge_state.executed:
532 log.debug(
555 log.debug(
533 "Merge was successful, updating the pull request comments.")
556 "Merge was successful, updating the pull request comments.")
534 self._comment_and_close_pr(pull_request, user, merge_state)
557 self._comment_and_close_pr(pull_request, user, merge_state)
535
558
536 self._log_audit_action(
559 self._log_audit_action(
537 'repo.pull_request.merge',
560 'repo.pull_request.merge',
538 {'merge_state': merge_state.__dict__},
561 {'merge_state': merge_state.__dict__},
539 user, pull_request)
562 user, pull_request)
540
563
541 else:
564 else:
542 log.warn("Merge failed, not updating the pull request.")
565 log.warn("Merge failed, not updating the pull request.")
543 return merge_state
566 return merge_state
544
567
545 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
568 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
546 target_vcs = pull_request.target_repo.scm_instance()
569 target_vcs = pull_request.target_repo.scm_instance()
547 source_vcs = pull_request.source_repo.scm_instance()
570 source_vcs = pull_request.source_repo.scm_instance()
548 target_ref = self._refresh_reference(
571 target_ref = self._refresh_reference(
549 pull_request.target_ref_parts, target_vcs)
572 pull_request.target_ref_parts, target_vcs)
550
573
551 message = merge_msg or (
574 message = merge_msg or (
552 'Merge pull request #%(pr_id)s from '
575 'Merge pull request #%(pr_id)s from '
553 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
576 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
554 'pr_id': pull_request.pull_request_id,
577 'pr_id': pull_request.pull_request_id,
555 'source_repo': source_vcs.name,
578 'source_repo': source_vcs.name,
556 'source_ref_name': pull_request.source_ref_parts.name,
579 'source_ref_name': pull_request.source_ref_parts.name,
557 'pr_title': pull_request.title
580 'pr_title': pull_request.title
558 }
581 }
559
582
560 workspace_id = self._workspace_id(pull_request)
583 workspace_id = self._workspace_id(pull_request)
561 use_rebase = self._use_rebase_for_merging(pull_request)
584 use_rebase = self._use_rebase_for_merging(pull_request)
562 close_branch = self._close_branch_before_merging(pull_request)
585 close_branch = self._close_branch_before_merging(pull_request)
563
586
564 callback_daemon, extras = prepare_callback_daemon(
587 callback_daemon, extras = prepare_callback_daemon(
565 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
588 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
566 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
589 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
567
590
568 with callback_daemon:
591 with callback_daemon:
569 # TODO: johbo: Implement a clean way to run a config_override
592 # TODO: johbo: Implement a clean way to run a config_override
570 # for a single call.
593 # for a single call.
571 target_vcs.config.set(
594 target_vcs.config.set(
572 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
595 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
573 merge_state = target_vcs.merge(
596 merge_state = target_vcs.merge(
574 target_ref, source_vcs, pull_request.source_ref_parts,
597 target_ref, source_vcs, pull_request.source_ref_parts,
575 workspace_id, user_name=user.username,
598 workspace_id, user_name=user.username,
576 user_email=user.email, message=message, use_rebase=use_rebase,
599 user_email=user.email, message=message, use_rebase=use_rebase,
577 close_branch=close_branch)
600 close_branch=close_branch)
578 return merge_state
601 return merge_state
579
602
580 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
603 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
581 pull_request.merge_rev = merge_state.merge_ref.commit_id
604 pull_request.merge_rev = merge_state.merge_ref.commit_id
582 pull_request.updated_on = datetime.datetime.now()
605 pull_request.updated_on = datetime.datetime.now()
583 close_msg = close_msg or 'Pull request merged and closed'
606 close_msg = close_msg or 'Pull request merged and closed'
584
607
585 CommentsModel().create(
608 CommentsModel().create(
586 text=safe_unicode(close_msg),
609 text=safe_unicode(close_msg),
587 repo=pull_request.target_repo.repo_id,
610 repo=pull_request.target_repo.repo_id,
588 user=user.user_id,
611 user=user.user_id,
589 pull_request=pull_request.pull_request_id,
612 pull_request=pull_request.pull_request_id,
590 f_path=None,
613 f_path=None,
591 line_no=None,
614 line_no=None,
592 closing_pr=True
615 closing_pr=True
593 )
616 )
594
617
595 Session().add(pull_request)
618 Session().add(pull_request)
596 Session().flush()
619 Session().flush()
597 # TODO: paris: replace invalidation with less radical solution
620 # TODO: paris: replace invalidation with less radical solution
598 ScmModel().mark_for_invalidation(
621 ScmModel().mark_for_invalidation(
599 pull_request.target_repo.repo_name)
622 pull_request.target_repo.repo_name)
600 self._trigger_pull_request_hook(pull_request, user, 'merge')
623 self._trigger_pull_request_hook(pull_request, user, 'merge')
601
624
602 def has_valid_update_type(self, pull_request):
625 def has_valid_update_type(self, pull_request):
603 source_ref_type = pull_request.source_ref_parts.type
626 source_ref_type = pull_request.source_ref_parts.type
604 return source_ref_type in ['book', 'branch', 'tag']
627 return source_ref_type in ['book', 'branch', 'tag']
605
628
606 def update_commits(self, pull_request):
629 def update_commits(self, pull_request):
607 """
630 """
608 Get the updated list of commits for the pull request
631 Get the updated list of commits for the pull request
609 and return the new pull request version and the list
632 and return the new pull request version and the list
610 of commits processed by this update action
633 of commits processed by this update action
611 """
634 """
612 pull_request = self.__get_pull_request(pull_request)
635 pull_request = self.__get_pull_request(pull_request)
613 source_ref_type = pull_request.source_ref_parts.type
636 source_ref_type = pull_request.source_ref_parts.type
614 source_ref_name = pull_request.source_ref_parts.name
637 source_ref_name = pull_request.source_ref_parts.name
615 source_ref_id = pull_request.source_ref_parts.commit_id
638 source_ref_id = pull_request.source_ref_parts.commit_id
616
639
617 target_ref_type = pull_request.target_ref_parts.type
640 target_ref_type = pull_request.target_ref_parts.type
618 target_ref_name = pull_request.target_ref_parts.name
641 target_ref_name = pull_request.target_ref_parts.name
619 target_ref_id = pull_request.target_ref_parts.commit_id
642 target_ref_id = pull_request.target_ref_parts.commit_id
620
643
621 if not self.has_valid_update_type(pull_request):
644 if not self.has_valid_update_type(pull_request):
622 log.debug(
645 log.debug(
623 "Skipping update of pull request %s due to ref type: %s",
646 "Skipping update of pull request %s due to ref type: %s",
624 pull_request, source_ref_type)
647 pull_request, source_ref_type)
625 return UpdateResponse(
648 return UpdateResponse(
626 executed=False,
649 executed=False,
627 reason=UpdateFailureReason.WRONG_REF_TYPE,
650 reason=UpdateFailureReason.WRONG_REF_TYPE,
628 old=pull_request, new=None, changes=None,
651 old=pull_request, new=None, changes=None,
629 source_changed=False, target_changed=False)
652 source_changed=False, target_changed=False)
630
653
631 # source repo
654 # source repo
632 source_repo = pull_request.source_repo.scm_instance()
655 source_repo = pull_request.source_repo.scm_instance()
633 try:
656 try:
634 source_commit = source_repo.get_commit(commit_id=source_ref_name)
657 source_commit = source_repo.get_commit(commit_id=source_ref_name)
635 except CommitDoesNotExistError:
658 except CommitDoesNotExistError:
636 return UpdateResponse(
659 return UpdateResponse(
637 executed=False,
660 executed=False,
638 reason=UpdateFailureReason.MISSING_SOURCE_REF,
661 reason=UpdateFailureReason.MISSING_SOURCE_REF,
639 old=pull_request, new=None, changes=None,
662 old=pull_request, new=None, changes=None,
640 source_changed=False, target_changed=False)
663 source_changed=False, target_changed=False)
641
664
642 source_changed = source_ref_id != source_commit.raw_id
665 source_changed = source_ref_id != source_commit.raw_id
643
666
644 # target repo
667 # target repo
645 target_repo = pull_request.target_repo.scm_instance()
668 target_repo = pull_request.target_repo.scm_instance()
646 try:
669 try:
647 target_commit = target_repo.get_commit(commit_id=target_ref_name)
670 target_commit = target_repo.get_commit(commit_id=target_ref_name)
648 except CommitDoesNotExistError:
671 except CommitDoesNotExistError:
649 return UpdateResponse(
672 return UpdateResponse(
650 executed=False,
673 executed=False,
651 reason=UpdateFailureReason.MISSING_TARGET_REF,
674 reason=UpdateFailureReason.MISSING_TARGET_REF,
652 old=pull_request, new=None, changes=None,
675 old=pull_request, new=None, changes=None,
653 source_changed=False, target_changed=False)
676 source_changed=False, target_changed=False)
654 target_changed = target_ref_id != target_commit.raw_id
677 target_changed = target_ref_id != target_commit.raw_id
655
678
656 if not (source_changed or target_changed):
679 if not (source_changed or target_changed):
657 log.debug("Nothing changed in pull request %s", pull_request)
680 log.debug("Nothing changed in pull request %s", pull_request)
658 return UpdateResponse(
681 return UpdateResponse(
659 executed=False,
682 executed=False,
660 reason=UpdateFailureReason.NO_CHANGE,
683 reason=UpdateFailureReason.NO_CHANGE,
661 old=pull_request, new=None, changes=None,
684 old=pull_request, new=None, changes=None,
662 source_changed=target_changed, target_changed=source_changed)
685 source_changed=target_changed, target_changed=source_changed)
663
686
664 change_in_found = 'target repo' if target_changed else 'source repo'
687 change_in_found = 'target repo' if target_changed else 'source repo'
665 log.debug('Updating pull request because of change in %s detected',
688 log.debug('Updating pull request because of change in %s detected',
666 change_in_found)
689 change_in_found)
667
690
668 # Finally there is a need for an update, in case of source change
691 # Finally there is a need for an update, in case of source change
669 # we create a new version, else just an update
692 # we create a new version, else just an update
670 if source_changed:
693 if source_changed:
671 pull_request_version = self._create_version_from_snapshot(pull_request)
694 pull_request_version = self._create_version_from_snapshot(pull_request)
672 self._link_comments_to_version(pull_request_version)
695 self._link_comments_to_version(pull_request_version)
673 else:
696 else:
674 try:
697 try:
675 ver = pull_request.versions[-1]
698 ver = pull_request.versions[-1]
676 except IndexError:
699 except IndexError:
677 ver = None
700 ver = None
678
701
679 pull_request.pull_request_version_id = \
702 pull_request.pull_request_version_id = \
680 ver.pull_request_version_id if ver else None
703 ver.pull_request_version_id if ver else None
681 pull_request_version = pull_request
704 pull_request_version = pull_request
682
705
683 try:
706 try:
684 if target_ref_type in ('tag', 'branch', 'book'):
707 if target_ref_type in ('tag', 'branch', 'book'):
685 target_commit = target_repo.get_commit(target_ref_name)
708 target_commit = target_repo.get_commit(target_ref_name)
686 else:
709 else:
687 target_commit = target_repo.get_commit(target_ref_id)
710 target_commit = target_repo.get_commit(target_ref_id)
688 except CommitDoesNotExistError:
711 except CommitDoesNotExistError:
689 return UpdateResponse(
712 return UpdateResponse(
690 executed=False,
713 executed=False,
691 reason=UpdateFailureReason.MISSING_TARGET_REF,
714 reason=UpdateFailureReason.MISSING_TARGET_REF,
692 old=pull_request, new=None, changes=None,
715 old=pull_request, new=None, changes=None,
693 source_changed=source_changed, target_changed=target_changed)
716 source_changed=source_changed, target_changed=target_changed)
694
717
695 # re-compute commit ids
718 # re-compute commit ids
696 old_commit_ids = pull_request.revisions
719 old_commit_ids = pull_request.revisions
697 pre_load = ["author", "branch", "date", "message"]
720 pre_load = ["author", "branch", "date", "message"]
698 commit_ranges = target_repo.compare(
721 commit_ranges = target_repo.compare(
699 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
722 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
700 pre_load=pre_load)
723 pre_load=pre_load)
701
724
702 ancestor = target_repo.get_common_ancestor(
725 ancestor = target_repo.get_common_ancestor(
703 target_commit.raw_id, source_commit.raw_id, source_repo)
726 target_commit.raw_id, source_commit.raw_id, source_repo)
704
727
705 pull_request.source_ref = '%s:%s:%s' % (
728 pull_request.source_ref = '%s:%s:%s' % (
706 source_ref_type, source_ref_name, source_commit.raw_id)
729 source_ref_type, source_ref_name, source_commit.raw_id)
707 pull_request.target_ref = '%s:%s:%s' % (
730 pull_request.target_ref = '%s:%s:%s' % (
708 target_ref_type, target_ref_name, ancestor)
731 target_ref_type, target_ref_name, ancestor)
709
732
710 pull_request.revisions = [
733 pull_request.revisions = [
711 commit.raw_id for commit in reversed(commit_ranges)]
734 commit.raw_id for commit in reversed(commit_ranges)]
712 pull_request.updated_on = datetime.datetime.now()
735 pull_request.updated_on = datetime.datetime.now()
713 Session().add(pull_request)
736 Session().add(pull_request)
714 new_commit_ids = pull_request.revisions
737 new_commit_ids = pull_request.revisions
715
738
716 old_diff_data, new_diff_data = self._generate_update_diffs(
739 old_diff_data, new_diff_data = self._generate_update_diffs(
717 pull_request, pull_request_version)
740 pull_request, pull_request_version)
718
741
719 # calculate commit and file changes
742 # calculate commit and file changes
720 changes = self._calculate_commit_id_changes(
743 changes = self._calculate_commit_id_changes(
721 old_commit_ids, new_commit_ids)
744 old_commit_ids, new_commit_ids)
722 file_changes = self._calculate_file_changes(
745 file_changes = self._calculate_file_changes(
723 old_diff_data, new_diff_data)
746 old_diff_data, new_diff_data)
724
747
725 # set comments as outdated if DIFFS changed
748 # set comments as outdated if DIFFS changed
726 CommentsModel().outdate_comments(
749 CommentsModel().outdate_comments(
727 pull_request, old_diff_data=old_diff_data,
750 pull_request, old_diff_data=old_diff_data,
728 new_diff_data=new_diff_data)
751 new_diff_data=new_diff_data)
729
752
730 commit_changes = (changes.added or changes.removed)
753 commit_changes = (changes.added or changes.removed)
731 file_node_changes = (
754 file_node_changes = (
732 file_changes.added or file_changes.modified or file_changes.removed)
755 file_changes.added or file_changes.modified or file_changes.removed)
733 pr_has_changes = commit_changes or file_node_changes
756 pr_has_changes = commit_changes or file_node_changes
734
757
735 # Add an automatic comment to the pull request, in case
758 # Add an automatic comment to the pull request, in case
736 # anything has changed
759 # anything has changed
737 if pr_has_changes:
760 if pr_has_changes:
738 update_comment = CommentsModel().create(
761 update_comment = CommentsModel().create(
739 text=self._render_update_message(changes, file_changes),
762 text=self._render_update_message(changes, file_changes),
740 repo=pull_request.target_repo,
763 repo=pull_request.target_repo,
741 user=pull_request.author,
764 user=pull_request.author,
742 pull_request=pull_request,
765 pull_request=pull_request,
743 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
766 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
744
767
745 # Update status to "Under Review" for added commits
768 # Update status to "Under Review" for added commits
746 for commit_id in changes.added:
769 for commit_id in changes.added:
747 ChangesetStatusModel().set_status(
770 ChangesetStatusModel().set_status(
748 repo=pull_request.source_repo,
771 repo=pull_request.source_repo,
749 status=ChangesetStatus.STATUS_UNDER_REVIEW,
772 status=ChangesetStatus.STATUS_UNDER_REVIEW,
750 comment=update_comment,
773 comment=update_comment,
751 user=pull_request.author,
774 user=pull_request.author,
752 pull_request=pull_request,
775 pull_request=pull_request,
753 revision=commit_id)
776 revision=commit_id)
754
777
755 log.debug(
778 log.debug(
756 'Updated pull request %s, added_ids: %s, common_ids: %s, '
779 'Updated pull request %s, added_ids: %s, common_ids: %s, '
757 'removed_ids: %s', pull_request.pull_request_id,
780 'removed_ids: %s', pull_request.pull_request_id,
758 changes.added, changes.common, changes.removed)
781 changes.added, changes.common, changes.removed)
759 log.debug(
782 log.debug(
760 'Updated pull request with the following file changes: %s',
783 'Updated pull request with the following file changes: %s',
761 file_changes)
784 file_changes)
762
785
763 log.info(
786 log.info(
764 "Updated pull request %s from commit %s to commit %s, "
787 "Updated pull request %s from commit %s to commit %s, "
765 "stored new version %s of this pull request.",
788 "stored new version %s of this pull request.",
766 pull_request.pull_request_id, source_ref_id,
789 pull_request.pull_request_id, source_ref_id,
767 pull_request.source_ref_parts.commit_id,
790 pull_request.source_ref_parts.commit_id,
768 pull_request_version.pull_request_version_id)
791 pull_request_version.pull_request_version_id)
769 Session().commit()
792 Session().commit()
770 self._trigger_pull_request_hook(
793 self._trigger_pull_request_hook(
771 pull_request, pull_request.author, 'update')
794 pull_request, pull_request.author, 'update')
772
795
773 return UpdateResponse(
796 return UpdateResponse(
774 executed=True, reason=UpdateFailureReason.NONE,
797 executed=True, reason=UpdateFailureReason.NONE,
775 old=pull_request, new=pull_request_version, changes=changes,
798 old=pull_request, new=pull_request_version, changes=changes,
776 source_changed=source_changed, target_changed=target_changed)
799 source_changed=source_changed, target_changed=target_changed)
777
800
778 def _create_version_from_snapshot(self, pull_request):
801 def _create_version_from_snapshot(self, pull_request):
779 version = PullRequestVersion()
802 version = PullRequestVersion()
780 version.title = pull_request.title
803 version.title = pull_request.title
781 version.description = pull_request.description
804 version.description = pull_request.description
782 version.status = pull_request.status
805 version.status = pull_request.status
783 version.created_on = datetime.datetime.now()
806 version.created_on = datetime.datetime.now()
784 version.updated_on = pull_request.updated_on
807 version.updated_on = pull_request.updated_on
785 version.user_id = pull_request.user_id
808 version.user_id = pull_request.user_id
786 version.source_repo = pull_request.source_repo
809 version.source_repo = pull_request.source_repo
787 version.source_ref = pull_request.source_ref
810 version.source_ref = pull_request.source_ref
788 version.target_repo = pull_request.target_repo
811 version.target_repo = pull_request.target_repo
789 version.target_ref = pull_request.target_ref
812 version.target_ref = pull_request.target_ref
790
813
791 version._last_merge_source_rev = pull_request._last_merge_source_rev
814 version._last_merge_source_rev = pull_request._last_merge_source_rev
792 version._last_merge_target_rev = pull_request._last_merge_target_rev
815 version._last_merge_target_rev = pull_request._last_merge_target_rev
793 version.last_merge_status = pull_request.last_merge_status
816 version.last_merge_status = pull_request.last_merge_status
794 version.shadow_merge_ref = pull_request.shadow_merge_ref
817 version.shadow_merge_ref = pull_request.shadow_merge_ref
795 version.merge_rev = pull_request.merge_rev
818 version.merge_rev = pull_request.merge_rev
796 version.reviewer_data = pull_request.reviewer_data
819 version.reviewer_data = pull_request.reviewer_data
797
820
798 version.revisions = pull_request.revisions
821 version.revisions = pull_request.revisions
799 version.pull_request = pull_request
822 version.pull_request = pull_request
800 Session().add(version)
823 Session().add(version)
801 Session().flush()
824 Session().flush()
802
825
803 return version
826 return version
804
827
805 def _generate_update_diffs(self, pull_request, pull_request_version):
828 def _generate_update_diffs(self, pull_request, pull_request_version):
806
829
807 diff_context = (
830 diff_context = (
808 self.DIFF_CONTEXT +
831 self.DIFF_CONTEXT +
809 CommentsModel.needed_extra_diff_context())
832 CommentsModel.needed_extra_diff_context())
810
833
811 source_repo = pull_request_version.source_repo
834 source_repo = pull_request_version.source_repo
812 source_ref_id = pull_request_version.source_ref_parts.commit_id
835 source_ref_id = pull_request_version.source_ref_parts.commit_id
813 target_ref_id = pull_request_version.target_ref_parts.commit_id
836 target_ref_id = pull_request_version.target_ref_parts.commit_id
814 old_diff = self._get_diff_from_pr_or_version(
837 old_diff = self._get_diff_from_pr_or_version(
815 source_repo, source_ref_id, target_ref_id, context=diff_context)
838 source_repo, source_ref_id, target_ref_id, context=diff_context)
816
839
817 source_repo = pull_request.source_repo
840 source_repo = pull_request.source_repo
818 source_ref_id = pull_request.source_ref_parts.commit_id
841 source_ref_id = pull_request.source_ref_parts.commit_id
819 target_ref_id = pull_request.target_ref_parts.commit_id
842 target_ref_id = pull_request.target_ref_parts.commit_id
820
843
821 new_diff = self._get_diff_from_pr_or_version(
844 new_diff = self._get_diff_from_pr_or_version(
822 source_repo, source_ref_id, target_ref_id, context=diff_context)
845 source_repo, source_ref_id, target_ref_id, context=diff_context)
823
846
824 old_diff_data = diffs.DiffProcessor(old_diff)
847 old_diff_data = diffs.DiffProcessor(old_diff)
825 old_diff_data.prepare()
848 old_diff_data.prepare()
826 new_diff_data = diffs.DiffProcessor(new_diff)
849 new_diff_data = diffs.DiffProcessor(new_diff)
827 new_diff_data.prepare()
850 new_diff_data.prepare()
828
851
829 return old_diff_data, new_diff_data
852 return old_diff_data, new_diff_data
830
853
831 def _link_comments_to_version(self, pull_request_version):
854 def _link_comments_to_version(self, pull_request_version):
832 """
855 """
833 Link all unlinked comments of this pull request to the given version.
856 Link all unlinked comments of this pull request to the given version.
834
857
835 :param pull_request_version: The `PullRequestVersion` to which
858 :param pull_request_version: The `PullRequestVersion` to which
836 the comments shall be linked.
859 the comments shall be linked.
837
860
838 """
861 """
839 pull_request = pull_request_version.pull_request
862 pull_request = pull_request_version.pull_request
840 comments = ChangesetComment.query()\
863 comments = ChangesetComment.query()\
841 .filter(
864 .filter(
842 # TODO: johbo: Should we query for the repo at all here?
865 # TODO: johbo: Should we query for the repo at all here?
843 # Pending decision on how comments of PRs are to be related
866 # Pending decision on how comments of PRs are to be related
844 # to either the source repo, the target repo or no repo at all.
867 # to either the source repo, the target repo or no repo at all.
845 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
868 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
846 ChangesetComment.pull_request == pull_request,
869 ChangesetComment.pull_request == pull_request,
847 ChangesetComment.pull_request_version == None)\
870 ChangesetComment.pull_request_version == None)\
848 .order_by(ChangesetComment.comment_id.asc())
871 .order_by(ChangesetComment.comment_id.asc())
849
872
850 # TODO: johbo: Find out why this breaks if it is done in a bulk
873 # TODO: johbo: Find out why this breaks if it is done in a bulk
851 # operation.
874 # operation.
852 for comment in comments:
875 for comment in comments:
853 comment.pull_request_version_id = (
876 comment.pull_request_version_id = (
854 pull_request_version.pull_request_version_id)
877 pull_request_version.pull_request_version_id)
855 Session().add(comment)
878 Session().add(comment)
856
879
857 def _calculate_commit_id_changes(self, old_ids, new_ids):
880 def _calculate_commit_id_changes(self, old_ids, new_ids):
858 added = [x for x in new_ids if x not in old_ids]
881 added = [x for x in new_ids if x not in old_ids]
859 common = [x for x in new_ids if x in old_ids]
882 common = [x for x in new_ids if x in old_ids]
860 removed = [x for x in old_ids if x not in new_ids]
883 removed = [x for x in old_ids if x not in new_ids]
861 total = new_ids
884 total = new_ids
862 return ChangeTuple(added, common, removed, total)
885 return ChangeTuple(added, common, removed, total)
863
886
864 def _calculate_file_changes(self, old_diff_data, new_diff_data):
887 def _calculate_file_changes(self, old_diff_data, new_diff_data):
865
888
866 old_files = OrderedDict()
889 old_files = OrderedDict()
867 for diff_data in old_diff_data.parsed_diff:
890 for diff_data in old_diff_data.parsed_diff:
868 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
891 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
869
892
870 added_files = []
893 added_files = []
871 modified_files = []
894 modified_files = []
872 removed_files = []
895 removed_files = []
873 for diff_data in new_diff_data.parsed_diff:
896 for diff_data in new_diff_data.parsed_diff:
874 new_filename = diff_data['filename']
897 new_filename = diff_data['filename']
875 new_hash = md5_safe(diff_data['raw_diff'])
898 new_hash = md5_safe(diff_data['raw_diff'])
876
899
877 old_hash = old_files.get(new_filename)
900 old_hash = old_files.get(new_filename)
878 if not old_hash:
901 if not old_hash:
879 # file is not present in old diff, means it's added
902 # file is not present in old diff, means it's added
880 added_files.append(new_filename)
903 added_files.append(new_filename)
881 else:
904 else:
882 if new_hash != old_hash:
905 if new_hash != old_hash:
883 modified_files.append(new_filename)
906 modified_files.append(new_filename)
884 # now remove a file from old, since we have seen it already
907 # now remove a file from old, since we have seen it already
885 del old_files[new_filename]
908 del old_files[new_filename]
886
909
887 # removed files is when there are present in old, but not in NEW,
910 # removed files is when there are present in old, but not in NEW,
888 # since we remove old files that are present in new diff, left-overs
911 # since we remove old files that are present in new diff, left-overs
889 # if any should be the removed files
912 # if any should be the removed files
890 removed_files.extend(old_files.keys())
913 removed_files.extend(old_files.keys())
891
914
892 return FileChangeTuple(added_files, modified_files, removed_files)
915 return FileChangeTuple(added_files, modified_files, removed_files)
893
916
894 def _render_update_message(self, changes, file_changes):
917 def _render_update_message(self, changes, file_changes):
895 """
918 """
896 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
919 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
897 so it's always looking the same disregarding on which default
920 so it's always looking the same disregarding on which default
898 renderer system is using.
921 renderer system is using.
899
922
900 :param changes: changes named tuple
923 :param changes: changes named tuple
901 :param file_changes: file changes named tuple
924 :param file_changes: file changes named tuple
902
925
903 """
926 """
904 new_status = ChangesetStatus.get_status_lbl(
927 new_status = ChangesetStatus.get_status_lbl(
905 ChangesetStatus.STATUS_UNDER_REVIEW)
928 ChangesetStatus.STATUS_UNDER_REVIEW)
906
929
907 changed_files = (
930 changed_files = (
908 file_changes.added + file_changes.modified + file_changes.removed)
931 file_changes.added + file_changes.modified + file_changes.removed)
909
932
910 params = {
933 params = {
911 'under_review_label': new_status,
934 'under_review_label': new_status,
912 'added_commits': changes.added,
935 'added_commits': changes.added,
913 'removed_commits': changes.removed,
936 'removed_commits': changes.removed,
914 'changed_files': changed_files,
937 'changed_files': changed_files,
915 'added_files': file_changes.added,
938 'added_files': file_changes.added,
916 'modified_files': file_changes.modified,
939 'modified_files': file_changes.modified,
917 'removed_files': file_changes.removed,
940 'removed_files': file_changes.removed,
918 }
941 }
919 renderer = RstTemplateRenderer()
942 renderer = RstTemplateRenderer()
920 return renderer.render('pull_request_update.mako', **params)
943 return renderer.render('pull_request_update.mako', **params)
921
944
922 def edit(self, pull_request, title, description, user):
945 def edit(self, pull_request, title, description, user):
923 pull_request = self.__get_pull_request(pull_request)
946 pull_request = self.__get_pull_request(pull_request)
924 old_data = pull_request.get_api_data(with_merge_state=False)
947 old_data = pull_request.get_api_data(with_merge_state=False)
925 if pull_request.is_closed():
948 if pull_request.is_closed():
926 raise ValueError('This pull request is closed')
949 raise ValueError('This pull request is closed')
927 if title:
950 if title:
928 pull_request.title = title
951 pull_request.title = title
929 pull_request.description = description
952 pull_request.description = description
930 pull_request.updated_on = datetime.datetime.now()
953 pull_request.updated_on = datetime.datetime.now()
931 Session().add(pull_request)
954 Session().add(pull_request)
932 self._log_audit_action(
955 self._log_audit_action(
933 'repo.pull_request.edit', {'old_data': old_data},
956 'repo.pull_request.edit', {'old_data': old_data},
934 user, pull_request)
957 user, pull_request)
935
958
936 def update_reviewers(self, pull_request, reviewer_data, user):
959 def update_reviewers(self, pull_request, reviewer_data, user):
937 """
960 """
938 Update the reviewers in the pull request
961 Update the reviewers in the pull request
939
962
940 :param pull_request: the pr to update
963 :param pull_request: the pr to update
941 :param reviewer_data: list of tuples
964 :param reviewer_data: list of tuples
942 [(user, ['reason1', 'reason2'], mandatory_flag)]
965 [(user, ['reason1', 'reason2'], mandatory_flag)]
943 """
966 """
944 pull_request = self.__get_pull_request(pull_request)
967 pull_request = self.__get_pull_request(pull_request)
945 if pull_request.is_closed():
968 if pull_request.is_closed():
946 raise ValueError('This pull request is closed')
969 raise ValueError('This pull request is closed')
947
970
948 reviewers = {}
971 reviewers = {}
949 for user_id, reasons, mandatory in reviewer_data:
972 for user_id, reasons, mandatory in reviewer_data:
950 if isinstance(user_id, (int, basestring)):
973 if isinstance(user_id, (int, basestring)):
951 user_id = self._get_user(user_id).user_id
974 user_id = self._get_user(user_id).user_id
952 reviewers[user_id] = {
975 reviewers[user_id] = {
953 'reasons': reasons, 'mandatory': mandatory}
976 'reasons': reasons, 'mandatory': mandatory}
954
977
955 reviewers_ids = set(reviewers.keys())
978 reviewers_ids = set(reviewers.keys())
956 current_reviewers = PullRequestReviewers.query()\
979 current_reviewers = PullRequestReviewers.query()\
957 .filter(PullRequestReviewers.pull_request ==
980 .filter(PullRequestReviewers.pull_request ==
958 pull_request).all()
981 pull_request).all()
959 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
982 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
960
983
961 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
984 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
962 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
985 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
963
986
964 log.debug("Adding %s reviewers", ids_to_add)
987 log.debug("Adding %s reviewers", ids_to_add)
965 log.debug("Removing %s reviewers", ids_to_remove)
988 log.debug("Removing %s reviewers", ids_to_remove)
966 changed = False
989 changed = False
967 for uid in ids_to_add:
990 for uid in ids_to_add:
968 changed = True
991 changed = True
969 _usr = self._get_user(uid)
992 _usr = self._get_user(uid)
970 reviewer = PullRequestReviewers()
993 reviewer = PullRequestReviewers()
971 reviewer.user = _usr
994 reviewer.user = _usr
972 reviewer.pull_request = pull_request
995 reviewer.pull_request = pull_request
973 reviewer.reasons = reviewers[uid]['reasons']
996 reviewer.reasons = reviewers[uid]['reasons']
974 # NOTE(marcink): mandatory shouldn't be changed now
997 # NOTE(marcink): mandatory shouldn't be changed now
975 # reviewer.mandatory = reviewers[uid]['reasons']
998 # reviewer.mandatory = reviewers[uid]['reasons']
976 Session().add(reviewer)
999 Session().add(reviewer)
977 self._log_audit_action(
1000 self._log_audit_action(
978 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1001 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
979 user, pull_request)
1002 user, pull_request)
980
1003
981 for uid in ids_to_remove:
1004 for uid in ids_to_remove:
982 changed = True
1005 changed = True
983 reviewers = PullRequestReviewers.query()\
1006 reviewers = PullRequestReviewers.query()\
984 .filter(PullRequestReviewers.user_id == uid,
1007 .filter(PullRequestReviewers.user_id == uid,
985 PullRequestReviewers.pull_request == pull_request)\
1008 PullRequestReviewers.pull_request == pull_request)\
986 .all()
1009 .all()
987 # use .all() in case we accidentally added the same person twice
1010 # use .all() in case we accidentally added the same person twice
988 # this CAN happen due to the lack of DB checks
1011 # this CAN happen due to the lack of DB checks
989 for obj in reviewers:
1012 for obj in reviewers:
990 old_data = obj.get_dict()
1013 old_data = obj.get_dict()
991 Session().delete(obj)
1014 Session().delete(obj)
992 self._log_audit_action(
1015 self._log_audit_action(
993 'repo.pull_request.reviewer.delete',
1016 'repo.pull_request.reviewer.delete',
994 {'old_data': old_data}, user, pull_request)
1017 {'old_data': old_data}, user, pull_request)
995
1018
996 if changed:
1019 if changed:
997 pull_request.updated_on = datetime.datetime.now()
1020 pull_request.updated_on = datetime.datetime.now()
998 Session().add(pull_request)
1021 Session().add(pull_request)
999
1022
1000 self.notify_reviewers(pull_request, ids_to_add)
1023 self.notify_reviewers(pull_request, ids_to_add)
1001 return ids_to_add, ids_to_remove
1024 return ids_to_add, ids_to_remove
1002
1025
1003 def get_url(self, pull_request, request=None, permalink=False):
1026 def get_url(self, pull_request, request=None, permalink=False):
1004 if not request:
1027 if not request:
1005 request = get_current_request()
1028 request = get_current_request()
1006
1029
1007 if permalink:
1030 if permalink:
1008 return request.route_url(
1031 return request.route_url(
1009 'pull_requests_global',
1032 'pull_requests_global',
1010 pull_request_id=pull_request.pull_request_id,)
1033 pull_request_id=pull_request.pull_request_id,)
1011 else:
1034 else:
1012 return request.route_url('pullrequest_show',
1035 return request.route_url('pullrequest_show',
1013 repo_name=safe_str(pull_request.target_repo.repo_name),
1036 repo_name=safe_str(pull_request.target_repo.repo_name),
1014 pull_request_id=pull_request.pull_request_id,)
1037 pull_request_id=pull_request.pull_request_id,)
1015
1038
1016 def get_shadow_clone_url(self, pull_request):
1039 def get_shadow_clone_url(self, pull_request):
1017 """
1040 """
1018 Returns qualified url pointing to the shadow repository. If this pull
1041 Returns qualified url pointing to the shadow repository. If this pull
1019 request is closed there is no shadow repository and ``None`` will be
1042 request is closed there is no shadow repository and ``None`` will be
1020 returned.
1043 returned.
1021 """
1044 """
1022 if pull_request.is_closed():
1045 if pull_request.is_closed():
1023 return None
1046 return None
1024 else:
1047 else:
1025 pr_url = urllib.unquote(self.get_url(pull_request))
1048 pr_url = urllib.unquote(self.get_url(pull_request))
1026 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1049 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1027
1050
1028 def notify_reviewers(self, pull_request, reviewers_ids):
1051 def notify_reviewers(self, pull_request, reviewers_ids):
1029 # notification to reviewers
1052 # notification to reviewers
1030 if not reviewers_ids:
1053 if not reviewers_ids:
1031 return
1054 return
1032
1055
1033 pull_request_obj = pull_request
1056 pull_request_obj = pull_request
1034 # get the current participants of this pull request
1057 # get the current participants of this pull request
1035 recipients = reviewers_ids
1058 recipients = reviewers_ids
1036 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1059 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1037
1060
1038 pr_source_repo = pull_request_obj.source_repo
1061 pr_source_repo = pull_request_obj.source_repo
1039 pr_target_repo = pull_request_obj.target_repo
1062 pr_target_repo = pull_request_obj.target_repo
1040
1063
1041 pr_url = h.route_url('pullrequest_show',
1064 pr_url = h.route_url('pullrequest_show',
1042 repo_name=pr_target_repo.repo_name,
1065 repo_name=pr_target_repo.repo_name,
1043 pull_request_id=pull_request_obj.pull_request_id,)
1066 pull_request_id=pull_request_obj.pull_request_id,)
1044
1067
1045 # set some variables for email notification
1068 # set some variables for email notification
1046 pr_target_repo_url = h.route_url(
1069 pr_target_repo_url = h.route_url(
1047 'repo_summary', repo_name=pr_target_repo.repo_name)
1070 'repo_summary', repo_name=pr_target_repo.repo_name)
1048
1071
1049 pr_source_repo_url = h.route_url(
1072 pr_source_repo_url = h.route_url(
1050 'repo_summary', repo_name=pr_source_repo.repo_name)
1073 'repo_summary', repo_name=pr_source_repo.repo_name)
1051
1074
1052 # pull request specifics
1075 # pull request specifics
1053 pull_request_commits = [
1076 pull_request_commits = [
1054 (x.raw_id, x.message)
1077 (x.raw_id, x.message)
1055 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1078 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1056
1079
1057 kwargs = {
1080 kwargs = {
1058 'user': pull_request.author,
1081 'user': pull_request.author,
1059 'pull_request': pull_request_obj,
1082 'pull_request': pull_request_obj,
1060 'pull_request_commits': pull_request_commits,
1083 'pull_request_commits': pull_request_commits,
1061
1084
1062 'pull_request_target_repo': pr_target_repo,
1085 'pull_request_target_repo': pr_target_repo,
1063 'pull_request_target_repo_url': pr_target_repo_url,
1086 'pull_request_target_repo_url': pr_target_repo_url,
1064
1087
1065 'pull_request_source_repo': pr_source_repo,
1088 'pull_request_source_repo': pr_source_repo,
1066 'pull_request_source_repo_url': pr_source_repo_url,
1089 'pull_request_source_repo_url': pr_source_repo_url,
1067
1090
1068 'pull_request_url': pr_url,
1091 'pull_request_url': pr_url,
1069 }
1092 }
1070
1093
1071 # pre-generate the subject for notification itself
1094 # pre-generate the subject for notification itself
1072 (subject,
1095 (subject,
1073 _h, _e, # we don't care about those
1096 _h, _e, # we don't care about those
1074 body_plaintext) = EmailNotificationModel().render_email(
1097 body_plaintext) = EmailNotificationModel().render_email(
1075 notification_type, **kwargs)
1098 notification_type, **kwargs)
1076
1099
1077 # create notification objects, and emails
1100 # create notification objects, and emails
1078 NotificationModel().create(
1101 NotificationModel().create(
1079 created_by=pull_request.author,
1102 created_by=pull_request.author,
1080 notification_subject=subject,
1103 notification_subject=subject,
1081 notification_body=body_plaintext,
1104 notification_body=body_plaintext,
1082 notification_type=notification_type,
1105 notification_type=notification_type,
1083 recipients=recipients,
1106 recipients=recipients,
1084 email_kwargs=kwargs,
1107 email_kwargs=kwargs,
1085 )
1108 )
1086
1109
1087 def delete(self, pull_request, user):
1110 def delete(self, pull_request, user):
1088 pull_request = self.__get_pull_request(pull_request)
1111 pull_request = self.__get_pull_request(pull_request)
1089 old_data = pull_request.get_api_data(with_merge_state=False)
1112 old_data = pull_request.get_api_data(with_merge_state=False)
1090 self._cleanup_merge_workspace(pull_request)
1113 self._cleanup_merge_workspace(pull_request)
1091 self._log_audit_action(
1114 self._log_audit_action(
1092 'repo.pull_request.delete', {'old_data': old_data},
1115 'repo.pull_request.delete', {'old_data': old_data},
1093 user, pull_request)
1116 user, pull_request)
1094 Session().delete(pull_request)
1117 Session().delete(pull_request)
1095
1118
1096 def close_pull_request(self, pull_request, user):
1119 def close_pull_request(self, pull_request, user):
1097 pull_request = self.__get_pull_request(pull_request)
1120 pull_request = self.__get_pull_request(pull_request)
1098 self._cleanup_merge_workspace(pull_request)
1121 self._cleanup_merge_workspace(pull_request)
1099 pull_request.status = PullRequest.STATUS_CLOSED
1122 pull_request.status = PullRequest.STATUS_CLOSED
1100 pull_request.updated_on = datetime.datetime.now()
1123 pull_request.updated_on = datetime.datetime.now()
1101 Session().add(pull_request)
1124 Session().add(pull_request)
1102 self._trigger_pull_request_hook(
1125 self._trigger_pull_request_hook(
1103 pull_request, pull_request.author, 'close')
1126 pull_request, pull_request.author, 'close')
1104
1127
1105 pr_data = pull_request.get_api_data(with_merge_state=False)
1128 pr_data = pull_request.get_api_data(with_merge_state=False)
1106 self._log_audit_action(
1129 self._log_audit_action(
1107 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1130 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1108
1131
1109 def close_pull_request_with_comment(
1132 def close_pull_request_with_comment(
1110 self, pull_request, user, repo, message=None):
1133 self, pull_request, user, repo, message=None):
1111
1134
1112 pull_request_review_status = pull_request.calculated_review_status()
1135 pull_request_review_status = pull_request.calculated_review_status()
1113
1136
1114 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1137 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1115 # approved only if we have voting consent
1138 # approved only if we have voting consent
1116 status = ChangesetStatus.STATUS_APPROVED
1139 status = ChangesetStatus.STATUS_APPROVED
1117 else:
1140 else:
1118 status = ChangesetStatus.STATUS_REJECTED
1141 status = ChangesetStatus.STATUS_REJECTED
1119 status_lbl = ChangesetStatus.get_status_lbl(status)
1142 status_lbl = ChangesetStatus.get_status_lbl(status)
1120
1143
1121 default_message = (
1144 default_message = (
1122 'Closing with status change {transition_icon} {status}.'
1145 'Closing with status change {transition_icon} {status}.'
1123 ).format(transition_icon='>', status=status_lbl)
1146 ).format(transition_icon='>', status=status_lbl)
1124 text = message or default_message
1147 text = message or default_message
1125
1148
1126 # create a comment, and link it to new status
1149 # create a comment, and link it to new status
1127 comment = CommentsModel().create(
1150 comment = CommentsModel().create(
1128 text=text,
1151 text=text,
1129 repo=repo.repo_id,
1152 repo=repo.repo_id,
1130 user=user.user_id,
1153 user=user.user_id,
1131 pull_request=pull_request.pull_request_id,
1154 pull_request=pull_request.pull_request_id,
1132 status_change=status_lbl,
1155 status_change=status_lbl,
1133 status_change_type=status,
1156 status_change_type=status,
1134 closing_pr=True
1157 closing_pr=True
1135 )
1158 )
1136
1159
1137 # calculate old status before we change it
1160 # calculate old status before we change it
1138 old_calculated_status = pull_request.calculated_review_status()
1161 old_calculated_status = pull_request.calculated_review_status()
1139 ChangesetStatusModel().set_status(
1162 ChangesetStatusModel().set_status(
1140 repo.repo_id,
1163 repo.repo_id,
1141 status,
1164 status,
1142 user.user_id,
1165 user.user_id,
1143 comment=comment,
1166 comment=comment,
1144 pull_request=pull_request.pull_request_id
1167 pull_request=pull_request.pull_request_id
1145 )
1168 )
1146
1169
1147 Session().flush()
1170 Session().flush()
1148 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1171 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1149 # we now calculate the status of pull request again, and based on that
1172 # we now calculate the status of pull request again, and based on that
1150 # calculation trigger status change. This might happen in cases
1173 # calculation trigger status change. This might happen in cases
1151 # that non-reviewer admin closes a pr, which means his vote doesn't
1174 # that non-reviewer admin closes a pr, which means his vote doesn't
1152 # change the status, while if he's a reviewer this might change it.
1175 # change the status, while if he's a reviewer this might change it.
1153 calculated_status = pull_request.calculated_review_status()
1176 calculated_status = pull_request.calculated_review_status()
1154 if old_calculated_status != calculated_status:
1177 if old_calculated_status != calculated_status:
1155 self._trigger_pull_request_hook(
1178 self._trigger_pull_request_hook(
1156 pull_request, user, 'review_status_change')
1179 pull_request, user, 'review_status_change')
1157
1180
1158 # finally close the PR
1181 # finally close the PR
1159 PullRequestModel().close_pull_request(
1182 PullRequestModel().close_pull_request(
1160 pull_request.pull_request_id, user)
1183 pull_request.pull_request_id, user)
1161
1184
1162 return comment, status
1185 return comment, status
1163
1186
1164 def merge_status(self, pull_request, translator=None):
1187 def merge_status(self, pull_request, translator=None):
1165 _ = translator or get_current_request().translate
1188 _ = translator or get_current_request().translate
1166
1189
1167 if not self._is_merge_enabled(pull_request):
1190 if not self._is_merge_enabled(pull_request):
1168 return False, _('Server-side pull request merging is disabled.')
1191 return False, _('Server-side pull request merging is disabled.')
1169 if pull_request.is_closed():
1192 if pull_request.is_closed():
1170 return False, _('This pull request is closed.')
1193 return False, _('This pull request is closed.')
1171 merge_possible, msg = self._check_repo_requirements(
1194 merge_possible, msg = self._check_repo_requirements(
1172 target=pull_request.target_repo, source=pull_request.source_repo,
1195 target=pull_request.target_repo, source=pull_request.source_repo,
1173 translator=_)
1196 translator=_)
1174 if not merge_possible:
1197 if not merge_possible:
1175 return merge_possible, msg
1198 return merge_possible, msg
1176
1199
1177 try:
1200 try:
1178 resp = self._try_merge(pull_request)
1201 resp = self._try_merge(pull_request)
1179 log.debug("Merge response: %s", resp)
1202 log.debug("Merge response: %s", resp)
1180 status = resp.possible, self.merge_status_message(
1203 status = resp.possible, self.merge_status_message(
1181 resp.failure_reason)
1204 resp.failure_reason)
1182 except NotImplementedError:
1205 except NotImplementedError:
1183 status = False, _('Pull request merging is not supported.')
1206 status = False, _('Pull request merging is not supported.')
1184
1207
1185 return status
1208 return status
1186
1209
1187 def _check_repo_requirements(self, target, source, translator):
1210 def _check_repo_requirements(self, target, source, translator):
1188 """
1211 """
1189 Check if `target` and `source` have compatible requirements.
1212 Check if `target` and `source` have compatible requirements.
1190
1213
1191 Currently this is just checking for largefiles.
1214 Currently this is just checking for largefiles.
1192 """
1215 """
1193 _ = translator
1216 _ = translator
1194 target_has_largefiles = self._has_largefiles(target)
1217 target_has_largefiles = self._has_largefiles(target)
1195 source_has_largefiles = self._has_largefiles(source)
1218 source_has_largefiles = self._has_largefiles(source)
1196 merge_possible = True
1219 merge_possible = True
1197 message = u''
1220 message = u''
1198
1221
1199 if target_has_largefiles != source_has_largefiles:
1222 if target_has_largefiles != source_has_largefiles:
1200 merge_possible = False
1223 merge_possible = False
1201 if source_has_largefiles:
1224 if source_has_largefiles:
1202 message = _(
1225 message = _(
1203 'Target repository large files support is disabled.')
1226 'Target repository large files support is disabled.')
1204 else:
1227 else:
1205 message = _(
1228 message = _(
1206 'Source repository large files support is disabled.')
1229 'Source repository large files support is disabled.')
1207
1230
1208 return merge_possible, message
1231 return merge_possible, message
1209
1232
1210 def _has_largefiles(self, repo):
1233 def _has_largefiles(self, repo):
1211 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1234 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1212 'extensions', 'largefiles')
1235 'extensions', 'largefiles')
1213 return largefiles_ui and largefiles_ui[0].active
1236 return largefiles_ui and largefiles_ui[0].active
1214
1237
1215 def _try_merge(self, pull_request):
1238 def _try_merge(self, pull_request):
1216 """
1239 """
1217 Try to merge the pull request and return the merge status.
1240 Try to merge the pull request and return the merge status.
1218 """
1241 """
1219 log.debug(
1242 log.debug(
1220 "Trying out if the pull request %s can be merged.",
1243 "Trying out if the pull request %s can be merged.",
1221 pull_request.pull_request_id)
1244 pull_request.pull_request_id)
1222 target_vcs = pull_request.target_repo.scm_instance()
1245 target_vcs = pull_request.target_repo.scm_instance()
1223
1246
1224 # Refresh the target reference.
1247 # Refresh the target reference.
1225 try:
1248 try:
1226 target_ref = self._refresh_reference(
1249 target_ref = self._refresh_reference(
1227 pull_request.target_ref_parts, target_vcs)
1250 pull_request.target_ref_parts, target_vcs)
1228 except CommitDoesNotExistError:
1251 except CommitDoesNotExistError:
1229 merge_state = MergeResponse(
1252 merge_state = MergeResponse(
1230 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1253 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1231 return merge_state
1254 return merge_state
1232
1255
1233 target_locked = pull_request.target_repo.locked
1256 target_locked = pull_request.target_repo.locked
1234 if target_locked and target_locked[0]:
1257 if target_locked and target_locked[0]:
1235 log.debug("The target repository is locked.")
1258 log.debug("The target repository is locked.")
1236 merge_state = MergeResponse(
1259 merge_state = MergeResponse(
1237 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1260 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1238 elif self._needs_merge_state_refresh(pull_request, target_ref):
1261 elif self._needs_merge_state_refresh(pull_request, target_ref):
1239 log.debug("Refreshing the merge status of the repository.")
1262 log.debug("Refreshing the merge status of the repository.")
1240 merge_state = self._refresh_merge_state(
1263 merge_state = self._refresh_merge_state(
1241 pull_request, target_vcs, target_ref)
1264 pull_request, target_vcs, target_ref)
1242 else:
1265 else:
1243 possible = pull_request.\
1266 possible = pull_request.\
1244 last_merge_status == MergeFailureReason.NONE
1267 last_merge_status == MergeFailureReason.NONE
1245 merge_state = MergeResponse(
1268 merge_state = MergeResponse(
1246 possible, False, None, pull_request.last_merge_status)
1269 possible, False, None, pull_request.last_merge_status)
1247
1270
1248 return merge_state
1271 return merge_state
1249
1272
1250 def _refresh_reference(self, reference, vcs_repository):
1273 def _refresh_reference(self, reference, vcs_repository):
1251 if reference.type in ('branch', 'book'):
1274 if reference.type in ('branch', 'book'):
1252 name_or_id = reference.name
1275 name_or_id = reference.name
1253 else:
1276 else:
1254 name_or_id = reference.commit_id
1277 name_or_id = reference.commit_id
1255 refreshed_commit = vcs_repository.get_commit(name_or_id)
1278 refreshed_commit = vcs_repository.get_commit(name_or_id)
1256 refreshed_reference = Reference(
1279 refreshed_reference = Reference(
1257 reference.type, reference.name, refreshed_commit.raw_id)
1280 reference.type, reference.name, refreshed_commit.raw_id)
1258 return refreshed_reference
1281 return refreshed_reference
1259
1282
1260 def _needs_merge_state_refresh(self, pull_request, target_reference):
1283 def _needs_merge_state_refresh(self, pull_request, target_reference):
1261 return not(
1284 return not(
1262 pull_request.revisions and
1285 pull_request.revisions and
1263 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1286 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1264 target_reference.commit_id == pull_request._last_merge_target_rev)
1287 target_reference.commit_id == pull_request._last_merge_target_rev)
1265
1288
1266 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1289 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1267 workspace_id = self._workspace_id(pull_request)
1290 workspace_id = self._workspace_id(pull_request)
1268 source_vcs = pull_request.source_repo.scm_instance()
1291 source_vcs = pull_request.source_repo.scm_instance()
1269 use_rebase = self._use_rebase_for_merging(pull_request)
1292 use_rebase = self._use_rebase_for_merging(pull_request)
1270 close_branch = self._close_branch_before_merging(pull_request)
1293 close_branch = self._close_branch_before_merging(pull_request)
1271 merge_state = target_vcs.merge(
1294 merge_state = target_vcs.merge(
1272 target_reference, source_vcs, pull_request.source_ref_parts,
1295 target_reference, source_vcs, pull_request.source_ref_parts,
1273 workspace_id, dry_run=True, use_rebase=use_rebase,
1296 workspace_id, dry_run=True, use_rebase=use_rebase,
1274 close_branch=close_branch)
1297 close_branch=close_branch)
1275
1298
1276 # Do not store the response if there was an unknown error.
1299 # Do not store the response if there was an unknown error.
1277 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1300 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1278 pull_request._last_merge_source_rev = \
1301 pull_request._last_merge_source_rev = \
1279 pull_request.source_ref_parts.commit_id
1302 pull_request.source_ref_parts.commit_id
1280 pull_request._last_merge_target_rev = target_reference.commit_id
1303 pull_request._last_merge_target_rev = target_reference.commit_id
1281 pull_request.last_merge_status = merge_state.failure_reason
1304 pull_request.last_merge_status = merge_state.failure_reason
1282 pull_request.shadow_merge_ref = merge_state.merge_ref
1305 pull_request.shadow_merge_ref = merge_state.merge_ref
1283 Session().add(pull_request)
1306 Session().add(pull_request)
1284 Session().commit()
1307 Session().commit()
1285
1308
1286 return merge_state
1309 return merge_state
1287
1310
1288 def _workspace_id(self, pull_request):
1311 def _workspace_id(self, pull_request):
1289 workspace_id = 'pr-%s' % pull_request.pull_request_id
1312 workspace_id = 'pr-%s' % pull_request.pull_request_id
1290 return workspace_id
1313 return workspace_id
1291
1314
1292 def merge_status_message(self, status_code):
1315 def merge_status_message(self, status_code):
1293 """
1316 """
1294 Return a human friendly error message for the given merge status code.
1317 Return a human friendly error message for the given merge status code.
1295 """
1318 """
1296 return self.MERGE_STATUS_MESSAGES[status_code]
1319 return self.MERGE_STATUS_MESSAGES[status_code]
1297
1320
1298 def generate_repo_data(self, repo, commit_id=None, branch=None,
1321 def generate_repo_data(self, repo, commit_id=None, branch=None,
1299 bookmark=None, translator=None):
1322 bookmark=None, translator=None):
1300
1323
1301 all_refs, selected_ref = \
1324 all_refs, selected_ref = \
1302 self._get_repo_pullrequest_sources(
1325 self._get_repo_pullrequest_sources(
1303 repo.scm_instance(), commit_id=commit_id,
1326 repo.scm_instance(), commit_id=commit_id,
1304 branch=branch, bookmark=bookmark, translator=translator)
1327 branch=branch, bookmark=bookmark, translator=translator)
1305
1328
1306 refs_select2 = []
1329 refs_select2 = []
1307 for element in all_refs:
1330 for element in all_refs:
1308 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1331 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1309 refs_select2.append({'text': element[1], 'children': children})
1332 refs_select2.append({'text': element[1], 'children': children})
1310
1333
1311 return {
1334 return {
1312 'user': {
1335 'user': {
1313 'user_id': repo.user.user_id,
1336 'user_id': repo.user.user_id,
1314 'username': repo.user.username,
1337 'username': repo.user.username,
1315 'firstname': repo.user.first_name,
1338 'firstname': repo.user.first_name,
1316 'lastname': repo.user.last_name,
1339 'lastname': repo.user.last_name,
1317 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1340 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1318 },
1341 },
1319 'description': h.chop_at_smart(repo.description_safe, '\n'),
1342 'description': h.chop_at_smart(repo.description_safe, '\n'),
1320 'refs': {
1343 'refs': {
1321 'all_refs': all_refs,
1344 'all_refs': all_refs,
1322 'selected_ref': selected_ref,
1345 'selected_ref': selected_ref,
1323 'select2_refs': refs_select2
1346 'select2_refs': refs_select2
1324 }
1347 }
1325 }
1348 }
1326
1349
1327 def generate_pullrequest_title(self, source, source_ref, target):
1350 def generate_pullrequest_title(self, source, source_ref, target):
1328 return u'{source}#{at_ref} to {target}'.format(
1351 return u'{source}#{at_ref} to {target}'.format(
1329 source=source,
1352 source=source,
1330 at_ref=source_ref,
1353 at_ref=source_ref,
1331 target=target,
1354 target=target,
1332 )
1355 )
1333
1356
1334 def _cleanup_merge_workspace(self, pull_request):
1357 def _cleanup_merge_workspace(self, pull_request):
1335 # Merging related cleanup
1358 # Merging related cleanup
1336 target_scm = pull_request.target_repo.scm_instance()
1359 target_scm = pull_request.target_repo.scm_instance()
1337 workspace_id = 'pr-%s' % pull_request.pull_request_id
1360 workspace_id = 'pr-%s' % pull_request.pull_request_id
1338
1361
1339 try:
1362 try:
1340 target_scm.cleanup_merge_workspace(workspace_id)
1363 target_scm.cleanup_merge_workspace(workspace_id)
1341 except NotImplementedError:
1364 except NotImplementedError:
1342 pass
1365 pass
1343
1366
1344 def _get_repo_pullrequest_sources(
1367 def _get_repo_pullrequest_sources(
1345 self, repo, commit_id=None, branch=None, bookmark=None,
1368 self, repo, commit_id=None, branch=None, bookmark=None,
1346 translator=None):
1369 translator=None):
1347 """
1370 """
1348 Return a structure with repo's interesting commits, suitable for
1371 Return a structure with repo's interesting commits, suitable for
1349 the selectors in pullrequest controller
1372 the selectors in pullrequest controller
1350
1373
1351 :param commit_id: a commit that must be in the list somehow
1374 :param commit_id: a commit that must be in the list somehow
1352 and selected by default
1375 and selected by default
1353 :param branch: a branch that must be in the list and selected
1376 :param branch: a branch that must be in the list and selected
1354 by default - even if closed
1377 by default - even if closed
1355 :param bookmark: a bookmark that must be in the list and selected
1378 :param bookmark: a bookmark that must be in the list and selected
1356 """
1379 """
1357 _ = translator or get_current_request().translate
1380 _ = translator or get_current_request().translate
1358
1381
1359 commit_id = safe_str(commit_id) if commit_id else None
1382 commit_id = safe_str(commit_id) if commit_id else None
1360 branch = safe_str(branch) if branch else None
1383 branch = safe_str(branch) if branch else None
1361 bookmark = safe_str(bookmark) if bookmark else None
1384 bookmark = safe_str(bookmark) if bookmark else None
1362
1385
1363 selected = None
1386 selected = None
1364
1387
1365 # order matters: first source that has commit_id in it will be selected
1388 # order matters: first source that has commit_id in it will be selected
1366 sources = []
1389 sources = []
1367 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1390 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1368 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1391 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1369
1392
1370 if commit_id:
1393 if commit_id:
1371 ref_commit = (h.short_id(commit_id), commit_id)
1394 ref_commit = (h.short_id(commit_id), commit_id)
1372 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1395 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1373
1396
1374 sources.append(
1397 sources.append(
1375 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1398 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1376 )
1399 )
1377
1400
1378 groups = []
1401 groups = []
1379 for group_key, ref_list, group_name, match in sources:
1402 for group_key, ref_list, group_name, match in sources:
1380 group_refs = []
1403 group_refs = []
1381 for ref_name, ref_id in ref_list:
1404 for ref_name, ref_id in ref_list:
1382 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1405 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1383 group_refs.append((ref_key, ref_name))
1406 group_refs.append((ref_key, ref_name))
1384
1407
1385 if not selected:
1408 if not selected:
1386 if set([commit_id, match]) & set([ref_id, ref_name]):
1409 if set([commit_id, match]) & set([ref_id, ref_name]):
1387 selected = ref_key
1410 selected = ref_key
1388
1411
1389 if group_refs:
1412 if group_refs:
1390 groups.append((group_refs, group_name))
1413 groups.append((group_refs, group_name))
1391
1414
1392 if not selected:
1415 if not selected:
1393 ref = commit_id or branch or bookmark
1416 ref = commit_id or branch or bookmark
1394 if ref:
1417 if ref:
1395 raise CommitDoesNotExistError(
1418 raise CommitDoesNotExistError(
1396 'No commit refs could be found matching: %s' % ref)
1419 'No commit refs could be found matching: %s' % ref)
1397 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1420 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1398 selected = 'branch:%s:%s' % (
1421 selected = 'branch:%s:%s' % (
1399 repo.DEFAULT_BRANCH_NAME,
1422 repo.DEFAULT_BRANCH_NAME,
1400 repo.branches[repo.DEFAULT_BRANCH_NAME]
1423 repo.branches[repo.DEFAULT_BRANCH_NAME]
1401 )
1424 )
1402 elif repo.commit_ids:
1425 elif repo.commit_ids:
1403 rev = repo.commit_ids[0]
1426 rev = repo.commit_ids[0]
1404 selected = 'rev:%s:%s' % (rev, rev)
1427 selected = 'rev:%s:%s' % (rev, rev)
1405 else:
1428 else:
1406 raise EmptyRepositoryError()
1429 raise EmptyRepositoryError()
1407 return groups, selected
1430 return groups, selected
1408
1431
1409 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1432 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1410 return self._get_diff_from_pr_or_version(
1433 return self._get_diff_from_pr_or_version(
1411 source_repo, source_ref_id, target_ref_id, context=context)
1434 source_repo, source_ref_id, target_ref_id, context=context)
1412
1435
1413 def _get_diff_from_pr_or_version(
1436 def _get_diff_from_pr_or_version(
1414 self, source_repo, source_ref_id, target_ref_id, context):
1437 self, source_repo, source_ref_id, target_ref_id, context):
1415 target_commit = source_repo.get_commit(
1438 target_commit = source_repo.get_commit(
1416 commit_id=safe_str(target_ref_id))
1439 commit_id=safe_str(target_ref_id))
1417 source_commit = source_repo.get_commit(
1440 source_commit = source_repo.get_commit(
1418 commit_id=safe_str(source_ref_id))
1441 commit_id=safe_str(source_ref_id))
1419 if isinstance(source_repo, Repository):
1442 if isinstance(source_repo, Repository):
1420 vcs_repo = source_repo.scm_instance()
1443 vcs_repo = source_repo.scm_instance()
1421 else:
1444 else:
1422 vcs_repo = source_repo
1445 vcs_repo = source_repo
1423
1446
1424 # TODO: johbo: In the context of an update, we cannot reach
1447 # TODO: johbo: In the context of an update, we cannot reach
1425 # the old commit anymore with our normal mechanisms. It needs
1448 # the old commit anymore with our normal mechanisms. It needs
1426 # some sort of special support in the vcs layer to avoid this
1449 # some sort of special support in the vcs layer to avoid this
1427 # workaround.
1450 # workaround.
1428 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1451 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1429 vcs_repo.alias == 'git'):
1452 vcs_repo.alias == 'git'):
1430 source_commit.raw_id = safe_str(source_ref_id)
1453 source_commit.raw_id = safe_str(source_ref_id)
1431
1454
1432 log.debug('calculating diff between '
1455 log.debug('calculating diff between '
1433 'source_ref:%s and target_ref:%s for repo `%s`',
1456 'source_ref:%s and target_ref:%s for repo `%s`',
1434 target_ref_id, source_ref_id,
1457 target_ref_id, source_ref_id,
1435 safe_unicode(vcs_repo.path))
1458 safe_unicode(vcs_repo.path))
1436
1459
1437 vcs_diff = vcs_repo.get_diff(
1460 vcs_diff = vcs_repo.get_diff(
1438 commit1=target_commit, commit2=source_commit, context=context)
1461 commit1=target_commit, commit2=source_commit, context=context)
1439 return vcs_diff
1462 return vcs_diff
1440
1463
1441 def _is_merge_enabled(self, pull_request):
1464 def _is_merge_enabled(self, pull_request):
1442 return self._get_general_setting(
1465 return self._get_general_setting(
1443 pull_request, 'rhodecode_pr_merge_enabled')
1466 pull_request, 'rhodecode_pr_merge_enabled')
1444
1467
1445 def _use_rebase_for_merging(self, pull_request):
1468 def _use_rebase_for_merging(self, pull_request):
1446 repo_type = pull_request.target_repo.repo_type
1469 repo_type = pull_request.target_repo.repo_type
1447 if repo_type == 'hg':
1470 if repo_type == 'hg':
1448 return self._get_general_setting(
1471 return self._get_general_setting(
1449 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1472 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1450 elif repo_type == 'git':
1473 elif repo_type == 'git':
1451 return self._get_general_setting(
1474 return self._get_general_setting(
1452 pull_request, 'rhodecode_git_use_rebase_for_merging')
1475 pull_request, 'rhodecode_git_use_rebase_for_merging')
1453
1476
1454 return False
1477 return False
1455
1478
1456 def _close_branch_before_merging(self, pull_request):
1479 def _close_branch_before_merging(self, pull_request):
1457 repo_type = pull_request.target_repo.repo_type
1480 repo_type = pull_request.target_repo.repo_type
1458 if repo_type == 'hg':
1481 if repo_type == 'hg':
1459 return self._get_general_setting(
1482 return self._get_general_setting(
1460 pull_request, 'rhodecode_hg_close_branch_before_merging')
1483 pull_request, 'rhodecode_hg_close_branch_before_merging')
1461 elif repo_type == 'git':
1484 elif repo_type == 'git':
1462 return self._get_general_setting(
1485 return self._get_general_setting(
1463 pull_request, 'rhodecode_git_close_branch_before_merging')
1486 pull_request, 'rhodecode_git_close_branch_before_merging')
1464
1487
1465 return False
1488 return False
1466
1489
1467 def _get_general_setting(self, pull_request, settings_key, default=False):
1490 def _get_general_setting(self, pull_request, settings_key, default=False):
1468 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1491 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1469 settings = settings_model.get_general_settings()
1492 settings = settings_model.get_general_settings()
1470 return settings.get(settings_key, default)
1493 return settings.get(settings_key, default)
1471
1494
1472 def _log_audit_action(self, action, action_data, user, pull_request):
1495 def _log_audit_action(self, action, action_data, user, pull_request):
1473 audit_logger.store(
1496 audit_logger.store(
1474 action=action,
1497 action=action,
1475 action_data=action_data,
1498 action_data=action_data,
1476 user=user,
1499 user=user,
1477 repo=pull_request.target_repo)
1500 repo=pull_request.target_repo)
1478
1501
1479 def get_reviewer_functions(self):
1502 def get_reviewer_functions(self):
1480 """
1503 """
1481 Fetches functions for validation and fetching default reviewers.
1504 Fetches functions for validation and fetching default reviewers.
1482 If available we use the EE package, else we fallback to CE
1505 If available we use the EE package, else we fallback to CE
1483 package functions
1506 package functions
1484 """
1507 """
1485 try:
1508 try:
1486 from rc_reviewers.utils import get_default_reviewers_data
1509 from rc_reviewers.utils import get_default_reviewers_data
1487 from rc_reviewers.utils import validate_default_reviewers
1510 from rc_reviewers.utils import validate_default_reviewers
1488 except ImportError:
1511 except ImportError:
1489 from rhodecode.apps.repository.utils import \
1512 from rhodecode.apps.repository.utils import \
1490 get_default_reviewers_data
1513 get_default_reviewers_data
1491 from rhodecode.apps.repository.utils import \
1514 from rhodecode.apps.repository.utils import \
1492 validate_default_reviewers
1515 validate_default_reviewers
1493
1516
1494 return get_default_reviewers_data, validate_default_reviewers
1517 return get_default_reviewers_data, validate_default_reviewers
1495
1518
1496
1519
1497 class MergeCheck(object):
1520 class MergeCheck(object):
1498 """
1521 """
1499 Perform Merge Checks and returns a check object which stores information
1522 Perform Merge Checks and returns a check object which stores information
1500 about merge errors, and merge conditions
1523 about merge errors, and merge conditions
1501 """
1524 """
1502 TODO_CHECK = 'todo'
1525 TODO_CHECK = 'todo'
1503 PERM_CHECK = 'perm'
1526 PERM_CHECK = 'perm'
1504 REVIEW_CHECK = 'review'
1527 REVIEW_CHECK = 'review'
1505 MERGE_CHECK = 'merge'
1528 MERGE_CHECK = 'merge'
1506
1529
1507 def __init__(self):
1530 def __init__(self):
1508 self.review_status = None
1531 self.review_status = None
1509 self.merge_possible = None
1532 self.merge_possible = None
1510 self.merge_msg = ''
1533 self.merge_msg = ''
1511 self.failed = None
1534 self.failed = None
1512 self.errors = []
1535 self.errors = []
1513 self.error_details = OrderedDict()
1536 self.error_details = OrderedDict()
1514
1537
1515 def push_error(self, error_type, message, error_key, details):
1538 def push_error(self, error_type, message, error_key, details):
1516 self.failed = True
1539 self.failed = True
1517 self.errors.append([error_type, message])
1540 self.errors.append([error_type, message])
1518 self.error_details[error_key] = dict(
1541 self.error_details[error_key] = dict(
1519 details=details,
1542 details=details,
1520 error_type=error_type,
1543 error_type=error_type,
1521 message=message
1544 message=message
1522 )
1545 )
1523
1546
1524 @classmethod
1547 @classmethod
1525 def validate(cls, pull_request, user, translator, fail_early=False):
1548 def validate(cls, pull_request, user, translator, fail_early=False):
1526 _ = translator
1549 _ = translator
1527 merge_check = cls()
1550 merge_check = cls()
1528
1551
1529 # permissions to merge
1552 # permissions to merge
1530 user_allowed_to_merge = PullRequestModel().check_user_merge(
1553 user_allowed_to_merge = PullRequestModel().check_user_merge(
1531 pull_request, user)
1554 pull_request, user)
1532 if not user_allowed_to_merge:
1555 if not user_allowed_to_merge:
1533 log.debug("MergeCheck: cannot merge, approval is pending.")
1556 log.debug("MergeCheck: cannot merge, approval is pending.")
1534
1557
1535 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1558 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1536 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1559 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1537 if fail_early:
1560 if fail_early:
1538 return merge_check
1561 return merge_check
1539
1562
1540 # review status, must be always present
1563 # review status, must be always present
1541 review_status = pull_request.calculated_review_status()
1564 review_status = pull_request.calculated_review_status()
1542 merge_check.review_status = review_status
1565 merge_check.review_status = review_status
1543
1566
1544 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1567 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1545 if not status_approved:
1568 if not status_approved:
1546 log.debug("MergeCheck: cannot merge, approval is pending.")
1569 log.debug("MergeCheck: cannot merge, approval is pending.")
1547
1570
1548 msg = _('Pull request reviewer approval is pending.')
1571 msg = _('Pull request reviewer approval is pending.')
1549
1572
1550 merge_check.push_error(
1573 merge_check.push_error(
1551 'warning', msg, cls.REVIEW_CHECK, review_status)
1574 'warning', msg, cls.REVIEW_CHECK, review_status)
1552
1575
1553 if fail_early:
1576 if fail_early:
1554 return merge_check
1577 return merge_check
1555
1578
1556 # left over TODOs
1579 # left over TODOs
1557 todos = CommentsModel().get_unresolved_todos(pull_request)
1580 todos = CommentsModel().get_unresolved_todos(pull_request)
1558 if todos:
1581 if todos:
1559 log.debug("MergeCheck: cannot merge, {} "
1582 log.debug("MergeCheck: cannot merge, {} "
1560 "unresolved todos left.".format(len(todos)))
1583 "unresolved todos left.".format(len(todos)))
1561
1584
1562 if len(todos) == 1:
1585 if len(todos) == 1:
1563 msg = _('Cannot merge, {} TODO still not resolved.').format(
1586 msg = _('Cannot merge, {} TODO still not resolved.').format(
1564 len(todos))
1587 len(todos))
1565 else:
1588 else:
1566 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1589 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1567 len(todos))
1590 len(todos))
1568
1591
1569 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1592 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1570
1593
1571 if fail_early:
1594 if fail_early:
1572 return merge_check
1595 return merge_check
1573
1596
1574 # merge possible
1597 # merge possible
1575 merge_status, msg = PullRequestModel().merge_status(
1598 merge_status, msg = PullRequestModel().merge_status(
1576 pull_request, translator=translator)
1599 pull_request, translator=translator)
1577 merge_check.merge_possible = merge_status
1600 merge_check.merge_possible = merge_status
1578 merge_check.merge_msg = msg
1601 merge_check.merge_msg = msg
1579 if not merge_status:
1602 if not merge_status:
1580 log.debug(
1603 log.debug(
1581 "MergeCheck: cannot merge, pull request merge not possible.")
1604 "MergeCheck: cannot merge, pull request merge not possible.")
1582 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1605 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1583
1606
1584 if fail_early:
1607 if fail_early:
1585 return merge_check
1608 return merge_check
1586
1609
1587 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1610 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1588 return merge_check
1611 return merge_check
1589
1612
1590 @classmethod
1613 @classmethod
1591 def get_merge_conditions(cls, pull_request, translator):
1614 def get_merge_conditions(cls, pull_request, translator):
1592 _ = translator
1615 _ = translator
1593 merge_details = {}
1616 merge_details = {}
1594
1617
1595 model = PullRequestModel()
1618 model = PullRequestModel()
1596 use_rebase = model._use_rebase_for_merging(pull_request)
1619 use_rebase = model._use_rebase_for_merging(pull_request)
1597
1620
1598 if use_rebase:
1621 if use_rebase:
1599 merge_details['merge_strategy'] = dict(
1622 merge_details['merge_strategy'] = dict(
1600 details={},
1623 details={},
1601 message=_('Merge strategy: rebase')
1624 message=_('Merge strategy: rebase')
1602 )
1625 )
1603 else:
1626 else:
1604 merge_details['merge_strategy'] = dict(
1627 merge_details['merge_strategy'] = dict(
1605 details={},
1628 details={},
1606 message=_('Merge strategy: explicit merge commit')
1629 message=_('Merge strategy: explicit merge commit')
1607 )
1630 )
1608
1631
1609 close_branch = model._close_branch_before_merging(pull_request)
1632 close_branch = model._close_branch_before_merging(pull_request)
1610 if close_branch:
1633 if close_branch:
1611 repo_type = pull_request.target_repo.repo_type
1634 repo_type = pull_request.target_repo.repo_type
1612 if repo_type == 'hg':
1635 if repo_type == 'hg':
1613 close_msg = _('Source branch will be closed after merge.')
1636 close_msg = _('Source branch will be closed after merge.')
1614 elif repo_type == 'git':
1637 elif repo_type == 'git':
1615 close_msg = _('Source branch will be deleted after merge.')
1638 close_msg = _('Source branch will be deleted after merge.')
1616
1639
1617 merge_details['close_branch'] = dict(
1640 merge_details['close_branch'] = dict(
1618 details={},
1641 details={},
1619 message=close_msg
1642 message=close_msg
1620 )
1643 )
1621
1644
1622 return merge_details
1645 return merge_details
1623
1646
1624 ChangeTuple = collections.namedtuple(
1647 ChangeTuple = collections.namedtuple(
1625 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1648 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1626
1649
1627 FileChangeTuple = collections.namedtuple(
1650 FileChangeTuple = collections.namedtuple(
1628 'FileChangeTuple', ['added', 'modified', 'removed'])
1651 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now