##// END OF EJS Templates
pull-requests: security, check for permissions on exposure of repo-refs
ergo -
r2379:76c34f08 default
parent child Browse files
Show More
@@ -1,1244 +1,1251 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):
204 def _get_pr_version(self, pull_request_id, version=None):
205 at_version = None
205 at_version = None
206
206
207 if version and version == 'latest':
207 if version and version == 'latest':
208 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_ver = PullRequest.get(pull_request_id)
209 pull_request_obj = pull_request_ver
209 pull_request_obj = pull_request_ver
210 _org_pull_request_obj = pull_request_obj
210 _org_pull_request_obj = pull_request_obj
211 at_version = 'latest'
211 at_version = 'latest'
212 elif version:
212 elif version:
213 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_ver = PullRequestVersion.get_or_404(version)
214 pull_request_obj = pull_request_ver
214 pull_request_obj = pull_request_ver
215 _org_pull_request_obj = pull_request_ver.pull_request
215 _org_pull_request_obj = pull_request_ver.pull_request
216 at_version = pull_request_ver.pull_request_version_id
216 at_version = pull_request_ver.pull_request_version_id
217 else:
217 else:
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
219 pull_request_id)
219 pull_request_id)
220
220
221 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_display_obj = PullRequest.get_pr_display_object(
222 pull_request_obj, _org_pull_request_obj)
222 pull_request_obj, _org_pull_request_obj)
223
223
224 return _org_pull_request_obj, pull_request_obj, \
224 return _org_pull_request_obj, pull_request_obj, \
225 pull_request_display_obj, at_version
225 pull_request_display_obj, at_version
226
226
227 def _get_diffset(self, source_repo_name, source_repo,
227 def _get_diffset(self, source_repo_name, source_repo,
228 source_ref_id, target_ref_id,
228 source_ref_id, target_ref_id,
229 target_commit, source_commit, diff_limit, fulldiff,
229 target_commit, source_commit, diff_limit, fulldiff,
230 file_limit, display_inline_comments):
230 file_limit, display_inline_comments):
231
231
232 vcs_diff = PullRequestModel().get_diff(
232 vcs_diff = PullRequestModel().get_diff(
233 source_repo, source_ref_id, target_ref_id)
233 source_repo, source_ref_id, target_ref_id)
234
234
235 diff_processor = diffs.DiffProcessor(
235 diff_processor = diffs.DiffProcessor(
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 vcs_diff, format='newdiff', diff_limit=diff_limit,
237 file_limit=file_limit, show_full_diff=fulldiff)
237 file_limit=file_limit, show_full_diff=fulldiff)
238
238
239 _parsed = diff_processor.prepare()
239 _parsed = diff_processor.prepare()
240
240
241 def _node_getter(commit):
241 def _node_getter(commit):
242 def get_node(fname):
242 def get_node(fname):
243 try:
243 try:
244 return commit.get_node(fname)
244 return commit.get_node(fname)
245 except NodeDoesNotExistError:
245 except NodeDoesNotExistError:
246 return None
246 return None
247
247
248 return get_node
248 return get_node
249
249
250 diffset = codeblocks.DiffSet(
250 diffset = codeblocks.DiffSet(
251 repo_name=self.db_repo_name,
251 repo_name=self.db_repo_name,
252 source_repo_name=source_repo_name,
252 source_repo_name=source_repo_name,
253 source_node_getter=_node_getter(target_commit),
253 source_node_getter=_node_getter(target_commit),
254 target_node_getter=_node_getter(source_commit),
254 target_node_getter=_node_getter(source_commit),
255 comments=display_inline_comments
255 comments=display_inline_comments
256 )
256 )
257 diffset = diffset.render_patchset(
257 diffset = diffset.render_patchset(
258 _parsed, target_commit.raw_id, source_commit.raw_id)
258 _parsed, target_commit.raw_id, source_commit.raw_id)
259
259
260 return diffset
260 return diffset
261
261
262 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
265 @view_config(
266 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
268 def pull_request_show(self):
269 pull_request_id = self.request.matchdict['pull_request_id']
269 pull_request_id = self.request.matchdict['pull_request_id']
270
270
271 c = self.load_default_context()
271 c = self.load_default_context()
272
272
273 version = self.request.GET.get('version')
273 version = self.request.GET.get('version')
274 from_version = self.request.GET.get('from_version') or version
274 from_version = self.request.GET.get('from_version') or version
275 merge_checks = self.request.GET.get('merge_checks')
275 merge_checks = self.request.GET.get('merge_checks')
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
277
277
278 (pull_request_latest,
278 (pull_request_latest,
279 pull_request_at_ver,
279 pull_request_at_ver,
280 pull_request_display_obj,
280 pull_request_display_obj,
281 at_version) = self._get_pr_version(
281 at_version) = self._get_pr_version(
282 pull_request_id, version=version)
282 pull_request_id, version=version)
283 pr_closed = pull_request_latest.is_closed()
283 pr_closed = pull_request_latest.is_closed()
284
284
285 if pr_closed and (version or from_version):
285 if pr_closed and (version or from_version):
286 # not allow to browse versions
286 # not allow to browse versions
287 raise HTTPFound(h.route_path(
287 raise HTTPFound(h.route_path(
288 'pullrequest_show', repo_name=self.db_repo_name,
288 'pullrequest_show', repo_name=self.db_repo_name,
289 pull_request_id=pull_request_id))
289 pull_request_id=pull_request_id))
290
290
291 versions = pull_request_display_obj.versions()
291 versions = pull_request_display_obj.versions()
292
292
293 c.at_version = at_version
293 c.at_version = at_version
294 c.at_version_num = (at_version
294 c.at_version_num = (at_version
295 if at_version and at_version != 'latest'
295 if at_version and at_version != 'latest'
296 else None)
296 else None)
297 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_pos = ChangesetComment.get_index_from_version(
298 c.at_version_num, versions)
298 c.at_version_num, versions)
299
299
300 (prev_pull_request_latest,
300 (prev_pull_request_latest,
301 prev_pull_request_at_ver,
301 prev_pull_request_at_ver,
302 prev_pull_request_display_obj,
302 prev_pull_request_display_obj,
303 prev_at_version) = self._get_pr_version(
303 prev_at_version) = self._get_pr_version(
304 pull_request_id, version=from_version)
304 pull_request_id, version=from_version)
305
305
306 c.from_version = prev_at_version
306 c.from_version = prev_at_version
307 c.from_version_num = (prev_at_version
307 c.from_version_num = (prev_at_version
308 if prev_at_version and prev_at_version != 'latest'
308 if prev_at_version and prev_at_version != 'latest'
309 else None)
309 else None)
310 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_pos = ChangesetComment.get_index_from_version(
311 c.from_version_num, versions)
311 c.from_version_num, versions)
312
312
313 # define if we're in COMPARE mode or VIEW at version mode
313 # define if we're in COMPARE mode or VIEW at version mode
314 compare = at_version != prev_at_version
314 compare = at_version != prev_at_version
315
315
316 # pull_requests repo_name we opened it against
316 # pull_requests repo_name we opened it against
317 # ie. target_repo must match
317 # ie. target_repo must match
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
319 raise HTTPNotFound()
319 raise HTTPNotFound()
320
320
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
322 pull_request_at_ver)
322 pull_request_at_ver)
323
323
324 c.pull_request = pull_request_display_obj
324 c.pull_request = pull_request_display_obj
325 c.pull_request_latest = pull_request_latest
325 c.pull_request_latest = pull_request_latest
326
326
327 if compare or (at_version and not at_version == 'latest'):
327 if compare or (at_version and not at_version == 'latest'):
328 c.allowed_to_change_status = False
328 c.allowed_to_change_status = False
329 c.allowed_to_update = False
329 c.allowed_to_update = False
330 c.allowed_to_merge = False
330 c.allowed_to_merge = False
331 c.allowed_to_delete = False
331 c.allowed_to_delete = False
332 c.allowed_to_comment = False
332 c.allowed_to_comment = False
333 c.allowed_to_close = False
333 c.allowed_to_close = False
334 else:
334 else:
335 can_change_status = PullRequestModel().check_user_change_status(
335 can_change_status = PullRequestModel().check_user_change_status(
336 pull_request_at_ver, self._rhodecode_user)
336 pull_request_at_ver, self._rhodecode_user)
337 c.allowed_to_change_status = can_change_status and not pr_closed
337 c.allowed_to_change_status = can_change_status and not pr_closed
338
338
339 c.allowed_to_update = PullRequestModel().check_user_update(
339 c.allowed_to_update = PullRequestModel().check_user_update(
340 pull_request_latest, self._rhodecode_user) and not pr_closed
340 pull_request_latest, self._rhodecode_user) and not pr_closed
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 c.allowed_to_merge = PullRequestModel().check_user_merge(
342 pull_request_latest, self._rhodecode_user) and not pr_closed
342 pull_request_latest, self._rhodecode_user) and not pr_closed
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 c.allowed_to_delete = PullRequestModel().check_user_delete(
344 pull_request_latest, self._rhodecode_user) and not pr_closed
344 pull_request_latest, self._rhodecode_user) and not pr_closed
345 c.allowed_to_comment = not pr_closed
345 c.allowed_to_comment = not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 c.allowed_to_close = c.allowed_to_merge and not pr_closed
347
347
348 c.forbid_adding_reviewers = False
348 c.forbid_adding_reviewers = False
349 c.forbid_author_to_review = False
349 c.forbid_author_to_review = False
350 c.forbid_commit_author_to_review = False
350 c.forbid_commit_author_to_review = False
351
351
352 if pull_request_latest.reviewer_data and \
352 if pull_request_latest.reviewer_data and \
353 'rules' in pull_request_latest.reviewer_data:
353 'rules' in pull_request_latest.reviewer_data:
354 rules = pull_request_latest.reviewer_data['rules'] or {}
354 rules = pull_request_latest.reviewer_data['rules'] or {}
355 try:
355 try:
356 c.forbid_adding_reviewers = rules.get(
356 c.forbid_adding_reviewers = rules.get(
357 'forbid_adding_reviewers')
357 'forbid_adding_reviewers')
358 c.forbid_author_to_review = rules.get(
358 c.forbid_author_to_review = rules.get(
359 'forbid_author_to_review')
359 'forbid_author_to_review')
360 c.forbid_commit_author_to_review = rules.get(
360 c.forbid_commit_author_to_review = rules.get(
361 'forbid_commit_author_to_review')
361 'forbid_commit_author_to_review')
362 except Exception:
362 except Exception:
363 pass
363 pass
364
364
365 # check merge capabilities
365 # check merge capabilities
366 _merge_check = MergeCheck.validate(
366 _merge_check = MergeCheck.validate(
367 pull_request_latest, user=self._rhodecode_user,
367 pull_request_latest, user=self._rhodecode_user,
368 translator=self.request.translate)
368 translator=self.request.translate)
369 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_errors = _merge_check.error_details
370 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_possible = not _merge_check.failed
371 c.pr_merge_message = _merge_check.merge_msg
371 c.pr_merge_message = _merge_check.merge_msg
372
372
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 c.pr_merge_info = MergeCheck.get_merge_conditions(
374 pull_request_latest, translator=self.request.translate)
374 pull_request_latest, translator=self.request.translate)
375
375
376 c.pull_request_review_status = _merge_check.review_status
376 c.pull_request_review_status = _merge_check.review_status
377 if merge_checks:
377 if merge_checks:
378 self.request.override_renderer = \
378 self.request.override_renderer = \
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
380 return self._get_template_context(c)
380 return self._get_template_context(c)
381
381
382 comments_model = CommentsModel()
382 comments_model = CommentsModel()
383
383
384 # reviewers and statuses
384 # reviewers and statuses
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
387
387
388 # GENERAL COMMENTS with versions #
388 # GENERAL COMMENTS with versions #
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
390 q = q.order_by(ChangesetComment.comment_id.asc())
390 q = q.order_by(ChangesetComment.comment_id.asc())
391 general_comments = q
391 general_comments = q
392
392
393 # pick comments we want to render at current version
393 # pick comments we want to render at current version
394 c.comment_versions = comments_model.aggregate_comments(
394 c.comment_versions = comments_model.aggregate_comments(
395 general_comments, versions, c.at_version_num)
395 general_comments, versions, c.at_version_num)
396 c.comments = c.comment_versions[c.at_version_num]['until']
396 c.comments = c.comment_versions[c.at_version_num]['until']
397
397
398 # INLINE COMMENTS with versions #
398 # INLINE COMMENTS with versions #
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
400 q = q.order_by(ChangesetComment.comment_id.asc())
400 q = q.order_by(ChangesetComment.comment_id.asc())
401 inline_comments = q
401 inline_comments = q
402
402
403 c.inline_versions = comments_model.aggregate_comments(
403 c.inline_versions = comments_model.aggregate_comments(
404 inline_comments, versions, c.at_version_num, inline=True)
404 inline_comments, versions, c.at_version_num, inline=True)
405
405
406 # inject latest version
406 # inject latest version
407 latest_ver = PullRequest.get_pr_display_object(
407 latest_ver = PullRequest.get_pr_display_object(
408 pull_request_latest, pull_request_latest)
408 pull_request_latest, pull_request_latest)
409
409
410 c.versions = versions + [latest_ver]
410 c.versions = versions + [latest_ver]
411
411
412 # if we use version, then do not show later comments
412 # if we use version, then do not show later comments
413 # than current version
413 # than current version
414 display_inline_comments = collections.defaultdict(
414 display_inline_comments = collections.defaultdict(
415 lambda: collections.defaultdict(list))
415 lambda: collections.defaultdict(list))
416 for co in inline_comments:
416 for co in inline_comments:
417 if c.at_version_num:
417 if c.at_version_num:
418 # pick comments that are at least UPTO given version, so we
418 # pick comments that are at least UPTO given version, so we
419 # don't render comments for higher version
419 # don't render comments for higher version
420 should_render = co.pull_request_version_id and \
420 should_render = co.pull_request_version_id and \
421 co.pull_request_version_id <= c.at_version_num
421 co.pull_request_version_id <= c.at_version_num
422 else:
422 else:
423 # showing all, for 'latest'
423 # showing all, for 'latest'
424 should_render = True
424 should_render = True
425
425
426 if should_render:
426 if should_render:
427 display_inline_comments[co.f_path][co.line_no].append(co)
427 display_inline_comments[co.f_path][co.line_no].append(co)
428
428
429 # load diff data into template context, if we use compare mode then
429 # load diff data into template context, if we use compare mode then
430 # diff is calculated based on changes between versions of PR
430 # diff is calculated based on changes between versions of PR
431
431
432 source_repo = pull_request_at_ver.source_repo
432 source_repo = pull_request_at_ver.source_repo
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
434
434
435 target_repo = pull_request_at_ver.target_repo
435 target_repo = pull_request_at_ver.target_repo
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
437
437
438 if compare:
438 if compare:
439 # in compare switch the diff base to latest commit from prev version
439 # in compare switch the diff base to latest commit from prev version
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 target_ref_id = prev_pull_request_display_obj.revisions[0]
441
441
442 # despite opening commits for bookmarks/branches/tags, we always
442 # despite opening commits for bookmarks/branches/tags, we always
443 # convert this to rev to prevent changes after bookmark or branch change
443 # convert this to rev to prevent changes after bookmark or branch change
444 c.source_ref_type = 'rev'
444 c.source_ref_type = 'rev'
445 c.source_ref = source_ref_id
445 c.source_ref = source_ref_id
446
446
447 c.target_ref_type = 'rev'
447 c.target_ref_type = 'rev'
448 c.target_ref = target_ref_id
448 c.target_ref = target_ref_id
449
449
450 c.source_repo = source_repo
450 c.source_repo = source_repo
451 c.target_repo = target_repo
451 c.target_repo = target_repo
452
452
453 c.commit_ranges = []
453 c.commit_ranges = []
454 source_commit = EmptyCommit()
454 source_commit = EmptyCommit()
455 target_commit = EmptyCommit()
455 target_commit = EmptyCommit()
456 c.missing_requirements = False
456 c.missing_requirements = False
457
457
458 source_scm = source_repo.scm_instance()
458 source_scm = source_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
459 target_scm = target_repo.scm_instance()
460
460
461 # try first shadow repo, fallback to regular repo
461 # try first shadow repo, fallback to regular repo
462 try:
462 try:
463 commits_source_repo = pull_request_latest.get_shadow_repo()
463 commits_source_repo = pull_request_latest.get_shadow_repo()
464 except Exception:
464 except Exception:
465 log.debug('Failed to get shadow repo', exc_info=True)
465 log.debug('Failed to get shadow repo', exc_info=True)
466 commits_source_repo = source_scm
466 commits_source_repo = source_scm
467
467
468 c.commits_source_repo = commits_source_repo
468 c.commits_source_repo = commits_source_repo
469 commit_cache = {}
469 commit_cache = {}
470 try:
470 try:
471 pre_load = ["author", "branch", "date", "message"]
471 pre_load = ["author", "branch", "date", "message"]
472 show_revs = pull_request_at_ver.revisions
472 show_revs = pull_request_at_ver.revisions
473 for rev in show_revs:
473 for rev in show_revs:
474 comm = commits_source_repo.get_commit(
474 comm = commits_source_repo.get_commit(
475 commit_id=rev, pre_load=pre_load)
475 commit_id=rev, pre_load=pre_load)
476 c.commit_ranges.append(comm)
476 c.commit_ranges.append(comm)
477 commit_cache[comm.raw_id] = comm
477 commit_cache[comm.raw_id] = comm
478
478
479 # Order here matters, we first need to get target, and then
479 # Order here matters, we first need to get target, and then
480 # the source
480 # the source
481 target_commit = commits_source_repo.get_commit(
481 target_commit = commits_source_repo.get_commit(
482 commit_id=safe_str(target_ref_id))
482 commit_id=safe_str(target_ref_id))
483
483
484 source_commit = commits_source_repo.get_commit(
484 source_commit = commits_source_repo.get_commit(
485 commit_id=safe_str(source_ref_id))
485 commit_id=safe_str(source_ref_id))
486
486
487 except CommitDoesNotExistError:
487 except CommitDoesNotExistError:
488 log.warning(
488 log.warning(
489 'Failed to get commit from `{}` repo'.format(
489 'Failed to get commit from `{}` repo'.format(
490 commits_source_repo), exc_info=True)
490 commits_source_repo), exc_info=True)
491 except RepositoryRequirementError:
491 except RepositoryRequirementError:
492 log.warning(
492 log.warning(
493 'Failed to get all required data from repo', exc_info=True)
493 'Failed to get all required data from repo', exc_info=True)
494 c.missing_requirements = True
494 c.missing_requirements = True
495
495
496 c.ancestor = None # set it to None, to hide it from PR view
496 c.ancestor = None # set it to None, to hide it from PR view
497
497
498 try:
498 try:
499 ancestor_id = source_scm.get_common_ancestor(
499 ancestor_id = source_scm.get_common_ancestor(
500 source_commit.raw_id, target_commit.raw_id, target_scm)
500 source_commit.raw_id, target_commit.raw_id, target_scm)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 c.ancestor_commit = source_scm.get_commit(ancestor_id)
502 except Exception:
502 except Exception:
503 c.ancestor_commit = None
503 c.ancestor_commit = None
504
504
505 c.statuses = source_repo.statuses(
505 c.statuses = source_repo.statuses(
506 [x.raw_id for x in c.commit_ranges])
506 [x.raw_id for x in c.commit_ranges])
507
507
508 # auto collapse if we have more than limit
508 # auto collapse if we have more than limit
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 collapse_limit = diffs.DiffProcessor._collapse_commits_over
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
511 c.compare_mode = compare
511 c.compare_mode = compare
512
512
513 # diff_limit is the old behavior, will cut off the whole diff
513 # diff_limit is the old behavior, will cut off the whole diff
514 # if the limit is applied otherwise will just hide the
514 # if the limit is applied otherwise will just hide the
515 # big files from the front-end
515 # big files from the front-end
516 diff_limit = c.visual.cut_off_limit_diff
516 diff_limit = c.visual.cut_off_limit_diff
517 file_limit = c.visual.cut_off_limit_file
517 file_limit = c.visual.cut_off_limit_file
518
518
519 c.missing_commits = False
519 c.missing_commits = False
520 if (c.missing_requirements
520 if (c.missing_requirements
521 or isinstance(source_commit, EmptyCommit)
521 or isinstance(source_commit, EmptyCommit)
522 or source_commit == target_commit):
522 or source_commit == target_commit):
523
523
524 c.missing_commits = True
524 c.missing_commits = True
525 else:
525 else:
526
526
527 c.diffset = self._get_diffset(
527 c.diffset = self._get_diffset(
528 c.source_repo.repo_name, commits_source_repo,
528 c.source_repo.repo_name, commits_source_repo,
529 source_ref_id, target_ref_id,
529 source_ref_id, target_ref_id,
530 target_commit, source_commit,
530 target_commit, source_commit,
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 diff_limit, c.fulldiff, file_limit, display_inline_comments)
532
532
533 c.limited_diff = c.diffset.limited_diff
533 c.limited_diff = c.diffset.limited_diff
534
534
535 # calculate removed files that are bound to comments
535 # calculate removed files that are bound to comments
536 comment_deleted_files = [
536 comment_deleted_files = [
537 fname for fname in display_inline_comments
537 fname for fname in display_inline_comments
538 if fname not in c.diffset.file_stats]
538 if fname not in c.diffset.file_stats]
539
539
540 c.deleted_files_comments = collections.defaultdict(dict)
540 c.deleted_files_comments = collections.defaultdict(dict)
541 for fname, per_line_comments in display_inline_comments.items():
541 for fname, per_line_comments in display_inline_comments.items():
542 if fname in comment_deleted_files:
542 if fname in comment_deleted_files:
543 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['stats'] = 0
544 c.deleted_files_comments[fname]['comments'] = list()
544 c.deleted_files_comments[fname]['comments'] = list()
545 for lno, comments in per_line_comments.items():
545 for lno, comments in per_line_comments.items():
546 c.deleted_files_comments[fname]['comments'].extend(
546 c.deleted_files_comments[fname]['comments'].extend(
547 comments)
547 comments)
548
548
549 # this is a hack to properly display links, when creating PR, the
549 # this is a hack to properly display links, when creating PR, the
550 # compare view and others uses different notation, and
550 # compare view and others uses different notation, and
551 # compare_commits.mako renders links based on the target_repo.
551 # 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
552 # We need to swap that here to generate it properly on the html side
553 c.target_repo = c.source_repo
553 c.target_repo = c.source_repo
554
554
555 c.commit_statuses = ChangesetStatus.STATUSES
555 c.commit_statuses = ChangesetStatus.STATUSES
556
556
557 c.show_version_changes = not pr_closed
557 c.show_version_changes = not pr_closed
558 if c.show_version_changes:
558 if c.show_version_changes:
559 cur_obj = pull_request_at_ver
559 cur_obj = pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
560 prev_obj = prev_pull_request_at_ver
561
561
562 old_commit_ids = prev_obj.revisions
562 old_commit_ids = prev_obj.revisions
563 new_commit_ids = cur_obj.revisions
563 new_commit_ids = cur_obj.revisions
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 commit_changes = PullRequestModel()._calculate_commit_id_changes(
565 old_commit_ids, new_commit_ids)
565 old_commit_ids, new_commit_ids)
566 c.commit_changes_summary = commit_changes
566 c.commit_changes_summary = commit_changes
567
567
568 # calculate the diff for commits between versions
568 # calculate the diff for commits between versions
569 c.commit_changes = []
569 c.commit_changes = []
570 mark = lambda cs, fw: list(
570 mark = lambda cs, fw: list(
571 h.itertools.izip_longest([], cs, fillvalue=fw))
571 h.itertools.izip_longest([], cs, fillvalue=fw))
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 for c_type, raw_id in mark(commit_changes.added, 'a') \
573 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.removed, 'r') \
574 + mark(commit_changes.common, 'c'):
574 + mark(commit_changes.common, 'c'):
575
575
576 if raw_id in commit_cache:
576 if raw_id in commit_cache:
577 commit = commit_cache[raw_id]
577 commit = commit_cache[raw_id]
578 else:
578 else:
579 try:
579 try:
580 commit = commits_source_repo.get_commit(raw_id)
580 commit = commits_source_repo.get_commit(raw_id)
581 except CommitDoesNotExistError:
581 except CommitDoesNotExistError:
582 # in case we fail extracting still use "dummy" commit
582 # in case we fail extracting still use "dummy" commit
583 # for display in commit diff
583 # for display in commit diff
584 commit = h.AttributeDict(
584 commit = h.AttributeDict(
585 {'raw_id': raw_id,
585 {'raw_id': raw_id,
586 'message': 'EMPTY or MISSING COMMIT'})
586 'message': 'EMPTY or MISSING COMMIT'})
587 c.commit_changes.append([c_type, commit])
587 c.commit_changes.append([c_type, commit])
588
588
589 # current user review statuses for each version
589 # current user review statuses for each version
590 c.review_versions = {}
590 c.review_versions = {}
591 if self._rhodecode_user.user_id in allowed_reviewers:
591 if self._rhodecode_user.user_id in allowed_reviewers:
592 for co in general_comments:
592 for co in general_comments:
593 if co.author.user_id == self._rhodecode_user.user_id:
593 if co.author.user_id == self._rhodecode_user.user_id:
594 # each comment has a status change
594 # each comment has a status change
595 status = co.status_change
595 status = co.status_change
596 if status:
596 if status:
597 _ver_pr = status[0].comment.pull_request_version_id
597 _ver_pr = status[0].comment.pull_request_version_id
598 c.review_versions[_ver_pr] = status[0]
598 c.review_versions[_ver_pr] = status[0]
599
599
600 return self._get_template_context(c)
600 return self._get_template_context(c)
601
601
602 def assure_not_empty_repo(self):
602 def assure_not_empty_repo(self):
603 _ = self.request.translate
603 _ = self.request.translate
604
604
605 try:
605 try:
606 self.db_repo.scm_instance().get_commit()
606 self.db_repo.scm_instance().get_commit()
607 except EmptyRepositoryError:
607 except EmptyRepositoryError:
608 h.flash(h.literal(_('There are no commits yet')),
608 h.flash(h.literal(_('There are no commits yet')),
609 category='warning')
609 category='warning')
610 raise HTTPFound(
610 raise HTTPFound(
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
612
612
613 @LoginRequired()
613 @LoginRequired()
614 @NotAnonymous()
614 @NotAnonymous()
615 @HasRepoPermissionAnyDecorator(
615 @HasRepoPermissionAnyDecorator(
616 'repository.read', 'repository.write', 'repository.admin')
616 'repository.read', 'repository.write', 'repository.admin')
617 @view_config(
617 @view_config(
618 route_name='pullrequest_new', request_method='GET',
618 route_name='pullrequest_new', request_method='GET',
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
620 def pull_request_new(self):
620 def pull_request_new(self):
621 _ = self.request.translate
621 _ = self.request.translate
622 c = self.load_default_context()
622 c = self.load_default_context()
623
623
624 self.assure_not_empty_repo()
624 self.assure_not_empty_repo()
625 source_repo = self.db_repo
625 source_repo = self.db_repo
626
626
627 commit_id = self.request.GET.get('commit')
627 commit_id = self.request.GET.get('commit')
628 branch_ref = self.request.GET.get('branch')
628 branch_ref = self.request.GET.get('branch')
629 bookmark_ref = self.request.GET.get('bookmark')
629 bookmark_ref = self.request.GET.get('bookmark')
630
630
631 try:
631 try:
632 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo_data = PullRequestModel().generate_repo_data(
633 source_repo, commit_id=commit_id,
633 source_repo, commit_id=commit_id,
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
635 except CommitDoesNotExistError as e:
635 except CommitDoesNotExistError as e:
636 log.exception(e)
636 log.exception(e)
637 h.flash(_('Commit does not exist'), 'error')
637 h.flash(_('Commit does not exist'), 'error')
638 raise HTTPFound(
638 raise HTTPFound(
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
640
640
641 default_target_repo = source_repo
641 default_target_repo = source_repo
642
642
643 if source_repo.parent:
643 if source_repo.parent:
644 parent_vcs_obj = source_repo.parent.scm_instance()
644 parent_vcs_obj = source_repo.parent.scm_instance()
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 if parent_vcs_obj and not parent_vcs_obj.is_empty():
646 # change default if we have a parent repo
646 # change default if we have a parent repo
647 default_target_repo = source_repo.parent
647 default_target_repo = source_repo.parent
648
648
649 target_repo_data = PullRequestModel().generate_repo_data(
649 target_repo_data = PullRequestModel().generate_repo_data(
650 default_target_repo, translator=self.request.translate)
650 default_target_repo, translator=self.request.translate)
651
651
652 selected_source_ref = source_repo_data['refs']['selected_ref']
652 selected_source_ref = source_repo_data['refs']['selected_ref']
653
653
654 title_source_ref = selected_source_ref.split(':', 2)[1]
654 title_source_ref = selected_source_ref.split(':', 2)[1]
655 c.default_title = PullRequestModel().generate_pullrequest_title(
655 c.default_title = PullRequestModel().generate_pullrequest_title(
656 source=source_repo.repo_name,
656 source=source_repo.repo_name,
657 source_ref=title_source_ref,
657 source_ref=title_source_ref,
658 target=default_target_repo.repo_name
658 target=default_target_repo.repo_name
659 )
659 )
660
660
661 c.default_repo_data = {
661 c.default_repo_data = {
662 'source_repo_name': source_repo.repo_name,
662 'source_repo_name': source_repo.repo_name,
663 'source_refs_json': json.dumps(source_repo_data),
663 'source_refs_json': json.dumps(source_repo_data),
664 'target_repo_name': default_target_repo.repo_name,
664 'target_repo_name': default_target_repo.repo_name,
665 'target_refs_json': json.dumps(target_repo_data),
665 'target_refs_json': json.dumps(target_repo_data),
666 }
666 }
667 c.default_source_ref = selected_source_ref
667 c.default_source_ref = selected_source_ref
668
668
669 return self._get_template_context(c)
669 return self._get_template_context(c)
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator(
673 @HasRepoPermissionAnyDecorator(
674 'repository.read', 'repository.write', 'repository.admin')
674 'repository.read', 'repository.write', 'repository.admin')
675 @view_config(
675 @view_config(
676 route_name='pullrequest_repo_refs', request_method='GET',
676 route_name='pullrequest_repo_refs', request_method='GET',
677 renderer='json_ext', xhr=True)
677 renderer='json_ext', xhr=True)
678 def pull_request_repo_refs(self):
678 def pull_request_repo_refs(self):
679 self.load_default_context()
679 self.load_default_context()
680 target_repo_name = self.request.matchdict['target_repo_name']
680 target_repo_name = self.request.matchdict['target_repo_name']
681 repo = Repository.get_by_repo_name(target_repo_name)
681 repo = Repository.get_by_repo_name(target_repo_name)
682 if not repo:
682 if not repo:
683 raise HTTPNotFound()
683 raise HTTPNotFound()
684
685 target_perm = HasRepoPermissionAny(
686 'repository.read', 'repository.write', 'repository.admin')(
687 target_repo_name)
688 if not target_perm:
689 raise HTTPNotFound()
690
684 return PullRequestModel().generate_repo_data(
691 return PullRequestModel().generate_repo_data(
685 repo, translator=self.request.translate)
692 repo, translator=self.request.translate)
686
693
687 @LoginRequired()
694 @LoginRequired()
688 @NotAnonymous()
695 @NotAnonymous()
689 @HasRepoPermissionAnyDecorator(
696 @HasRepoPermissionAnyDecorator(
690 'repository.read', 'repository.write', 'repository.admin')
697 'repository.read', 'repository.write', 'repository.admin')
691 @view_config(
698 @view_config(
692 route_name='pullrequest_repo_destinations', request_method='GET',
699 route_name='pullrequest_repo_destinations', request_method='GET',
693 renderer='json_ext', xhr=True)
700 renderer='json_ext', xhr=True)
694 def pull_request_repo_destinations(self):
701 def pull_request_repo_destinations(self):
695 _ = self.request.translate
702 _ = self.request.translate
696 filter_query = self.request.GET.get('query')
703 filter_query = self.request.GET.get('query')
697
704
698 query = Repository.query() \
705 query = Repository.query() \
699 .order_by(func.length(Repository.repo_name)) \
706 .order_by(func.length(Repository.repo_name)) \
700 .filter(
707 .filter(
701 or_(Repository.repo_name == self.db_repo.repo_name,
708 or_(Repository.repo_name == self.db_repo.repo_name,
702 Repository.fork_id == self.db_repo.repo_id))
709 Repository.fork_id == self.db_repo.repo_id))
703
710
704 if filter_query:
711 if filter_query:
705 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
712 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
706 query = query.filter(
713 query = query.filter(
707 Repository.repo_name.ilike(ilike_expression))
714 Repository.repo_name.ilike(ilike_expression))
708
715
709 add_parent = False
716 add_parent = False
710 if self.db_repo.parent:
717 if self.db_repo.parent:
711 if filter_query in self.db_repo.parent.repo_name:
718 if filter_query in self.db_repo.parent.repo_name:
712 parent_vcs_obj = self.db_repo.parent.scm_instance()
719 parent_vcs_obj = self.db_repo.parent.scm_instance()
713 if parent_vcs_obj and not parent_vcs_obj.is_empty():
720 if parent_vcs_obj and not parent_vcs_obj.is_empty():
714 add_parent = True
721 add_parent = True
715
722
716 limit = 20 - 1 if add_parent else 20
723 limit = 20 - 1 if add_parent else 20
717 all_repos = query.limit(limit).all()
724 all_repos = query.limit(limit).all()
718 if add_parent:
725 if add_parent:
719 all_repos += [self.db_repo.parent]
726 all_repos += [self.db_repo.parent]
720
727
721 repos = []
728 repos = []
722 for obj in ScmModel().get_repos(all_repos):
729 for obj in ScmModel().get_repos(all_repos):
723 repos.append({
730 repos.append({
724 'id': obj['name'],
731 'id': obj['name'],
725 'text': obj['name'],
732 'text': obj['name'],
726 'type': 'repo',
733 'type': 'repo',
727 'obj': obj['dbrepo']
734 'obj': obj['dbrepo']
728 })
735 })
729
736
730 data = {
737 data = {
731 'more': False,
738 'more': False,
732 'results': [{
739 'results': [{
733 'text': _('Repositories'),
740 'text': _('Repositories'),
734 'children': repos
741 'children': repos
735 }] if repos else []
742 }] if repos else []
736 }
743 }
737 return data
744 return data
738
745
739 @LoginRequired()
746 @LoginRequired()
740 @NotAnonymous()
747 @NotAnonymous()
741 @HasRepoPermissionAnyDecorator(
748 @HasRepoPermissionAnyDecorator(
742 'repository.read', 'repository.write', 'repository.admin')
749 'repository.read', 'repository.write', 'repository.admin')
743 @CSRFRequired()
750 @CSRFRequired()
744 @view_config(
751 @view_config(
745 route_name='pullrequest_create', request_method='POST',
752 route_name='pullrequest_create', request_method='POST',
746 renderer=None)
753 renderer=None)
747 def pull_request_create(self):
754 def pull_request_create(self):
748 _ = self.request.translate
755 _ = self.request.translate
749 self.assure_not_empty_repo()
756 self.assure_not_empty_repo()
750 self.load_default_context()
757 self.load_default_context()
751
758
752 controls = peppercorn.parse(self.request.POST.items())
759 controls = peppercorn.parse(self.request.POST.items())
753
760
754 try:
761 try:
755 form = PullRequestForm(
762 form = PullRequestForm(
756 self.request.translate, self.db_repo.repo_id)()
763 self.request.translate, self.db_repo.repo_id)()
757 _form = form.to_python(controls)
764 _form = form.to_python(controls)
758 except formencode.Invalid as errors:
765 except formencode.Invalid as errors:
759 if errors.error_dict.get('revisions'):
766 if errors.error_dict.get('revisions'):
760 msg = 'Revisions: %s' % errors.error_dict['revisions']
767 msg = 'Revisions: %s' % errors.error_dict['revisions']
761 elif errors.error_dict.get('pullrequest_title'):
768 elif errors.error_dict.get('pullrequest_title'):
762 msg = _('Pull request requires a title with min. 3 chars')
769 msg = _('Pull request requires a title with min. 3 chars')
763 else:
770 else:
764 msg = _('Error creating pull request: {}').format(errors)
771 msg = _('Error creating pull request: {}').format(errors)
765 log.exception(msg)
772 log.exception(msg)
766 h.flash(msg, 'error')
773 h.flash(msg, 'error')
767
774
768 # would rather just go back to form ...
775 # would rather just go back to form ...
769 raise HTTPFound(
776 raise HTTPFound(
770 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
777 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
771
778
772 source_repo = _form['source_repo']
779 source_repo = _form['source_repo']
773 source_ref = _form['source_ref']
780 source_ref = _form['source_ref']
774 target_repo = _form['target_repo']
781 target_repo = _form['target_repo']
775 target_ref = _form['target_ref']
782 target_ref = _form['target_ref']
776 commit_ids = _form['revisions'][::-1]
783 commit_ids = _form['revisions'][::-1]
777
784
778 # find the ancestor for this pr
785 # find the ancestor for this pr
779 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
786 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
780 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
787 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
781
788
782 # re-check permissions again here
789 # re-check permissions again here
783 # source_repo we must have read permissions
790 # source_repo we must have read permissions
784
791
785 source_perm = HasRepoPermissionAny(
792 source_perm = HasRepoPermissionAny(
786 'repository.read',
793 'repository.read',
787 'repository.write', 'repository.admin')(source_db_repo.repo_name)
794 'repository.write', 'repository.admin')(source_db_repo.repo_name)
788 if not source_perm:
795 if not source_perm:
789 msg = _('Not Enough permissions to source repo `{}`.'.format(
796 msg = _('Not Enough permissions to source repo `{}`.'.format(
790 source_db_repo.repo_name))
797 source_db_repo.repo_name))
791 h.flash(msg, category='error')
798 h.flash(msg, category='error')
792 # copy the args back to redirect
799 # copy the args back to redirect
793 org_query = self.request.GET.mixed()
800 org_query = self.request.GET.mixed()
794 raise HTTPFound(
801 raise HTTPFound(
795 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
802 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
796 _query=org_query))
803 _query=org_query))
797
804
798 # target repo we must have read permissions, and also later on
805 # target repo we must have read permissions, and also later on
799 # we want to check branch permissions here
806 # we want to check branch permissions here
800 target_perm = HasRepoPermissionAny(
807 target_perm = HasRepoPermissionAny(
801 'repository.read',
808 'repository.read',
802 'repository.write', 'repository.admin')(target_db_repo.repo_name)
809 'repository.write', 'repository.admin')(target_db_repo.repo_name)
803 if not target_perm:
810 if not target_perm:
804 msg = _('Not Enough permissions to target repo `{}`.'.format(
811 msg = _('Not Enough permissions to target repo `{}`.'.format(
805 target_db_repo.repo_name))
812 target_db_repo.repo_name))
806 h.flash(msg, category='error')
813 h.flash(msg, category='error')
807 # copy the args back to redirect
814 # copy the args back to redirect
808 org_query = self.request.GET.mixed()
815 org_query = self.request.GET.mixed()
809 raise HTTPFound(
816 raise HTTPFound(
810 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
817 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
811 _query=org_query))
818 _query=org_query))
812
819
813 source_scm = source_db_repo.scm_instance()
820 source_scm = source_db_repo.scm_instance()
814 target_scm = target_db_repo.scm_instance()
821 target_scm = target_db_repo.scm_instance()
815
822
816 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
823 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
817 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
824 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
818
825
819 ancestor = source_scm.get_common_ancestor(
826 ancestor = source_scm.get_common_ancestor(
820 source_commit.raw_id, target_commit.raw_id, target_scm)
827 source_commit.raw_id, target_commit.raw_id, target_scm)
821
828
822 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
829 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
823 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
830 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
824
831
825 pullrequest_title = _form['pullrequest_title']
832 pullrequest_title = _form['pullrequest_title']
826 title_source_ref = source_ref.split(':', 2)[1]
833 title_source_ref = source_ref.split(':', 2)[1]
827 if not pullrequest_title:
834 if not pullrequest_title:
828 pullrequest_title = PullRequestModel().generate_pullrequest_title(
835 pullrequest_title = PullRequestModel().generate_pullrequest_title(
829 source=source_repo,
836 source=source_repo,
830 source_ref=title_source_ref,
837 source_ref=title_source_ref,
831 target=target_repo
838 target=target_repo
832 )
839 )
833
840
834 description = _form['pullrequest_desc']
841 description = _form['pullrequest_desc']
835
842
836 get_default_reviewers_data, validate_default_reviewers = \
843 get_default_reviewers_data, validate_default_reviewers = \
837 PullRequestModel().get_reviewer_functions()
844 PullRequestModel().get_reviewer_functions()
838
845
839 # recalculate reviewers logic, to make sure we can validate this
846 # recalculate reviewers logic, to make sure we can validate this
840 reviewer_rules = get_default_reviewers_data(
847 reviewer_rules = get_default_reviewers_data(
841 self._rhodecode_db_user, source_db_repo,
848 self._rhodecode_db_user, source_db_repo,
842 source_commit, target_db_repo, target_commit)
849 source_commit, target_db_repo, target_commit)
843
850
844 given_reviewers = _form['review_members']
851 given_reviewers = _form['review_members']
845 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
852 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
846
853
847 try:
854 try:
848 pull_request = PullRequestModel().create(
855 pull_request = PullRequestModel().create(
849 self._rhodecode_user.user_id, source_repo, source_ref,
856 self._rhodecode_user.user_id, source_repo, source_ref,
850 target_repo, target_ref, commit_ids, reviewers,
857 target_repo, target_ref, commit_ids, reviewers,
851 pullrequest_title, description, reviewer_rules
858 pullrequest_title, description, reviewer_rules
852 )
859 )
853 Session().commit()
860 Session().commit()
854
861
855 h.flash(_('Successfully opened new pull request'),
862 h.flash(_('Successfully opened new pull request'),
856 category='success')
863 category='success')
857 except Exception:
864 except Exception:
858 msg = _('Error occurred during creation of this pull request.')
865 msg = _('Error occurred during creation of this pull request.')
859 log.exception(msg)
866 log.exception(msg)
860 h.flash(msg, category='error')
867 h.flash(msg, category='error')
861
868
862 # copy the args back to redirect
869 # copy the args back to redirect
863 org_query = self.request.GET.mixed()
870 org_query = self.request.GET.mixed()
864 raise HTTPFound(
871 raise HTTPFound(
865 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
872 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
866 _query=org_query))
873 _query=org_query))
867
874
868 raise HTTPFound(
875 raise HTTPFound(
869 h.route_path('pullrequest_show', repo_name=target_repo,
876 h.route_path('pullrequest_show', repo_name=target_repo,
870 pull_request_id=pull_request.pull_request_id))
877 pull_request_id=pull_request.pull_request_id))
871
878
872 @LoginRequired()
879 @LoginRequired()
873 @NotAnonymous()
880 @NotAnonymous()
874 @HasRepoPermissionAnyDecorator(
881 @HasRepoPermissionAnyDecorator(
875 'repository.read', 'repository.write', 'repository.admin')
882 'repository.read', 'repository.write', 'repository.admin')
876 @CSRFRequired()
883 @CSRFRequired()
877 @view_config(
884 @view_config(
878 route_name='pullrequest_update', request_method='POST',
885 route_name='pullrequest_update', request_method='POST',
879 renderer='json_ext')
886 renderer='json_ext')
880 def pull_request_update(self):
887 def pull_request_update(self):
881 pull_request = PullRequest.get_or_404(
888 pull_request = PullRequest.get_or_404(
882 self.request.matchdict['pull_request_id'])
889 self.request.matchdict['pull_request_id'])
883
890
884 self.load_default_context()
891 self.load_default_context()
885 # only owner or admin can update it
892 # only owner or admin can update it
886 allowed_to_update = PullRequestModel().check_user_update(
893 allowed_to_update = PullRequestModel().check_user_update(
887 pull_request, self._rhodecode_user)
894 pull_request, self._rhodecode_user)
888 if allowed_to_update:
895 if allowed_to_update:
889 controls = peppercorn.parse(self.request.POST.items())
896 controls = peppercorn.parse(self.request.POST.items())
890
897
891 if 'review_members' in controls:
898 if 'review_members' in controls:
892 self._update_reviewers(
899 self._update_reviewers(
893 pull_request, controls['review_members'],
900 pull_request, controls['review_members'],
894 pull_request.reviewer_data)
901 pull_request.reviewer_data)
895 elif str2bool(self.request.POST.get('update_commits', 'false')):
902 elif str2bool(self.request.POST.get('update_commits', 'false')):
896 self._update_commits(pull_request)
903 self._update_commits(pull_request)
897 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
904 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
898 self._edit_pull_request(pull_request)
905 self._edit_pull_request(pull_request)
899 else:
906 else:
900 raise HTTPBadRequest()
907 raise HTTPBadRequest()
901 return True
908 return True
902 raise HTTPForbidden()
909 raise HTTPForbidden()
903
910
904 def _edit_pull_request(self, pull_request):
911 def _edit_pull_request(self, pull_request):
905 _ = self.request.translate
912 _ = self.request.translate
906 try:
913 try:
907 PullRequestModel().edit(
914 PullRequestModel().edit(
908 pull_request, self.request.POST.get('title'),
915 pull_request, self.request.POST.get('title'),
909 self.request.POST.get('description'), self._rhodecode_user)
916 self.request.POST.get('description'), self._rhodecode_user)
910 except ValueError:
917 except ValueError:
911 msg = _(u'Cannot update closed pull requests.')
918 msg = _(u'Cannot update closed pull requests.')
912 h.flash(msg, category='error')
919 h.flash(msg, category='error')
913 return
920 return
914 else:
921 else:
915 Session().commit()
922 Session().commit()
916
923
917 msg = _(u'Pull request title & description updated.')
924 msg = _(u'Pull request title & description updated.')
918 h.flash(msg, category='success')
925 h.flash(msg, category='success')
919 return
926 return
920
927
921 def _update_commits(self, pull_request):
928 def _update_commits(self, pull_request):
922 _ = self.request.translate
929 _ = self.request.translate
923 resp = PullRequestModel().update_commits(pull_request)
930 resp = PullRequestModel().update_commits(pull_request)
924
931
925 if resp.executed:
932 if resp.executed:
926
933
927 if resp.target_changed and resp.source_changed:
934 if resp.target_changed and resp.source_changed:
928 changed = 'target and source repositories'
935 changed = 'target and source repositories'
929 elif resp.target_changed and not resp.source_changed:
936 elif resp.target_changed and not resp.source_changed:
930 changed = 'target repository'
937 changed = 'target repository'
931 elif not resp.target_changed and resp.source_changed:
938 elif not resp.target_changed and resp.source_changed:
932 changed = 'source repository'
939 changed = 'source repository'
933 else:
940 else:
934 changed = 'nothing'
941 changed = 'nothing'
935
942
936 msg = _(
943 msg = _(
937 u'Pull request updated to "{source_commit_id}" with '
944 u'Pull request updated to "{source_commit_id}" with '
938 u'{count_added} added, {count_removed} removed commits. '
945 u'{count_added} added, {count_removed} removed commits. '
939 u'Source of changes: {change_source}')
946 u'Source of changes: {change_source}')
940 msg = msg.format(
947 msg = msg.format(
941 source_commit_id=pull_request.source_ref_parts.commit_id,
948 source_commit_id=pull_request.source_ref_parts.commit_id,
942 count_added=len(resp.changes.added),
949 count_added=len(resp.changes.added),
943 count_removed=len(resp.changes.removed),
950 count_removed=len(resp.changes.removed),
944 change_source=changed)
951 change_source=changed)
945 h.flash(msg, category='success')
952 h.flash(msg, category='success')
946
953
947 channel = '/repo${}$/pr/{}'.format(
954 channel = '/repo${}$/pr/{}'.format(
948 pull_request.target_repo.repo_name,
955 pull_request.target_repo.repo_name,
949 pull_request.pull_request_id)
956 pull_request.pull_request_id)
950 message = msg + (
957 message = msg + (
951 ' - <a onclick="window.location.reload()">'
958 ' - <a onclick="window.location.reload()">'
952 '<strong>{}</strong></a>'.format(_('Reload page')))
959 '<strong>{}</strong></a>'.format(_('Reload page')))
953 channelstream.post_message(
960 channelstream.post_message(
954 channel, message, self._rhodecode_user.username,
961 channel, message, self._rhodecode_user.username,
955 registry=self.request.registry)
962 registry=self.request.registry)
956 else:
963 else:
957 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
964 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
958 warning_reasons = [
965 warning_reasons = [
959 UpdateFailureReason.NO_CHANGE,
966 UpdateFailureReason.NO_CHANGE,
960 UpdateFailureReason.WRONG_REF_TYPE,
967 UpdateFailureReason.WRONG_REF_TYPE,
961 ]
968 ]
962 category = 'warning' if resp.reason in warning_reasons else 'error'
969 category = 'warning' if resp.reason in warning_reasons else 'error'
963 h.flash(msg, category=category)
970 h.flash(msg, category=category)
964
971
965 @LoginRequired()
972 @LoginRequired()
966 @NotAnonymous()
973 @NotAnonymous()
967 @HasRepoPermissionAnyDecorator(
974 @HasRepoPermissionAnyDecorator(
968 'repository.read', 'repository.write', 'repository.admin')
975 'repository.read', 'repository.write', 'repository.admin')
969 @CSRFRequired()
976 @CSRFRequired()
970 @view_config(
977 @view_config(
971 route_name='pullrequest_merge', request_method='POST',
978 route_name='pullrequest_merge', request_method='POST',
972 renderer='json_ext')
979 renderer='json_ext')
973 def pull_request_merge(self):
980 def pull_request_merge(self):
974 """
981 """
975 Merge will perform a server-side merge of the specified
982 Merge will perform a server-side merge of the specified
976 pull request, if the pull request is approved and mergeable.
983 pull request, if the pull request is approved and mergeable.
977 After successful merging, the pull request is automatically
984 After successful merging, the pull request is automatically
978 closed, with a relevant comment.
985 closed, with a relevant comment.
979 """
986 """
980 pull_request = PullRequest.get_or_404(
987 pull_request = PullRequest.get_or_404(
981 self.request.matchdict['pull_request_id'])
988 self.request.matchdict['pull_request_id'])
982
989
983 self.load_default_context()
990 self.load_default_context()
984 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
991 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
985 translator=self.request.translate)
992 translator=self.request.translate)
986 merge_possible = not check.failed
993 merge_possible = not check.failed
987
994
988 for err_type, error_msg in check.errors:
995 for err_type, error_msg in check.errors:
989 h.flash(error_msg, category=err_type)
996 h.flash(error_msg, category=err_type)
990
997
991 if merge_possible:
998 if merge_possible:
992 log.debug("Pre-conditions checked, trying to merge.")
999 log.debug("Pre-conditions checked, trying to merge.")
993 extras = vcs_operation_context(
1000 extras = vcs_operation_context(
994 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1001 self.request.environ, repo_name=pull_request.target_repo.repo_name,
995 username=self._rhodecode_db_user.username, action='push',
1002 username=self._rhodecode_db_user.username, action='push',
996 scm=pull_request.target_repo.repo_type)
1003 scm=pull_request.target_repo.repo_type)
997 self._merge_pull_request(
1004 self._merge_pull_request(
998 pull_request, self._rhodecode_db_user, extras)
1005 pull_request, self._rhodecode_db_user, extras)
999 else:
1006 else:
1000 log.debug("Pre-conditions failed, NOT merging.")
1007 log.debug("Pre-conditions failed, NOT merging.")
1001
1008
1002 raise HTTPFound(
1009 raise HTTPFound(
1003 h.route_path('pullrequest_show',
1010 h.route_path('pullrequest_show',
1004 repo_name=pull_request.target_repo.repo_name,
1011 repo_name=pull_request.target_repo.repo_name,
1005 pull_request_id=pull_request.pull_request_id))
1012 pull_request_id=pull_request.pull_request_id))
1006
1013
1007 def _merge_pull_request(self, pull_request, user, extras):
1014 def _merge_pull_request(self, pull_request, user, extras):
1008 _ = self.request.translate
1015 _ = self.request.translate
1009 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1016 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1010
1017
1011 if merge_resp.executed:
1018 if merge_resp.executed:
1012 log.debug("The merge was successful, closing the pull request.")
1019 log.debug("The merge was successful, closing the pull request.")
1013 PullRequestModel().close_pull_request(
1020 PullRequestModel().close_pull_request(
1014 pull_request.pull_request_id, user)
1021 pull_request.pull_request_id, user)
1015 Session().commit()
1022 Session().commit()
1016 msg = _('Pull request was successfully merged and closed.')
1023 msg = _('Pull request was successfully merged and closed.')
1017 h.flash(msg, category='success')
1024 h.flash(msg, category='success')
1018 else:
1025 else:
1019 log.debug(
1026 log.debug(
1020 "The merge was not successful. Merge response: %s",
1027 "The merge was not successful. Merge response: %s",
1021 merge_resp)
1028 merge_resp)
1022 msg = PullRequestModel().merge_status_message(
1029 msg = PullRequestModel().merge_status_message(
1023 merge_resp.failure_reason)
1030 merge_resp.failure_reason)
1024 h.flash(msg, category='error')
1031 h.flash(msg, category='error')
1025
1032
1026 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1033 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1027 _ = self.request.translate
1034 _ = self.request.translate
1028 get_default_reviewers_data, validate_default_reviewers = \
1035 get_default_reviewers_data, validate_default_reviewers = \
1029 PullRequestModel().get_reviewer_functions()
1036 PullRequestModel().get_reviewer_functions()
1030
1037
1031 try:
1038 try:
1032 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1039 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1033 except ValueError as e:
1040 except ValueError as e:
1034 log.error('Reviewers Validation: {}'.format(e))
1041 log.error('Reviewers Validation: {}'.format(e))
1035 h.flash(e, category='error')
1042 h.flash(e, category='error')
1036 return
1043 return
1037
1044
1038 PullRequestModel().update_reviewers(
1045 PullRequestModel().update_reviewers(
1039 pull_request, reviewers, self._rhodecode_user)
1046 pull_request, reviewers, self._rhodecode_user)
1040 h.flash(_('Pull request reviewers updated.'), category='success')
1047 h.flash(_('Pull request reviewers updated.'), category='success')
1041 Session().commit()
1048 Session().commit()
1042
1049
1043 @LoginRequired()
1050 @LoginRequired()
1044 @NotAnonymous()
1051 @NotAnonymous()
1045 @HasRepoPermissionAnyDecorator(
1052 @HasRepoPermissionAnyDecorator(
1046 'repository.read', 'repository.write', 'repository.admin')
1053 'repository.read', 'repository.write', 'repository.admin')
1047 @CSRFRequired()
1054 @CSRFRequired()
1048 @view_config(
1055 @view_config(
1049 route_name='pullrequest_delete', request_method='POST',
1056 route_name='pullrequest_delete', request_method='POST',
1050 renderer='json_ext')
1057 renderer='json_ext')
1051 def pull_request_delete(self):
1058 def pull_request_delete(self):
1052 _ = self.request.translate
1059 _ = self.request.translate
1053
1060
1054 pull_request = PullRequest.get_or_404(
1061 pull_request = PullRequest.get_or_404(
1055 self.request.matchdict['pull_request_id'])
1062 self.request.matchdict['pull_request_id'])
1056 self.load_default_context()
1063 self.load_default_context()
1057
1064
1058 pr_closed = pull_request.is_closed()
1065 pr_closed = pull_request.is_closed()
1059 allowed_to_delete = PullRequestModel().check_user_delete(
1066 allowed_to_delete = PullRequestModel().check_user_delete(
1060 pull_request, self._rhodecode_user) and not pr_closed
1067 pull_request, self._rhodecode_user) and not pr_closed
1061
1068
1062 # only owner can delete it !
1069 # only owner can delete it !
1063 if allowed_to_delete:
1070 if allowed_to_delete:
1064 PullRequestModel().delete(pull_request, self._rhodecode_user)
1071 PullRequestModel().delete(pull_request, self._rhodecode_user)
1065 Session().commit()
1072 Session().commit()
1066 h.flash(_('Successfully deleted pull request'),
1073 h.flash(_('Successfully deleted pull request'),
1067 category='success')
1074 category='success')
1068 raise HTTPFound(h.route_path('pullrequest_show_all',
1075 raise HTTPFound(h.route_path('pullrequest_show_all',
1069 repo_name=self.db_repo_name))
1076 repo_name=self.db_repo_name))
1070
1077
1071 log.warning('user %s tried to delete pull request without access',
1078 log.warning('user %s tried to delete pull request without access',
1072 self._rhodecode_user)
1079 self._rhodecode_user)
1073 raise HTTPNotFound()
1080 raise HTTPNotFound()
1074
1081
1075 @LoginRequired()
1082 @LoginRequired()
1076 @NotAnonymous()
1083 @NotAnonymous()
1077 @HasRepoPermissionAnyDecorator(
1084 @HasRepoPermissionAnyDecorator(
1078 'repository.read', 'repository.write', 'repository.admin')
1085 'repository.read', 'repository.write', 'repository.admin')
1079 @CSRFRequired()
1086 @CSRFRequired()
1080 @view_config(
1087 @view_config(
1081 route_name='pullrequest_comment_create', request_method='POST',
1088 route_name='pullrequest_comment_create', request_method='POST',
1082 renderer='json_ext')
1089 renderer='json_ext')
1083 def pull_request_comment_create(self):
1090 def pull_request_comment_create(self):
1084 _ = self.request.translate
1091 _ = self.request.translate
1085
1092
1086 pull_request = PullRequest.get_or_404(
1093 pull_request = PullRequest.get_or_404(
1087 self.request.matchdict['pull_request_id'])
1094 self.request.matchdict['pull_request_id'])
1088 pull_request_id = pull_request.pull_request_id
1095 pull_request_id = pull_request.pull_request_id
1089
1096
1090 if pull_request.is_closed():
1097 if pull_request.is_closed():
1091 log.debug('comment: forbidden because pull request is closed')
1098 log.debug('comment: forbidden because pull request is closed')
1092 raise HTTPForbidden()
1099 raise HTTPForbidden()
1093
1100
1094 allowed_to_comment = PullRequestModel().check_user_comment(
1101 allowed_to_comment = PullRequestModel().check_user_comment(
1095 pull_request, self._rhodecode_user)
1102 pull_request, self._rhodecode_user)
1096 if not allowed_to_comment:
1103 if not allowed_to_comment:
1097 log.debug(
1104 log.debug(
1098 'comment: forbidden because pull request is from forbidden repo')
1105 'comment: forbidden because pull request is from forbidden repo')
1099 raise HTTPForbidden()
1106 raise HTTPForbidden()
1100
1107
1101 c = self.load_default_context()
1108 c = self.load_default_context()
1102
1109
1103 status = self.request.POST.get('changeset_status', None)
1110 status = self.request.POST.get('changeset_status', None)
1104 text = self.request.POST.get('text')
1111 text = self.request.POST.get('text')
1105 comment_type = self.request.POST.get('comment_type')
1112 comment_type = self.request.POST.get('comment_type')
1106 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1113 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1107 close_pull_request = self.request.POST.get('close_pull_request')
1114 close_pull_request = self.request.POST.get('close_pull_request')
1108
1115
1109 # the logic here should work like following, if we submit close
1116 # the logic here should work like following, if we submit close
1110 # pr comment, use `close_pull_request_with_comment` function
1117 # pr comment, use `close_pull_request_with_comment` function
1111 # else handle regular comment logic
1118 # else handle regular comment logic
1112
1119
1113 if close_pull_request:
1120 if close_pull_request:
1114 # only owner or admin or person with write permissions
1121 # only owner or admin or person with write permissions
1115 allowed_to_close = PullRequestModel().check_user_update(
1122 allowed_to_close = PullRequestModel().check_user_update(
1116 pull_request, self._rhodecode_user)
1123 pull_request, self._rhodecode_user)
1117 if not allowed_to_close:
1124 if not allowed_to_close:
1118 log.debug('comment: forbidden because not allowed to close '
1125 log.debug('comment: forbidden because not allowed to close '
1119 'pull request %s', pull_request_id)
1126 'pull request %s', pull_request_id)
1120 raise HTTPForbidden()
1127 raise HTTPForbidden()
1121 comment, status = PullRequestModel().close_pull_request_with_comment(
1128 comment, status = PullRequestModel().close_pull_request_with_comment(
1122 pull_request, self._rhodecode_user, self.db_repo, message=text)
1129 pull_request, self._rhodecode_user, self.db_repo, message=text)
1123 Session().flush()
1130 Session().flush()
1124 events.trigger(
1131 events.trigger(
1125 events.PullRequestCommentEvent(pull_request, comment))
1132 events.PullRequestCommentEvent(pull_request, comment))
1126
1133
1127 else:
1134 else:
1128 # regular comment case, could be inline, or one with status.
1135 # regular comment case, could be inline, or one with status.
1129 # for that one we check also permissions
1136 # for that one we check also permissions
1130
1137
1131 allowed_to_change_status = PullRequestModel().check_user_change_status(
1138 allowed_to_change_status = PullRequestModel().check_user_change_status(
1132 pull_request, self._rhodecode_user)
1139 pull_request, self._rhodecode_user)
1133
1140
1134 if status and allowed_to_change_status:
1141 if status and allowed_to_change_status:
1135 message = (_('Status change %(transition_icon)s %(status)s')
1142 message = (_('Status change %(transition_icon)s %(status)s')
1136 % {'transition_icon': '>',
1143 % {'transition_icon': '>',
1137 'status': ChangesetStatus.get_status_lbl(status)})
1144 'status': ChangesetStatus.get_status_lbl(status)})
1138 text = text or message
1145 text = text or message
1139
1146
1140 comment = CommentsModel().create(
1147 comment = CommentsModel().create(
1141 text=text,
1148 text=text,
1142 repo=self.db_repo.repo_id,
1149 repo=self.db_repo.repo_id,
1143 user=self._rhodecode_user.user_id,
1150 user=self._rhodecode_user.user_id,
1144 pull_request=pull_request,
1151 pull_request=pull_request,
1145 f_path=self.request.POST.get('f_path'),
1152 f_path=self.request.POST.get('f_path'),
1146 line_no=self.request.POST.get('line'),
1153 line_no=self.request.POST.get('line'),
1147 status_change=(ChangesetStatus.get_status_lbl(status)
1154 status_change=(ChangesetStatus.get_status_lbl(status)
1148 if status and allowed_to_change_status else None),
1155 if status and allowed_to_change_status else None),
1149 status_change_type=(status
1156 status_change_type=(status
1150 if status and allowed_to_change_status else None),
1157 if status and allowed_to_change_status else None),
1151 comment_type=comment_type,
1158 comment_type=comment_type,
1152 resolves_comment_id=resolves_comment_id
1159 resolves_comment_id=resolves_comment_id
1153 )
1160 )
1154
1161
1155 if allowed_to_change_status:
1162 if allowed_to_change_status:
1156 # calculate old status before we change it
1163 # calculate old status before we change it
1157 old_calculated_status = pull_request.calculated_review_status()
1164 old_calculated_status = pull_request.calculated_review_status()
1158
1165
1159 # get status if set !
1166 # get status if set !
1160 if status:
1167 if status:
1161 ChangesetStatusModel().set_status(
1168 ChangesetStatusModel().set_status(
1162 self.db_repo.repo_id,
1169 self.db_repo.repo_id,
1163 status,
1170 status,
1164 self._rhodecode_user.user_id,
1171 self._rhodecode_user.user_id,
1165 comment,
1172 comment,
1166 pull_request=pull_request
1173 pull_request=pull_request
1167 )
1174 )
1168
1175
1169 Session().flush()
1176 Session().flush()
1170 events.trigger(
1177 events.trigger(
1171 events.PullRequestCommentEvent(pull_request, comment))
1178 events.PullRequestCommentEvent(pull_request, comment))
1172
1179
1173 # we now calculate the status of pull request, and based on that
1180 # we now calculate the status of pull request, and based on that
1174 # calculation we set the commits status
1181 # calculation we set the commits status
1175 calculated_status = pull_request.calculated_review_status()
1182 calculated_status = pull_request.calculated_review_status()
1176 if old_calculated_status != calculated_status:
1183 if old_calculated_status != calculated_status:
1177 PullRequestModel()._trigger_pull_request_hook(
1184 PullRequestModel()._trigger_pull_request_hook(
1178 pull_request, self._rhodecode_user, 'review_status_change')
1185 pull_request, self._rhodecode_user, 'review_status_change')
1179
1186
1180 Session().commit()
1187 Session().commit()
1181
1188
1182 data = {
1189 data = {
1183 'target_id': h.safeid(h.safe_unicode(
1190 'target_id': h.safeid(h.safe_unicode(
1184 self.request.POST.get('f_path'))),
1191 self.request.POST.get('f_path'))),
1185 }
1192 }
1186 if comment:
1193 if comment:
1187 c.co = comment
1194 c.co = comment
1188 rendered_comment = render(
1195 rendered_comment = render(
1189 'rhodecode:templates/changeset/changeset_comment_block.mako',
1196 'rhodecode:templates/changeset/changeset_comment_block.mako',
1190 self._get_template_context(c), self.request)
1197 self._get_template_context(c), self.request)
1191
1198
1192 data.update(comment.get_dict())
1199 data.update(comment.get_dict())
1193 data.update({'rendered_text': rendered_comment})
1200 data.update({'rendered_text': rendered_comment})
1194
1201
1195 return data
1202 return data
1196
1203
1197 @LoginRequired()
1204 @LoginRequired()
1198 @NotAnonymous()
1205 @NotAnonymous()
1199 @HasRepoPermissionAnyDecorator(
1206 @HasRepoPermissionAnyDecorator(
1200 'repository.read', 'repository.write', 'repository.admin')
1207 'repository.read', 'repository.write', 'repository.admin')
1201 @CSRFRequired()
1208 @CSRFRequired()
1202 @view_config(
1209 @view_config(
1203 route_name='pullrequest_comment_delete', request_method='POST',
1210 route_name='pullrequest_comment_delete', request_method='POST',
1204 renderer='json_ext')
1211 renderer='json_ext')
1205 def pull_request_comment_delete(self):
1212 def pull_request_comment_delete(self):
1206 pull_request = PullRequest.get_or_404(
1213 pull_request = PullRequest.get_or_404(
1207 self.request.matchdict['pull_request_id'])
1214 self.request.matchdict['pull_request_id'])
1208
1215
1209 comment = ChangesetComment.get_or_404(
1216 comment = ChangesetComment.get_or_404(
1210 self.request.matchdict['comment_id'])
1217 self.request.matchdict['comment_id'])
1211 comment_id = comment.comment_id
1218 comment_id = comment.comment_id
1212
1219
1213 if pull_request.is_closed():
1220 if pull_request.is_closed():
1214 log.debug('comment: forbidden because pull request is closed')
1221 log.debug('comment: forbidden because pull request is closed')
1215 raise HTTPForbidden()
1222 raise HTTPForbidden()
1216
1223
1217 if not comment:
1224 if not comment:
1218 log.debug('Comment with id:%s not found, skipping', comment_id)
1225 log.debug('Comment with id:%s not found, skipping', comment_id)
1219 # comment already deleted in another call probably
1226 # comment already deleted in another call probably
1220 return True
1227 return True
1221
1228
1222 if comment.pull_request.is_closed():
1229 if comment.pull_request.is_closed():
1223 # don't allow deleting comments on closed pull request
1230 # don't allow deleting comments on closed pull request
1224 raise HTTPForbidden()
1231 raise HTTPForbidden()
1225
1232
1226 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1233 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1227 super_admin = h.HasPermissionAny('hg.admin')()
1234 super_admin = h.HasPermissionAny('hg.admin')()
1228 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1235 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1229 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1236 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1230 comment_repo_admin = is_repo_admin and is_repo_comment
1237 comment_repo_admin = is_repo_admin and is_repo_comment
1231
1238
1232 if super_admin or comment_owner or comment_repo_admin:
1239 if super_admin or comment_owner or comment_repo_admin:
1233 old_calculated_status = comment.pull_request.calculated_review_status()
1240 old_calculated_status = comment.pull_request.calculated_review_status()
1234 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1241 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1235 Session().commit()
1242 Session().commit()
1236 calculated_status = comment.pull_request.calculated_review_status()
1243 calculated_status = comment.pull_request.calculated_review_status()
1237 if old_calculated_status != calculated_status:
1244 if old_calculated_status != calculated_status:
1238 PullRequestModel()._trigger_pull_request_hook(
1245 PullRequestModel()._trigger_pull_request_hook(
1239 comment.pull_request, self._rhodecode_user, 'review_status_change')
1246 comment.pull_request, self._rhodecode_user, 'review_status_change')
1240 return True
1247 return True
1241 else:
1248 else:
1242 log.warning('No permissions for user %s to delete comment_id: %s',
1249 log.warning('No permissions for user %s to delete comment_id: %s',
1243 self._rhodecode_db_user, comment_id)
1250 self._rhodecode_db_user, comment_id)
1244 raise HTTPNotFound()
1251 raise HTTPNotFound()
General Comments 0
You need to be logged in to leave comments. Login now