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