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