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