##// END OF EJS Templates
commits: detect missing commits on diffsets from new PR ancestor logic....
marcink -
r4400:96b3ab25 stable
parent child Browse files
Show More
@@ -1,1520 +1,1526 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.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.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
47 from rhodecode.model.db import (
48 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
48 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.pull_request_state,
111 pr.pull_request_id, pr.pull_request_state,
112 pr.work_in_progress, pr.target_repo.repo_name),
112 pr.work_in_progress, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render('pullrequest_title', pr.title, pr.description),
116 'title': _render('pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'state': pr.pull_request_state,
124 'state': pr.pull_request_state,
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 @LoginRequired()
141 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
142 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
143 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
144 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
145 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
147 def pull_request_list(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149
149
150 req_get = self.request.GET
150 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
151 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
153 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
156
157 c.active = 'open'
157 c.active = 'open'
158 if c.my:
158 if c.my:
159 c.active = 'my'
159 c.active = 'my'
160 if c.closed:
160 if c.closed:
161 c.active = 'closed'
161 c.active = 'closed'
162 if c.awaiting_review and not c.source:
162 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
163 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
164 if c.source and not c.awaiting_review:
165 c.active = 'source'
165 c.active = 'source'
166 if c.awaiting_my_review:
166 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
167 c.active = 'awaiting_my'
168
168
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
174 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
175 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
176 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
213 ancestor_commit,
214 source_ref_id, target_ref_id,
214 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
215 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context):
216 fulldiff, hide_whitespace_changes, diff_context):
217
217
218 target_ref_id = ancestor_commit.raw_id
218 target_ref_id = ancestor_commit.raw_id
219 vcs_diff = PullRequestModel().get_diff(
219 vcs_diff = PullRequestModel().get_diff(
220 source_repo, source_ref_id, target_ref_id,
220 source_repo, source_ref_id, target_ref_id,
221 hide_whitespace_changes, diff_context)
221 hide_whitespace_changes, diff_context)
222
222
223 diff_processor = diffs.DiffProcessor(
223 diff_processor = diffs.DiffProcessor(
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 file_limit=file_limit, show_full_diff=fulldiff)
225 file_limit=file_limit, show_full_diff=fulldiff)
226
226
227 _parsed = diff_processor.prepare()
227 _parsed = diff_processor.prepare()
228
228
229 diffset = codeblocks.DiffSet(
229 diffset = codeblocks.DiffSet(
230 repo_name=self.db_repo_name,
230 repo_name=self.db_repo_name,
231 source_repo_name=source_repo_name,
231 source_repo_name=source_repo_name,
232 source_node_getter=codeblocks.diffset_node_getter(target_commit),
232 source_node_getter=codeblocks.diffset_node_getter(target_commit),
233 target_node_getter=codeblocks.diffset_node_getter(source_commit),
233 target_node_getter=codeblocks.diffset_node_getter(source_commit),
234 )
234 )
235 diffset = self.path_filter.render_patchset_filtered(
235 diffset = self.path_filter.render_patchset_filtered(
236 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
236 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
237
237
238 return diffset
238 return diffset
239
239
240 def _get_range_diffset(self, source_scm, source_repo,
240 def _get_range_diffset(self, source_scm, source_repo,
241 commit1, commit2, diff_limit, file_limit,
241 commit1, commit2, diff_limit, file_limit,
242 fulldiff, hide_whitespace_changes, diff_context):
242 fulldiff, hide_whitespace_changes, diff_context):
243 vcs_diff = source_scm.get_diff(
243 vcs_diff = source_scm.get_diff(
244 commit1, commit2,
244 commit1, commit2,
245 ignore_whitespace=hide_whitespace_changes,
245 ignore_whitespace=hide_whitespace_changes,
246 context=diff_context)
246 context=diff_context)
247
247
248 diff_processor = diffs.DiffProcessor(
248 diff_processor = diffs.DiffProcessor(
249 vcs_diff, format='newdiff', diff_limit=diff_limit,
249 vcs_diff, format='newdiff', diff_limit=diff_limit,
250 file_limit=file_limit, show_full_diff=fulldiff)
250 file_limit=file_limit, show_full_diff=fulldiff)
251
251
252 _parsed = diff_processor.prepare()
252 _parsed = diff_processor.prepare()
253
253
254 diffset = codeblocks.DiffSet(
254 diffset = codeblocks.DiffSet(
255 repo_name=source_repo.repo_name,
255 repo_name=source_repo.repo_name,
256 source_node_getter=codeblocks.diffset_node_getter(commit1),
256 source_node_getter=codeblocks.diffset_node_getter(commit1),
257 target_node_getter=codeblocks.diffset_node_getter(commit2))
257 target_node_getter=codeblocks.diffset_node_getter(commit2))
258
258
259 diffset = self.path_filter.render_patchset_filtered(
259 diffset = self.path_filter.render_patchset_filtered(
260 diffset, _parsed, commit1.raw_id, commit2.raw_id)
260 diffset, _parsed, commit1.raw_id, commit2.raw_id)
261
261
262 return diffset
262 return diffset
263
263
264 @LoginRequired()
264 @LoginRequired()
265 @HasRepoPermissionAnyDecorator(
265 @HasRepoPermissionAnyDecorator(
266 'repository.read', 'repository.write', 'repository.admin')
266 'repository.read', 'repository.write', 'repository.admin')
267 @view_config(
267 @view_config(
268 route_name='pullrequest_show', request_method='GET',
268 route_name='pullrequest_show', request_method='GET',
269 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
269 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
270 def pull_request_show(self):
270 def pull_request_show(self):
271 _ = self.request.translate
271 _ = self.request.translate
272 c = self.load_default_context()
272 c = self.load_default_context()
273
273
274 pull_request = PullRequest.get_or_404(
274 pull_request = PullRequest.get_or_404(
275 self.request.matchdict['pull_request_id'])
275 self.request.matchdict['pull_request_id'])
276 pull_request_id = pull_request.pull_request_id
276 pull_request_id = pull_request.pull_request_id
277
277
278 c.state_progressing = pull_request.is_state_changing()
278 c.state_progressing = pull_request.is_state_changing()
279
279
280 _new_state = {
280 _new_state = {
281 'created': PullRequest.STATE_CREATED,
281 'created': PullRequest.STATE_CREATED,
282 }.get(self.request.GET.get('force_state'))
282 }.get(self.request.GET.get('force_state'))
283
283
284 if c.is_super_admin and _new_state:
284 if c.is_super_admin and _new_state:
285 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
285 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
286 h.flash(
286 h.flash(
287 _('Pull Request state was force changed to `{}`').format(_new_state),
287 _('Pull Request state was force changed to `{}`').format(_new_state),
288 category='success')
288 category='success')
289 Session().commit()
289 Session().commit()
290
290
291 raise HTTPFound(h.route_path(
291 raise HTTPFound(h.route_path(
292 'pullrequest_show', repo_name=self.db_repo_name,
292 'pullrequest_show', repo_name=self.db_repo_name,
293 pull_request_id=pull_request_id))
293 pull_request_id=pull_request_id))
294
294
295 version = self.request.GET.get('version')
295 version = self.request.GET.get('version')
296 from_version = self.request.GET.get('from_version') or version
296 from_version = self.request.GET.get('from_version') or version
297 merge_checks = self.request.GET.get('merge_checks')
297 merge_checks = self.request.GET.get('merge_checks')
298 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
298 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
299
299
300 # fetch global flags of ignore ws or context lines
300 # fetch global flags of ignore ws or context lines
301 diff_context = diffs.get_diff_context(self.request)
301 diff_context = diffs.get_diff_context(self.request)
302 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
302 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
303
303
304 force_refresh = str2bool(self.request.GET.get('force_refresh'))
304 force_refresh = str2bool(self.request.GET.get('force_refresh'))
305
305
306 (pull_request_latest,
306 (pull_request_latest,
307 pull_request_at_ver,
307 pull_request_at_ver,
308 pull_request_display_obj,
308 pull_request_display_obj,
309 at_version) = PullRequestModel().get_pr_version(
309 at_version) = PullRequestModel().get_pr_version(
310 pull_request_id, version=version)
310 pull_request_id, version=version)
311 pr_closed = pull_request_latest.is_closed()
311 pr_closed = pull_request_latest.is_closed()
312
312
313 if pr_closed and (version or from_version):
313 if pr_closed and (version or from_version):
314 # not allow to browse versions
314 # not allow to browse versions
315 raise HTTPFound(h.route_path(
315 raise HTTPFound(h.route_path(
316 'pullrequest_show', repo_name=self.db_repo_name,
316 'pullrequest_show', repo_name=self.db_repo_name,
317 pull_request_id=pull_request_id))
317 pull_request_id=pull_request_id))
318
318
319 versions = pull_request_display_obj.versions()
319 versions = pull_request_display_obj.versions()
320 # used to store per-commit range diffs
320 # used to store per-commit range diffs
321 c.changes = collections.OrderedDict()
321 c.changes = collections.OrderedDict()
322 c.range_diff_on = self.request.GET.get('range-diff') == "1"
322 c.range_diff_on = self.request.GET.get('range-diff') == "1"
323
323
324 c.at_version = at_version
324 c.at_version = at_version
325 c.at_version_num = (at_version
325 c.at_version_num = (at_version
326 if at_version and at_version != 'latest'
326 if at_version and at_version != 'latest'
327 else None)
327 else None)
328 c.at_version_pos = ChangesetComment.get_index_from_version(
328 c.at_version_pos = ChangesetComment.get_index_from_version(
329 c.at_version_num, versions)
329 c.at_version_num, versions)
330
330
331 (prev_pull_request_latest,
331 (prev_pull_request_latest,
332 prev_pull_request_at_ver,
332 prev_pull_request_at_ver,
333 prev_pull_request_display_obj,
333 prev_pull_request_display_obj,
334 prev_at_version) = PullRequestModel().get_pr_version(
334 prev_at_version) = PullRequestModel().get_pr_version(
335 pull_request_id, version=from_version)
335 pull_request_id, version=from_version)
336
336
337 c.from_version = prev_at_version
337 c.from_version = prev_at_version
338 c.from_version_num = (prev_at_version
338 c.from_version_num = (prev_at_version
339 if prev_at_version and prev_at_version != 'latest'
339 if prev_at_version and prev_at_version != 'latest'
340 else None)
340 else None)
341 c.from_version_pos = ChangesetComment.get_index_from_version(
341 c.from_version_pos = ChangesetComment.get_index_from_version(
342 c.from_version_num, versions)
342 c.from_version_num, versions)
343
343
344 # define if we're in COMPARE mode or VIEW at version mode
344 # define if we're in COMPARE mode or VIEW at version mode
345 compare = at_version != prev_at_version
345 compare = at_version != prev_at_version
346
346
347 # pull_requests repo_name we opened it against
347 # pull_requests repo_name we opened it against
348 # ie. target_repo must match
348 # ie. target_repo must match
349 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
349 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
350 raise HTTPNotFound()
350 raise HTTPNotFound()
351
351
352 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
352 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
353 pull_request_at_ver)
353 pull_request_at_ver)
354
354
355 c.pull_request = pull_request_display_obj
355 c.pull_request = pull_request_display_obj
356 c.renderer = pull_request_at_ver.description_renderer or c.renderer
356 c.renderer = pull_request_at_ver.description_renderer or c.renderer
357 c.pull_request_latest = pull_request_latest
357 c.pull_request_latest = pull_request_latest
358
358
359 if compare or (at_version and not at_version == 'latest'):
359 if compare or (at_version and not at_version == 'latest'):
360 c.allowed_to_change_status = False
360 c.allowed_to_change_status = False
361 c.allowed_to_update = False
361 c.allowed_to_update = False
362 c.allowed_to_merge = False
362 c.allowed_to_merge = False
363 c.allowed_to_delete = False
363 c.allowed_to_delete = False
364 c.allowed_to_comment = False
364 c.allowed_to_comment = False
365 c.allowed_to_close = False
365 c.allowed_to_close = False
366 else:
366 else:
367 can_change_status = PullRequestModel().check_user_change_status(
367 can_change_status = PullRequestModel().check_user_change_status(
368 pull_request_at_ver, self._rhodecode_user)
368 pull_request_at_ver, self._rhodecode_user)
369 c.allowed_to_change_status = can_change_status and not pr_closed
369 c.allowed_to_change_status = can_change_status and not pr_closed
370
370
371 c.allowed_to_update = PullRequestModel().check_user_update(
371 c.allowed_to_update = PullRequestModel().check_user_update(
372 pull_request_latest, self._rhodecode_user) and not pr_closed
372 pull_request_latest, self._rhodecode_user) and not pr_closed
373 c.allowed_to_merge = PullRequestModel().check_user_merge(
373 c.allowed_to_merge = PullRequestModel().check_user_merge(
374 pull_request_latest, self._rhodecode_user) and not pr_closed
374 pull_request_latest, self._rhodecode_user) and not pr_closed
375 c.allowed_to_delete = PullRequestModel().check_user_delete(
375 c.allowed_to_delete = PullRequestModel().check_user_delete(
376 pull_request_latest, self._rhodecode_user) and not pr_closed
376 pull_request_latest, self._rhodecode_user) and not pr_closed
377 c.allowed_to_comment = not pr_closed
377 c.allowed_to_comment = not pr_closed
378 c.allowed_to_close = c.allowed_to_merge and not pr_closed
378 c.allowed_to_close = c.allowed_to_merge and not pr_closed
379
379
380 c.forbid_adding_reviewers = False
380 c.forbid_adding_reviewers = False
381 c.forbid_author_to_review = False
381 c.forbid_author_to_review = False
382 c.forbid_commit_author_to_review = False
382 c.forbid_commit_author_to_review = False
383
383
384 if pull_request_latest.reviewer_data and \
384 if pull_request_latest.reviewer_data and \
385 'rules' in pull_request_latest.reviewer_data:
385 'rules' in pull_request_latest.reviewer_data:
386 rules = pull_request_latest.reviewer_data['rules'] or {}
386 rules = pull_request_latest.reviewer_data['rules'] or {}
387 try:
387 try:
388 c.forbid_adding_reviewers = rules.get(
388 c.forbid_adding_reviewers = rules.get(
389 'forbid_adding_reviewers')
389 'forbid_adding_reviewers')
390 c.forbid_author_to_review = rules.get(
390 c.forbid_author_to_review = rules.get(
391 'forbid_author_to_review')
391 'forbid_author_to_review')
392 c.forbid_commit_author_to_review = rules.get(
392 c.forbid_commit_author_to_review = rules.get(
393 'forbid_commit_author_to_review')
393 'forbid_commit_author_to_review')
394 except Exception:
394 except Exception:
395 pass
395 pass
396
396
397 # check merge capabilities
397 # check merge capabilities
398 _merge_check = MergeCheck.validate(
398 _merge_check = MergeCheck.validate(
399 pull_request_latest, auth_user=self._rhodecode_user,
399 pull_request_latest, auth_user=self._rhodecode_user,
400 translator=self.request.translate,
400 translator=self.request.translate,
401 force_shadow_repo_refresh=force_refresh)
401 force_shadow_repo_refresh=force_refresh)
402
402
403 c.pr_merge_errors = _merge_check.error_details
403 c.pr_merge_errors = _merge_check.error_details
404 c.pr_merge_possible = not _merge_check.failed
404 c.pr_merge_possible = not _merge_check.failed
405 c.pr_merge_message = _merge_check.merge_msg
405 c.pr_merge_message = _merge_check.merge_msg
406 c.pr_merge_source_commit = _merge_check.source_commit
406 c.pr_merge_source_commit = _merge_check.source_commit
407 c.pr_merge_target_commit = _merge_check.target_commit
407 c.pr_merge_target_commit = _merge_check.target_commit
408
408
409 c.pr_merge_info = MergeCheck.get_merge_conditions(
409 c.pr_merge_info = MergeCheck.get_merge_conditions(
410 pull_request_latest, translator=self.request.translate)
410 pull_request_latest, translator=self.request.translate)
411
411
412 c.pull_request_review_status = _merge_check.review_status
412 c.pull_request_review_status = _merge_check.review_status
413 if merge_checks:
413 if merge_checks:
414 self.request.override_renderer = \
414 self.request.override_renderer = \
415 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
415 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
416 return self._get_template_context(c)
416 return self._get_template_context(c)
417
417
418 comments_model = CommentsModel()
418 comments_model = CommentsModel()
419
419
420 # reviewers and statuses
420 # reviewers and statuses
421 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
421 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
422 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
422 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
423
423
424 # GENERAL COMMENTS with versions #
424 # GENERAL COMMENTS with versions #
425 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
425 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
426 q = q.order_by(ChangesetComment.comment_id.asc())
426 q = q.order_by(ChangesetComment.comment_id.asc())
427 general_comments = q
427 general_comments = q
428
428
429 # pick comments we want to render at current version
429 # pick comments we want to render at current version
430 c.comment_versions = comments_model.aggregate_comments(
430 c.comment_versions = comments_model.aggregate_comments(
431 general_comments, versions, c.at_version_num)
431 general_comments, versions, c.at_version_num)
432 c.comments = c.comment_versions[c.at_version_num]['until']
432 c.comments = c.comment_versions[c.at_version_num]['until']
433
433
434 # INLINE COMMENTS with versions #
434 # INLINE COMMENTS with versions #
435 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
435 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
436 q = q.order_by(ChangesetComment.comment_id.asc())
436 q = q.order_by(ChangesetComment.comment_id.asc())
437 inline_comments = q
437 inline_comments = q
438
438
439 c.inline_versions = comments_model.aggregate_comments(
439 c.inline_versions = comments_model.aggregate_comments(
440 inline_comments, versions, c.at_version_num, inline=True)
440 inline_comments, versions, c.at_version_num, inline=True)
441
441
442 # TODOs
442 # TODOs
443 c.unresolved_comments = CommentsModel() \
443 c.unresolved_comments = CommentsModel() \
444 .get_pull_request_unresolved_todos(pull_request)
444 .get_pull_request_unresolved_todos(pull_request)
445 c.resolved_comments = CommentsModel() \
445 c.resolved_comments = CommentsModel() \
446 .get_pull_request_resolved_todos(pull_request)
446 .get_pull_request_resolved_todos(pull_request)
447
447
448 # inject latest version
448 # inject latest version
449 latest_ver = PullRequest.get_pr_display_object(
449 latest_ver = PullRequest.get_pr_display_object(
450 pull_request_latest, pull_request_latest)
450 pull_request_latest, pull_request_latest)
451
451
452 c.versions = versions + [latest_ver]
452 c.versions = versions + [latest_ver]
453
453
454 # if we use version, then do not show later comments
454 # if we use version, then do not show later comments
455 # than current version
455 # than current version
456 display_inline_comments = collections.defaultdict(
456 display_inline_comments = collections.defaultdict(
457 lambda: collections.defaultdict(list))
457 lambda: collections.defaultdict(list))
458 for co in inline_comments:
458 for co in inline_comments:
459 if c.at_version_num:
459 if c.at_version_num:
460 # pick comments that are at least UPTO given version, so we
460 # pick comments that are at least UPTO given version, so we
461 # don't render comments for higher version
461 # don't render comments for higher version
462 should_render = co.pull_request_version_id and \
462 should_render = co.pull_request_version_id and \
463 co.pull_request_version_id <= c.at_version_num
463 co.pull_request_version_id <= c.at_version_num
464 else:
464 else:
465 # showing all, for 'latest'
465 # showing all, for 'latest'
466 should_render = True
466 should_render = True
467
467
468 if should_render:
468 if should_render:
469 display_inline_comments[co.f_path][co.line_no].append(co)
469 display_inline_comments[co.f_path][co.line_no].append(co)
470
470
471 # load diff data into template context, if we use compare mode then
471 # load diff data into template context, if we use compare mode then
472 # diff is calculated based on changes between versions of PR
472 # diff is calculated based on changes between versions of PR
473
473
474 source_repo = pull_request_at_ver.source_repo
474 source_repo = pull_request_at_ver.source_repo
475 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
475 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
476
476
477 target_repo = pull_request_at_ver.target_repo
477 target_repo = pull_request_at_ver.target_repo
478 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
478 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
479
479
480 if compare:
480 if compare:
481 # in compare switch the diff base to latest commit from prev version
481 # in compare switch the diff base to latest commit from prev version
482 target_ref_id = prev_pull_request_display_obj.revisions[0]
482 target_ref_id = prev_pull_request_display_obj.revisions[0]
483
483
484 # despite opening commits for bookmarks/branches/tags, we always
484 # despite opening commits for bookmarks/branches/tags, we always
485 # convert this to rev to prevent changes after bookmark or branch change
485 # convert this to rev to prevent changes after bookmark or branch change
486 c.source_ref_type = 'rev'
486 c.source_ref_type = 'rev'
487 c.source_ref = source_ref_id
487 c.source_ref = source_ref_id
488
488
489 c.target_ref_type = 'rev'
489 c.target_ref_type = 'rev'
490 c.target_ref = target_ref_id
490 c.target_ref = target_ref_id
491
491
492 c.source_repo = source_repo
492 c.source_repo = source_repo
493 c.target_repo = target_repo
493 c.target_repo = target_repo
494
494
495 c.commit_ranges = []
495 c.commit_ranges = []
496 source_commit = EmptyCommit()
496 source_commit = EmptyCommit()
497 target_commit = EmptyCommit()
497 target_commit = EmptyCommit()
498 c.missing_requirements = False
498 c.missing_requirements = False
499
499
500 source_scm = source_repo.scm_instance()
500 source_scm = source_repo.scm_instance()
501 target_scm = target_repo.scm_instance()
501 target_scm = target_repo.scm_instance()
502
502
503 shadow_scm = None
503 shadow_scm = None
504 try:
504 try:
505 shadow_scm = pull_request_latest.get_shadow_repo()
505 shadow_scm = pull_request_latest.get_shadow_repo()
506 except Exception:
506 except Exception:
507 log.debug('Failed to get shadow repo', exc_info=True)
507 log.debug('Failed to get shadow repo', exc_info=True)
508 # try first the existing source_repo, and then shadow
508 # try first the existing source_repo, and then shadow
509 # repo if we can obtain one
509 # repo if we can obtain one
510 commits_source_repo = source_scm
510 commits_source_repo = source_scm
511 if shadow_scm:
511 if shadow_scm:
512 commits_source_repo = shadow_scm
512 commits_source_repo = shadow_scm
513
513
514 c.commits_source_repo = commits_source_repo
514 c.commits_source_repo = commits_source_repo
515 c.ancestor = None # set it to None, to hide it from PR view
515 c.ancestor = None # set it to None, to hide it from PR view
516
516
517 # empty version means latest, so we keep this to prevent
517 # empty version means latest, so we keep this to prevent
518 # double caching
518 # double caching
519 version_normalized = version or 'latest'
519 version_normalized = version or 'latest'
520 from_version_normalized = from_version or 'latest'
520 from_version_normalized = from_version or 'latest'
521
521
522 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
522 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
523 cache_file_path = diff_cache_exist(
523 cache_file_path = diff_cache_exist(
524 cache_path, 'pull_request', pull_request_id, version_normalized,
524 cache_path, 'pull_request', pull_request_id, version_normalized,
525 from_version_normalized, source_ref_id, target_ref_id,
525 from_version_normalized, source_ref_id, target_ref_id,
526 hide_whitespace_changes, diff_context, c.fulldiff)
526 hide_whitespace_changes, diff_context, c.fulldiff)
527
527
528 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
528 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
529 force_recache = self.get_recache_flag()
529 force_recache = self.get_recache_flag()
530
530
531 cached_diff = None
531 cached_diff = None
532 if caching_enabled:
532 if caching_enabled:
533 cached_diff = load_cached_diff(cache_file_path)
533 cached_diff = load_cached_diff(cache_file_path)
534
534
535 has_proper_commit_cache = (
535 has_proper_commit_cache = (
536 cached_diff and cached_diff.get('commits')
536 cached_diff and cached_diff.get('commits')
537 and len(cached_diff.get('commits', [])) == 5
537 and len(cached_diff.get('commits', [])) == 5
538 and cached_diff.get('commits')[0]
538 and cached_diff.get('commits')[0]
539 and cached_diff.get('commits')[3])
539 and cached_diff.get('commits')[3])
540
540
541 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
541 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
542 diff_commit_cache = \
542 diff_commit_cache = \
543 (ancestor_commit, commit_cache, missing_requirements,
543 (ancestor_commit, commit_cache, missing_requirements,
544 source_commit, target_commit) = cached_diff['commits']
544 source_commit, target_commit) = cached_diff['commits']
545 else:
545 else:
546 # NOTE(marcink): we reach potentially unreachable errors when a PR has
546 # NOTE(marcink): we reach potentially unreachable errors when a PR has
547 # merge errors resulting in potentially hidden commits in the shadow repo.
547 # merge errors resulting in potentially hidden commits in the shadow repo.
548 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
548 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
549 and _merge_check.merge_response
549 and _merge_check.merge_response
550 maybe_unreachable = maybe_unreachable \
550 maybe_unreachable = maybe_unreachable \
551 and _merge_check.merge_response.metadata.get('unresolved_files')
551 and _merge_check.merge_response.metadata.get('unresolved_files')
552 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
552 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
553 diff_commit_cache = \
553 diff_commit_cache = \
554 (ancestor_commit, commit_cache, missing_requirements,
554 (ancestor_commit, commit_cache, missing_requirements,
555 source_commit, target_commit) = self.get_commits(
555 source_commit, target_commit) = self.get_commits(
556 commits_source_repo,
556 commits_source_repo,
557 pull_request_at_ver,
557 pull_request_at_ver,
558 source_commit,
558 source_commit,
559 source_ref_id,
559 source_ref_id,
560 source_scm,
560 source_scm,
561 target_commit,
561 target_commit,
562 target_ref_id,
562 target_ref_id,
563 target_scm,
563 target_scm,
564 maybe_unreachable=maybe_unreachable)
564 maybe_unreachable=maybe_unreachable)
565
565
566 # register our commit range
566 # register our commit range
567 for comm in commit_cache.values():
567 for comm in commit_cache.values():
568 c.commit_ranges.append(comm)
568 c.commit_ranges.append(comm)
569
569
570 c.missing_requirements = missing_requirements
570 c.missing_requirements = missing_requirements
571
571 c.ancestor_commit = ancestor_commit
572 c.ancestor_commit = ancestor_commit
572 c.statuses = source_repo.statuses(
573 c.statuses = source_repo.statuses(
573 [x.raw_id for x in c.commit_ranges])
574 [x.raw_id for x in c.commit_ranges])
574
575
575 # auto collapse if we have more than limit
576 # auto collapse if we have more than limit
576 collapse_limit = diffs.DiffProcessor._collapse_commits_over
577 collapse_limit = diffs.DiffProcessor._collapse_commits_over
577 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
578 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
578 c.compare_mode = compare
579 c.compare_mode = compare
579
580
580 # diff_limit is the old behavior, will cut off the whole diff
581 # diff_limit is the old behavior, will cut off the whole diff
581 # if the limit is applied otherwise will just hide the
582 # if the limit is applied otherwise will just hide the
582 # big files from the front-end
583 # big files from the front-end
583 diff_limit = c.visual.cut_off_limit_diff
584 diff_limit = c.visual.cut_off_limit_diff
584 file_limit = c.visual.cut_off_limit_file
585 file_limit = c.visual.cut_off_limit_file
585
586
586 c.missing_commits = False
587 c.missing_commits = False
587 if (c.missing_requirements
588 if (c.missing_requirements
588 or isinstance(source_commit, EmptyCommit)
589 or isinstance(source_commit, EmptyCommit)
589 or source_commit == target_commit):
590 or source_commit == target_commit):
590
591
591 c.missing_commits = True
592 c.missing_commits = True
592 else:
593 else:
593 c.inline_comments = display_inline_comments
594 c.inline_comments = display_inline_comments
594
595
595 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
596 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
596 if not force_recache and has_proper_diff_cache:
597 if not force_recache and has_proper_diff_cache:
597 c.diffset = cached_diff['diff']
598 c.diffset = cached_diff['diff']
598 else:
599 else:
599 c.diffset = self._get_diffset(
600 try:
600 c.source_repo.repo_name, commits_source_repo,
601 c.diffset = self._get_diffset(
601 c.ancestor_commit,
602 c.source_repo.repo_name, commits_source_repo,
602 source_ref_id, target_ref_id,
603 c.ancestor_commit,
603 target_commit, source_commit,
604 source_ref_id, target_ref_id,
604 diff_limit, file_limit, c.fulldiff,
605 target_commit, source_commit,
605 hide_whitespace_changes, diff_context)
606 diff_limit, file_limit, c.fulldiff,
606
607 hide_whitespace_changes, diff_context)
607 # save cached diff
608 if caching_enabled:
609 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
610
611 c.limited_diff = c.diffset.limited_diff
612
613 # calculate removed files that are bound to comments
614 comment_deleted_files = [
615 fname for fname in display_inline_comments
616 if fname not in c.diffset.file_stats]
617
618 c.deleted_files_comments = collections.defaultdict(dict)
619 for fname, per_line_comments in display_inline_comments.items():
620 if fname in comment_deleted_files:
621 c.deleted_files_comments[fname]['stats'] = 0
622 c.deleted_files_comments[fname]['comments'] = list()
623 for lno, comments in per_line_comments.items():
624 c.deleted_files_comments[fname]['comments'].extend(comments)
625
626 # maybe calculate the range diff
627 if c.range_diff_on:
628 # TODO(marcink): set whitespace/context
629 context_lcl = 3
630 ign_whitespace_lcl = False
631
632 for commit in c.commit_ranges:
633 commit2 = commit
634 commit1 = commit.first_parent
635
636 range_diff_cache_file_path = diff_cache_exist(
637 cache_path, 'diff', commit.raw_id,
638 ign_whitespace_lcl, context_lcl, c.fulldiff)
639
640 cached_diff = None
641 if caching_enabled:
642 cached_diff = load_cached_diff(range_diff_cache_file_path)
643
644 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
645 if not force_recache and has_proper_diff_cache:
646 diffset = cached_diff['diff']
647 else:
648 diffset = self._get_range_diffset(
649 commits_source_repo, source_repo,
650 commit1, commit2, diff_limit, file_limit,
651 c.fulldiff, ign_whitespace_lcl, context_lcl
652 )
653
654 # save cached diff
608 # save cached diff
655 if caching_enabled:
609 if caching_enabled:
656 cache_diff(range_diff_cache_file_path, diffset, None)
610 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
611 except CommitDoesNotExistError:
612 log.exception('Failed to generate diffset')
613 c.missing_commits = True
614
615 if not c.missing_commits:
616
617 c.limited_diff = c.diffset.limited_diff
618
619 # calculate removed files that are bound to comments
620 comment_deleted_files = [
621 fname for fname in display_inline_comments
622 if fname not in c.diffset.file_stats]
623
624 c.deleted_files_comments = collections.defaultdict(dict)
625 for fname, per_line_comments in display_inline_comments.items():
626 if fname in comment_deleted_files:
627 c.deleted_files_comments[fname]['stats'] = 0
628 c.deleted_files_comments[fname]['comments'] = list()
629 for lno, comments in per_line_comments.items():
630 c.deleted_files_comments[fname]['comments'].extend(comments)
631
632 # maybe calculate the range diff
633 if c.range_diff_on:
634 # TODO(marcink): set whitespace/context
635 context_lcl = 3
636 ign_whitespace_lcl = False
657
637
658 c.changes[commit.raw_id] = diffset
638 for commit in c.commit_ranges:
639 commit2 = commit
640 commit1 = commit.first_parent
641
642 range_diff_cache_file_path = diff_cache_exist(
643 cache_path, 'diff', commit.raw_id,
644 ign_whitespace_lcl, context_lcl, c.fulldiff)
645
646 cached_diff = None
647 if caching_enabled:
648 cached_diff = load_cached_diff(range_diff_cache_file_path)
649
650 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
651 if not force_recache and has_proper_diff_cache:
652 diffset = cached_diff['diff']
653 else:
654 diffset = self._get_range_diffset(
655 commits_source_repo, source_repo,
656 commit1, commit2, diff_limit, file_limit,
657 c.fulldiff, ign_whitespace_lcl, context_lcl
658 )
659
660 # save cached diff
661 if caching_enabled:
662 cache_diff(range_diff_cache_file_path, diffset, None)
663
664 c.changes[commit.raw_id] = diffset
659
665
660 # this is a hack to properly display links, when creating PR, the
666 # this is a hack to properly display links, when creating PR, the
661 # compare view and others uses different notation, and
667 # compare view and others uses different notation, and
662 # compare_commits.mako renders links based on the target_repo.
668 # compare_commits.mako renders links based on the target_repo.
663 # We need to swap that here to generate it properly on the html side
669 # We need to swap that here to generate it properly on the html side
664 c.target_repo = c.source_repo
670 c.target_repo = c.source_repo
665
671
666 c.commit_statuses = ChangesetStatus.STATUSES
672 c.commit_statuses = ChangesetStatus.STATUSES
667
673
668 c.show_version_changes = not pr_closed
674 c.show_version_changes = not pr_closed
669 if c.show_version_changes:
675 if c.show_version_changes:
670 cur_obj = pull_request_at_ver
676 cur_obj = pull_request_at_ver
671 prev_obj = prev_pull_request_at_ver
677 prev_obj = prev_pull_request_at_ver
672
678
673 old_commit_ids = prev_obj.revisions
679 old_commit_ids = prev_obj.revisions
674 new_commit_ids = cur_obj.revisions
680 new_commit_ids = cur_obj.revisions
675 commit_changes = PullRequestModel()._calculate_commit_id_changes(
681 commit_changes = PullRequestModel()._calculate_commit_id_changes(
676 old_commit_ids, new_commit_ids)
682 old_commit_ids, new_commit_ids)
677 c.commit_changes_summary = commit_changes
683 c.commit_changes_summary = commit_changes
678
684
679 # calculate the diff for commits between versions
685 # calculate the diff for commits between versions
680 c.commit_changes = []
686 c.commit_changes = []
681
687
682 def mark(cs, fw):
688 def mark(cs, fw):
683 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
689 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
684
690
685 for c_type, raw_id in mark(commit_changes.added, 'a') \
691 for c_type, raw_id in mark(commit_changes.added, 'a') \
686 + mark(commit_changes.removed, 'r') \
692 + mark(commit_changes.removed, 'r') \
687 + mark(commit_changes.common, 'c'):
693 + mark(commit_changes.common, 'c'):
688
694
689 if raw_id in commit_cache:
695 if raw_id in commit_cache:
690 commit = commit_cache[raw_id]
696 commit = commit_cache[raw_id]
691 else:
697 else:
692 try:
698 try:
693 commit = commits_source_repo.get_commit(raw_id)
699 commit = commits_source_repo.get_commit(raw_id)
694 except CommitDoesNotExistError:
700 except CommitDoesNotExistError:
695 # in case we fail extracting still use "dummy" commit
701 # in case we fail extracting still use "dummy" commit
696 # for display in commit diff
702 # for display in commit diff
697 commit = h.AttributeDict(
703 commit = h.AttributeDict(
698 {'raw_id': raw_id,
704 {'raw_id': raw_id,
699 'message': 'EMPTY or MISSING COMMIT'})
705 'message': 'EMPTY or MISSING COMMIT'})
700 c.commit_changes.append([c_type, commit])
706 c.commit_changes.append([c_type, commit])
701
707
702 # current user review statuses for each version
708 # current user review statuses for each version
703 c.review_versions = {}
709 c.review_versions = {}
704 if self._rhodecode_user.user_id in allowed_reviewers:
710 if self._rhodecode_user.user_id in allowed_reviewers:
705 for co in general_comments:
711 for co in general_comments:
706 if co.author.user_id == self._rhodecode_user.user_id:
712 if co.author.user_id == self._rhodecode_user.user_id:
707 status = co.status_change
713 status = co.status_change
708 if status:
714 if status:
709 _ver_pr = status[0].comment.pull_request_version_id
715 _ver_pr = status[0].comment.pull_request_version_id
710 c.review_versions[_ver_pr] = status[0]
716 c.review_versions[_ver_pr] = status[0]
711
717
712 return self._get_template_context(c)
718 return self._get_template_context(c)
713
719
714 def get_commits(
720 def get_commits(
715 self, commits_source_repo, pull_request_at_ver, source_commit,
721 self, commits_source_repo, pull_request_at_ver, source_commit,
716 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
722 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
717 maybe_unreachable=False):
723 maybe_unreachable=False):
718
724
719 commit_cache = collections.OrderedDict()
725 commit_cache = collections.OrderedDict()
720 missing_requirements = False
726 missing_requirements = False
721
727
722 try:
728 try:
723 pre_load = ["author", "date", "message", "branch", "parents"]
729 pre_load = ["author", "date", "message", "branch", "parents"]
724
730
725 pull_request_commits = pull_request_at_ver.revisions
731 pull_request_commits = pull_request_at_ver.revisions
726 log.debug('Loading %s commits from %s',
732 log.debug('Loading %s commits from %s',
727 len(pull_request_commits), commits_source_repo)
733 len(pull_request_commits), commits_source_repo)
728
734
729 for rev in pull_request_commits:
735 for rev in pull_request_commits:
730 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
736 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
731 maybe_unreachable=maybe_unreachable)
737 maybe_unreachable=maybe_unreachable)
732 commit_cache[comm.raw_id] = comm
738 commit_cache[comm.raw_id] = comm
733
739
734 # Order here matters, we first need to get target, and then
740 # Order here matters, we first need to get target, and then
735 # the source
741 # the source
736 target_commit = commits_source_repo.get_commit(
742 target_commit = commits_source_repo.get_commit(
737 commit_id=safe_str(target_ref_id))
743 commit_id=safe_str(target_ref_id))
738
744
739 source_commit = commits_source_repo.get_commit(
745 source_commit = commits_source_repo.get_commit(
740 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
746 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
741 except CommitDoesNotExistError:
747 except CommitDoesNotExistError:
742 log.warning('Failed to get commit from `{}` repo'.format(
748 log.warning('Failed to get commit from `{}` repo'.format(
743 commits_source_repo), exc_info=True)
749 commits_source_repo), exc_info=True)
744 except RepositoryRequirementError:
750 except RepositoryRequirementError:
745 log.warning('Failed to get all required data from repo', exc_info=True)
751 log.warning('Failed to get all required data from repo', exc_info=True)
746 missing_requirements = True
752 missing_requirements = True
747
753
748 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
754 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
749
755
750 try:
756 try:
751 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
757 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
752 except Exception:
758 except Exception:
753 ancestor_commit = None
759 ancestor_commit = None
754
760
755 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
761 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
756
762
757 def assure_not_empty_repo(self):
763 def assure_not_empty_repo(self):
758 _ = self.request.translate
764 _ = self.request.translate
759
765
760 try:
766 try:
761 self.db_repo.scm_instance().get_commit()
767 self.db_repo.scm_instance().get_commit()
762 except EmptyRepositoryError:
768 except EmptyRepositoryError:
763 h.flash(h.literal(_('There are no commits yet')),
769 h.flash(h.literal(_('There are no commits yet')),
764 category='warning')
770 category='warning')
765 raise HTTPFound(
771 raise HTTPFound(
766 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
772 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
767
773
768 @LoginRequired()
774 @LoginRequired()
769 @NotAnonymous()
775 @NotAnonymous()
770 @HasRepoPermissionAnyDecorator(
776 @HasRepoPermissionAnyDecorator(
771 'repository.read', 'repository.write', 'repository.admin')
777 'repository.read', 'repository.write', 'repository.admin')
772 @view_config(
778 @view_config(
773 route_name='pullrequest_new', request_method='GET',
779 route_name='pullrequest_new', request_method='GET',
774 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
780 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
775 def pull_request_new(self):
781 def pull_request_new(self):
776 _ = self.request.translate
782 _ = self.request.translate
777 c = self.load_default_context()
783 c = self.load_default_context()
778
784
779 self.assure_not_empty_repo()
785 self.assure_not_empty_repo()
780 source_repo = self.db_repo
786 source_repo = self.db_repo
781
787
782 commit_id = self.request.GET.get('commit')
788 commit_id = self.request.GET.get('commit')
783 branch_ref = self.request.GET.get('branch')
789 branch_ref = self.request.GET.get('branch')
784 bookmark_ref = self.request.GET.get('bookmark')
790 bookmark_ref = self.request.GET.get('bookmark')
785
791
786 try:
792 try:
787 source_repo_data = PullRequestModel().generate_repo_data(
793 source_repo_data = PullRequestModel().generate_repo_data(
788 source_repo, commit_id=commit_id,
794 source_repo, commit_id=commit_id,
789 branch=branch_ref, bookmark=bookmark_ref,
795 branch=branch_ref, bookmark=bookmark_ref,
790 translator=self.request.translate)
796 translator=self.request.translate)
791 except CommitDoesNotExistError as e:
797 except CommitDoesNotExistError as e:
792 log.exception(e)
798 log.exception(e)
793 h.flash(_('Commit does not exist'), 'error')
799 h.flash(_('Commit does not exist'), 'error')
794 raise HTTPFound(
800 raise HTTPFound(
795 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
801 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
796
802
797 default_target_repo = source_repo
803 default_target_repo = source_repo
798
804
799 if source_repo.parent and c.has_origin_repo_read_perm:
805 if source_repo.parent and c.has_origin_repo_read_perm:
800 parent_vcs_obj = source_repo.parent.scm_instance()
806 parent_vcs_obj = source_repo.parent.scm_instance()
801 if parent_vcs_obj and not parent_vcs_obj.is_empty():
807 if parent_vcs_obj and not parent_vcs_obj.is_empty():
802 # change default if we have a parent repo
808 # change default if we have a parent repo
803 default_target_repo = source_repo.parent
809 default_target_repo = source_repo.parent
804
810
805 target_repo_data = PullRequestModel().generate_repo_data(
811 target_repo_data = PullRequestModel().generate_repo_data(
806 default_target_repo, translator=self.request.translate)
812 default_target_repo, translator=self.request.translate)
807
813
808 selected_source_ref = source_repo_data['refs']['selected_ref']
814 selected_source_ref = source_repo_data['refs']['selected_ref']
809 title_source_ref = ''
815 title_source_ref = ''
810 if selected_source_ref:
816 if selected_source_ref:
811 title_source_ref = selected_source_ref.split(':', 2)[1]
817 title_source_ref = selected_source_ref.split(':', 2)[1]
812 c.default_title = PullRequestModel().generate_pullrequest_title(
818 c.default_title = PullRequestModel().generate_pullrequest_title(
813 source=source_repo.repo_name,
819 source=source_repo.repo_name,
814 source_ref=title_source_ref,
820 source_ref=title_source_ref,
815 target=default_target_repo.repo_name
821 target=default_target_repo.repo_name
816 )
822 )
817
823
818 c.default_repo_data = {
824 c.default_repo_data = {
819 'source_repo_name': source_repo.repo_name,
825 'source_repo_name': source_repo.repo_name,
820 'source_refs_json': json.dumps(source_repo_data),
826 'source_refs_json': json.dumps(source_repo_data),
821 'target_repo_name': default_target_repo.repo_name,
827 'target_repo_name': default_target_repo.repo_name,
822 'target_refs_json': json.dumps(target_repo_data),
828 'target_refs_json': json.dumps(target_repo_data),
823 }
829 }
824 c.default_source_ref = selected_source_ref
830 c.default_source_ref = selected_source_ref
825
831
826 return self._get_template_context(c)
832 return self._get_template_context(c)
827
833
828 @LoginRequired()
834 @LoginRequired()
829 @NotAnonymous()
835 @NotAnonymous()
830 @HasRepoPermissionAnyDecorator(
836 @HasRepoPermissionAnyDecorator(
831 'repository.read', 'repository.write', 'repository.admin')
837 'repository.read', 'repository.write', 'repository.admin')
832 @view_config(
838 @view_config(
833 route_name='pullrequest_repo_refs', request_method='GET',
839 route_name='pullrequest_repo_refs', request_method='GET',
834 renderer='json_ext', xhr=True)
840 renderer='json_ext', xhr=True)
835 def pull_request_repo_refs(self):
841 def pull_request_repo_refs(self):
836 self.load_default_context()
842 self.load_default_context()
837 target_repo_name = self.request.matchdict['target_repo_name']
843 target_repo_name = self.request.matchdict['target_repo_name']
838 repo = Repository.get_by_repo_name(target_repo_name)
844 repo = Repository.get_by_repo_name(target_repo_name)
839 if not repo:
845 if not repo:
840 raise HTTPNotFound()
846 raise HTTPNotFound()
841
847
842 target_perm = HasRepoPermissionAny(
848 target_perm = HasRepoPermissionAny(
843 'repository.read', 'repository.write', 'repository.admin')(
849 'repository.read', 'repository.write', 'repository.admin')(
844 target_repo_name)
850 target_repo_name)
845 if not target_perm:
851 if not target_perm:
846 raise HTTPNotFound()
852 raise HTTPNotFound()
847
853
848 return PullRequestModel().generate_repo_data(
854 return PullRequestModel().generate_repo_data(
849 repo, translator=self.request.translate)
855 repo, translator=self.request.translate)
850
856
851 @LoginRequired()
857 @LoginRequired()
852 @NotAnonymous()
858 @NotAnonymous()
853 @HasRepoPermissionAnyDecorator(
859 @HasRepoPermissionAnyDecorator(
854 'repository.read', 'repository.write', 'repository.admin')
860 'repository.read', 'repository.write', 'repository.admin')
855 @view_config(
861 @view_config(
856 route_name='pullrequest_repo_targets', request_method='GET',
862 route_name='pullrequest_repo_targets', request_method='GET',
857 renderer='json_ext', xhr=True)
863 renderer='json_ext', xhr=True)
858 def pullrequest_repo_targets(self):
864 def pullrequest_repo_targets(self):
859 _ = self.request.translate
865 _ = self.request.translate
860 filter_query = self.request.GET.get('query')
866 filter_query = self.request.GET.get('query')
861
867
862 # get the parents
868 # get the parents
863 parent_target_repos = []
869 parent_target_repos = []
864 if self.db_repo.parent:
870 if self.db_repo.parent:
865 parents_query = Repository.query() \
871 parents_query = Repository.query() \
866 .order_by(func.length(Repository.repo_name)) \
872 .order_by(func.length(Repository.repo_name)) \
867 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
873 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
868
874
869 if filter_query:
875 if filter_query:
870 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
876 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
871 parents_query = parents_query.filter(
877 parents_query = parents_query.filter(
872 Repository.repo_name.ilike(ilike_expression))
878 Repository.repo_name.ilike(ilike_expression))
873 parents = parents_query.limit(20).all()
879 parents = parents_query.limit(20).all()
874
880
875 for parent in parents:
881 for parent in parents:
876 parent_vcs_obj = parent.scm_instance()
882 parent_vcs_obj = parent.scm_instance()
877 if parent_vcs_obj and not parent_vcs_obj.is_empty():
883 if parent_vcs_obj and not parent_vcs_obj.is_empty():
878 parent_target_repos.append(parent)
884 parent_target_repos.append(parent)
879
885
880 # get other forks, and repo itself
886 # get other forks, and repo itself
881 query = Repository.query() \
887 query = Repository.query() \
882 .order_by(func.length(Repository.repo_name)) \
888 .order_by(func.length(Repository.repo_name)) \
883 .filter(
889 .filter(
884 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
890 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
885 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
891 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
886 ) \
892 ) \
887 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
893 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
888
894
889 if filter_query:
895 if filter_query:
890 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
896 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
891 query = query.filter(Repository.repo_name.ilike(ilike_expression))
897 query = query.filter(Repository.repo_name.ilike(ilike_expression))
892
898
893 limit = max(20 - len(parent_target_repos), 5) # not less then 5
899 limit = max(20 - len(parent_target_repos), 5) # not less then 5
894 target_repos = query.limit(limit).all()
900 target_repos = query.limit(limit).all()
895
901
896 all_target_repos = target_repos + parent_target_repos
902 all_target_repos = target_repos + parent_target_repos
897
903
898 repos = []
904 repos = []
899 # This checks permissions to the repositories
905 # This checks permissions to the repositories
900 for obj in ScmModel().get_repos(all_target_repos):
906 for obj in ScmModel().get_repos(all_target_repos):
901 repos.append({
907 repos.append({
902 'id': obj['name'],
908 'id': obj['name'],
903 'text': obj['name'],
909 'text': obj['name'],
904 'type': 'repo',
910 'type': 'repo',
905 'repo_id': obj['dbrepo']['repo_id'],
911 'repo_id': obj['dbrepo']['repo_id'],
906 'repo_type': obj['dbrepo']['repo_type'],
912 'repo_type': obj['dbrepo']['repo_type'],
907 'private': obj['dbrepo']['private'],
913 'private': obj['dbrepo']['private'],
908
914
909 })
915 })
910
916
911 data = {
917 data = {
912 'more': False,
918 'more': False,
913 'results': [{
919 'results': [{
914 'text': _('Repositories'),
920 'text': _('Repositories'),
915 'children': repos
921 'children': repos
916 }] if repos else []
922 }] if repos else []
917 }
923 }
918 return data
924 return data
919
925
920 @LoginRequired()
926 @LoginRequired()
921 @NotAnonymous()
927 @NotAnonymous()
922 @HasRepoPermissionAnyDecorator(
928 @HasRepoPermissionAnyDecorator(
923 'repository.read', 'repository.write', 'repository.admin')
929 'repository.read', 'repository.write', 'repository.admin')
924 @CSRFRequired()
930 @CSRFRequired()
925 @view_config(
931 @view_config(
926 route_name='pullrequest_create', request_method='POST',
932 route_name='pullrequest_create', request_method='POST',
927 renderer=None)
933 renderer=None)
928 def pull_request_create(self):
934 def pull_request_create(self):
929 _ = self.request.translate
935 _ = self.request.translate
930 self.assure_not_empty_repo()
936 self.assure_not_empty_repo()
931 self.load_default_context()
937 self.load_default_context()
932
938
933 controls = peppercorn.parse(self.request.POST.items())
939 controls = peppercorn.parse(self.request.POST.items())
934
940
935 try:
941 try:
936 form = PullRequestForm(
942 form = PullRequestForm(
937 self.request.translate, self.db_repo.repo_id)()
943 self.request.translate, self.db_repo.repo_id)()
938 _form = form.to_python(controls)
944 _form = form.to_python(controls)
939 except formencode.Invalid as errors:
945 except formencode.Invalid as errors:
940 if errors.error_dict.get('revisions'):
946 if errors.error_dict.get('revisions'):
941 msg = 'Revisions: %s' % errors.error_dict['revisions']
947 msg = 'Revisions: %s' % errors.error_dict['revisions']
942 elif errors.error_dict.get('pullrequest_title'):
948 elif errors.error_dict.get('pullrequest_title'):
943 msg = errors.error_dict.get('pullrequest_title')
949 msg = errors.error_dict.get('pullrequest_title')
944 else:
950 else:
945 msg = _('Error creating pull request: {}').format(errors)
951 msg = _('Error creating pull request: {}').format(errors)
946 log.exception(msg)
952 log.exception(msg)
947 h.flash(msg, 'error')
953 h.flash(msg, 'error')
948
954
949 # would rather just go back to form ...
955 # would rather just go back to form ...
950 raise HTTPFound(
956 raise HTTPFound(
951 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
957 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
952
958
953 source_repo = _form['source_repo']
959 source_repo = _form['source_repo']
954 source_ref = _form['source_ref']
960 source_ref = _form['source_ref']
955 target_repo = _form['target_repo']
961 target_repo = _form['target_repo']
956 target_ref = _form['target_ref']
962 target_ref = _form['target_ref']
957 commit_ids = _form['revisions'][::-1]
963 commit_ids = _form['revisions'][::-1]
958 common_ancestor_id = _form['common_ancestor']
964 common_ancestor_id = _form['common_ancestor']
959
965
960 # find the ancestor for this pr
966 # find the ancestor for this pr
961 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
967 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
962 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
968 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
963
969
964 if not (source_db_repo or target_db_repo):
970 if not (source_db_repo or target_db_repo):
965 h.flash(_('source_repo or target repo not found'), category='error')
971 h.flash(_('source_repo or target repo not found'), category='error')
966 raise HTTPFound(
972 raise HTTPFound(
967 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
973 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
968
974
969 # re-check permissions again here
975 # re-check permissions again here
970 # source_repo we must have read permissions
976 # source_repo we must have read permissions
971
977
972 source_perm = HasRepoPermissionAny(
978 source_perm = HasRepoPermissionAny(
973 'repository.read', 'repository.write', 'repository.admin')(
979 'repository.read', 'repository.write', 'repository.admin')(
974 source_db_repo.repo_name)
980 source_db_repo.repo_name)
975 if not source_perm:
981 if not source_perm:
976 msg = _('Not Enough permissions to source repo `{}`.'.format(
982 msg = _('Not Enough permissions to source repo `{}`.'.format(
977 source_db_repo.repo_name))
983 source_db_repo.repo_name))
978 h.flash(msg, category='error')
984 h.flash(msg, category='error')
979 # copy the args back to redirect
985 # copy the args back to redirect
980 org_query = self.request.GET.mixed()
986 org_query = self.request.GET.mixed()
981 raise HTTPFound(
987 raise HTTPFound(
982 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
988 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
983 _query=org_query))
989 _query=org_query))
984
990
985 # target repo we must have read permissions, and also later on
991 # target repo we must have read permissions, and also later on
986 # we want to check branch permissions here
992 # we want to check branch permissions here
987 target_perm = HasRepoPermissionAny(
993 target_perm = HasRepoPermissionAny(
988 'repository.read', 'repository.write', 'repository.admin')(
994 'repository.read', 'repository.write', 'repository.admin')(
989 target_db_repo.repo_name)
995 target_db_repo.repo_name)
990 if not target_perm:
996 if not target_perm:
991 msg = _('Not Enough permissions to target repo `{}`.'.format(
997 msg = _('Not Enough permissions to target repo `{}`.'.format(
992 target_db_repo.repo_name))
998 target_db_repo.repo_name))
993 h.flash(msg, category='error')
999 h.flash(msg, category='error')
994 # copy the args back to redirect
1000 # copy the args back to redirect
995 org_query = self.request.GET.mixed()
1001 org_query = self.request.GET.mixed()
996 raise HTTPFound(
1002 raise HTTPFound(
997 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1003 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
998 _query=org_query))
1004 _query=org_query))
999
1005
1000 source_scm = source_db_repo.scm_instance()
1006 source_scm = source_db_repo.scm_instance()
1001 target_scm = target_db_repo.scm_instance()
1007 target_scm = target_db_repo.scm_instance()
1002
1008
1003 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1009 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1004 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1010 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1005
1011
1006 ancestor = source_scm.get_common_ancestor(
1012 ancestor = source_scm.get_common_ancestor(
1007 source_commit.raw_id, target_commit.raw_id, target_scm)
1013 source_commit.raw_id, target_commit.raw_id, target_scm)
1008
1014
1009 # recalculate target ref based on ancestor
1015 # recalculate target ref based on ancestor
1010 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1016 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1011 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1017 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1012
1018
1013 get_default_reviewers_data, validate_default_reviewers = \
1019 get_default_reviewers_data, validate_default_reviewers = \
1014 PullRequestModel().get_reviewer_functions()
1020 PullRequestModel().get_reviewer_functions()
1015
1021
1016 # recalculate reviewers logic, to make sure we can validate this
1022 # recalculate reviewers logic, to make sure we can validate this
1017 reviewer_rules = get_default_reviewers_data(
1023 reviewer_rules = get_default_reviewers_data(
1018 self._rhodecode_db_user, source_db_repo,
1024 self._rhodecode_db_user, source_db_repo,
1019 source_commit, target_db_repo, target_commit)
1025 source_commit, target_db_repo, target_commit)
1020
1026
1021 given_reviewers = _form['review_members']
1027 given_reviewers = _form['review_members']
1022 reviewers = validate_default_reviewers(
1028 reviewers = validate_default_reviewers(
1023 given_reviewers, reviewer_rules)
1029 given_reviewers, reviewer_rules)
1024
1030
1025 pullrequest_title = _form['pullrequest_title']
1031 pullrequest_title = _form['pullrequest_title']
1026 title_source_ref = source_ref.split(':', 2)[1]
1032 title_source_ref = source_ref.split(':', 2)[1]
1027 if not pullrequest_title:
1033 if not pullrequest_title:
1028 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1034 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1029 source=source_repo,
1035 source=source_repo,
1030 source_ref=title_source_ref,
1036 source_ref=title_source_ref,
1031 target=target_repo
1037 target=target_repo
1032 )
1038 )
1033
1039
1034 description = _form['pullrequest_desc']
1040 description = _form['pullrequest_desc']
1035 description_renderer = _form['description_renderer']
1041 description_renderer = _form['description_renderer']
1036
1042
1037 try:
1043 try:
1038 pull_request = PullRequestModel().create(
1044 pull_request = PullRequestModel().create(
1039 created_by=self._rhodecode_user.user_id,
1045 created_by=self._rhodecode_user.user_id,
1040 source_repo=source_repo,
1046 source_repo=source_repo,
1041 source_ref=source_ref,
1047 source_ref=source_ref,
1042 target_repo=target_repo,
1048 target_repo=target_repo,
1043 target_ref=target_ref,
1049 target_ref=target_ref,
1044 revisions=commit_ids,
1050 revisions=commit_ids,
1045 common_ancestor_id=common_ancestor_id,
1051 common_ancestor_id=common_ancestor_id,
1046 reviewers=reviewers,
1052 reviewers=reviewers,
1047 title=pullrequest_title,
1053 title=pullrequest_title,
1048 description=description,
1054 description=description,
1049 description_renderer=description_renderer,
1055 description_renderer=description_renderer,
1050 reviewer_data=reviewer_rules,
1056 reviewer_data=reviewer_rules,
1051 auth_user=self._rhodecode_user
1057 auth_user=self._rhodecode_user
1052 )
1058 )
1053 Session().commit()
1059 Session().commit()
1054
1060
1055 h.flash(_('Successfully opened new pull request'),
1061 h.flash(_('Successfully opened new pull request'),
1056 category='success')
1062 category='success')
1057 except Exception:
1063 except Exception:
1058 msg = _('Error occurred during creation of this pull request.')
1064 msg = _('Error occurred during creation of this pull request.')
1059 log.exception(msg)
1065 log.exception(msg)
1060 h.flash(msg, category='error')
1066 h.flash(msg, category='error')
1061
1067
1062 # copy the args back to redirect
1068 # copy the args back to redirect
1063 org_query = self.request.GET.mixed()
1069 org_query = self.request.GET.mixed()
1064 raise HTTPFound(
1070 raise HTTPFound(
1065 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1071 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1066 _query=org_query))
1072 _query=org_query))
1067
1073
1068 raise HTTPFound(
1074 raise HTTPFound(
1069 h.route_path('pullrequest_show', repo_name=target_repo,
1075 h.route_path('pullrequest_show', repo_name=target_repo,
1070 pull_request_id=pull_request.pull_request_id))
1076 pull_request_id=pull_request.pull_request_id))
1071
1077
1072 @LoginRequired()
1078 @LoginRequired()
1073 @NotAnonymous()
1079 @NotAnonymous()
1074 @HasRepoPermissionAnyDecorator(
1080 @HasRepoPermissionAnyDecorator(
1075 'repository.read', 'repository.write', 'repository.admin')
1081 'repository.read', 'repository.write', 'repository.admin')
1076 @CSRFRequired()
1082 @CSRFRequired()
1077 @view_config(
1083 @view_config(
1078 route_name='pullrequest_update', request_method='POST',
1084 route_name='pullrequest_update', request_method='POST',
1079 renderer='json_ext')
1085 renderer='json_ext')
1080 def pull_request_update(self):
1086 def pull_request_update(self):
1081 pull_request = PullRequest.get_or_404(
1087 pull_request = PullRequest.get_or_404(
1082 self.request.matchdict['pull_request_id'])
1088 self.request.matchdict['pull_request_id'])
1083 _ = self.request.translate
1089 _ = self.request.translate
1084
1090
1085 self.load_default_context()
1091 self.load_default_context()
1086 redirect_url = None
1092 redirect_url = None
1087
1093
1088 if pull_request.is_closed():
1094 if pull_request.is_closed():
1089 log.debug('update: forbidden because pull request is closed')
1095 log.debug('update: forbidden because pull request is closed')
1090 msg = _(u'Cannot update closed pull requests.')
1096 msg = _(u'Cannot update closed pull requests.')
1091 h.flash(msg, category='error')
1097 h.flash(msg, category='error')
1092 return {'response': True,
1098 return {'response': True,
1093 'redirect_url': redirect_url}
1099 'redirect_url': redirect_url}
1094
1100
1095 is_state_changing = pull_request.is_state_changing()
1101 is_state_changing = pull_request.is_state_changing()
1096
1102
1097 # only owner or admin can update it
1103 # only owner or admin can update it
1098 allowed_to_update = PullRequestModel().check_user_update(
1104 allowed_to_update = PullRequestModel().check_user_update(
1099 pull_request, self._rhodecode_user)
1105 pull_request, self._rhodecode_user)
1100 if allowed_to_update:
1106 if allowed_to_update:
1101 controls = peppercorn.parse(self.request.POST.items())
1107 controls = peppercorn.parse(self.request.POST.items())
1102 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1108 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1103
1109
1104 if 'review_members' in controls:
1110 if 'review_members' in controls:
1105 self._update_reviewers(
1111 self._update_reviewers(
1106 pull_request, controls['review_members'],
1112 pull_request, controls['review_members'],
1107 pull_request.reviewer_data)
1113 pull_request.reviewer_data)
1108 elif str2bool(self.request.POST.get('update_commits', 'false')):
1114 elif str2bool(self.request.POST.get('update_commits', 'false')):
1109 if is_state_changing:
1115 if is_state_changing:
1110 log.debug('commits update: forbidden because pull request is in state %s',
1116 log.debug('commits update: forbidden because pull request is in state %s',
1111 pull_request.pull_request_state)
1117 pull_request.pull_request_state)
1112 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1118 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1113 u'Current state is: `{}`').format(
1119 u'Current state is: `{}`').format(
1114 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1120 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1115 h.flash(msg, category='error')
1121 h.flash(msg, category='error')
1116 return {'response': True,
1122 return {'response': True,
1117 'redirect_url': redirect_url}
1123 'redirect_url': redirect_url}
1118
1124
1119 self._update_commits(pull_request)
1125 self._update_commits(pull_request)
1120 if force_refresh:
1126 if force_refresh:
1121 redirect_url = h.route_path(
1127 redirect_url = h.route_path(
1122 'pullrequest_show', repo_name=self.db_repo_name,
1128 'pullrequest_show', repo_name=self.db_repo_name,
1123 pull_request_id=pull_request.pull_request_id,
1129 pull_request_id=pull_request.pull_request_id,
1124 _query={"force_refresh": 1})
1130 _query={"force_refresh": 1})
1125 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1131 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1126 self._edit_pull_request(pull_request)
1132 self._edit_pull_request(pull_request)
1127 else:
1133 else:
1128 raise HTTPBadRequest()
1134 raise HTTPBadRequest()
1129
1135
1130 return {'response': True,
1136 return {'response': True,
1131 'redirect_url': redirect_url}
1137 'redirect_url': redirect_url}
1132 raise HTTPForbidden()
1138 raise HTTPForbidden()
1133
1139
1134 def _edit_pull_request(self, pull_request):
1140 def _edit_pull_request(self, pull_request):
1135 _ = self.request.translate
1141 _ = self.request.translate
1136
1142
1137 try:
1143 try:
1138 PullRequestModel().edit(
1144 PullRequestModel().edit(
1139 pull_request,
1145 pull_request,
1140 self.request.POST.get('title'),
1146 self.request.POST.get('title'),
1141 self.request.POST.get('description'),
1147 self.request.POST.get('description'),
1142 self.request.POST.get('description_renderer'),
1148 self.request.POST.get('description_renderer'),
1143 self._rhodecode_user)
1149 self._rhodecode_user)
1144 except ValueError:
1150 except ValueError:
1145 msg = _(u'Cannot update closed pull requests.')
1151 msg = _(u'Cannot update closed pull requests.')
1146 h.flash(msg, category='error')
1152 h.flash(msg, category='error')
1147 return
1153 return
1148 else:
1154 else:
1149 Session().commit()
1155 Session().commit()
1150
1156
1151 msg = _(u'Pull request title & description updated.')
1157 msg = _(u'Pull request title & description updated.')
1152 h.flash(msg, category='success')
1158 h.flash(msg, category='success')
1153 return
1159 return
1154
1160
1155 def _update_commits(self, pull_request):
1161 def _update_commits(self, pull_request):
1156 _ = self.request.translate
1162 _ = self.request.translate
1157
1163
1158 with pull_request.set_state(PullRequest.STATE_UPDATING):
1164 with pull_request.set_state(PullRequest.STATE_UPDATING):
1159 resp = PullRequestModel().update_commits(
1165 resp = PullRequestModel().update_commits(
1160 pull_request, self._rhodecode_db_user)
1166 pull_request, self._rhodecode_db_user)
1161
1167
1162 if resp.executed:
1168 if resp.executed:
1163
1169
1164 if resp.target_changed and resp.source_changed:
1170 if resp.target_changed and resp.source_changed:
1165 changed = 'target and source repositories'
1171 changed = 'target and source repositories'
1166 elif resp.target_changed and not resp.source_changed:
1172 elif resp.target_changed and not resp.source_changed:
1167 changed = 'target repository'
1173 changed = 'target repository'
1168 elif not resp.target_changed and resp.source_changed:
1174 elif not resp.target_changed and resp.source_changed:
1169 changed = 'source repository'
1175 changed = 'source repository'
1170 else:
1176 else:
1171 changed = 'nothing'
1177 changed = 'nothing'
1172
1178
1173 msg = _(u'Pull request updated to "{source_commit_id}" with '
1179 msg = _(u'Pull request updated to "{source_commit_id}" with '
1174 u'{count_added} added, {count_removed} removed commits. '
1180 u'{count_added} added, {count_removed} removed commits. '
1175 u'Source of changes: {change_source}')
1181 u'Source of changes: {change_source}')
1176 msg = msg.format(
1182 msg = msg.format(
1177 source_commit_id=pull_request.source_ref_parts.commit_id,
1183 source_commit_id=pull_request.source_ref_parts.commit_id,
1178 count_added=len(resp.changes.added),
1184 count_added=len(resp.changes.added),
1179 count_removed=len(resp.changes.removed),
1185 count_removed=len(resp.changes.removed),
1180 change_source=changed)
1186 change_source=changed)
1181 h.flash(msg, category='success')
1187 h.flash(msg, category='success')
1182
1188
1183 channel = '/repo${}$/pr/{}'.format(
1189 channel = '/repo${}$/pr/{}'.format(
1184 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1190 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1185 message = msg + (
1191 message = msg + (
1186 ' - <a onclick="window.location.reload()">'
1192 ' - <a onclick="window.location.reload()">'
1187 '<strong>{}</strong></a>'.format(_('Reload page')))
1193 '<strong>{}</strong></a>'.format(_('Reload page')))
1188 channelstream.post_message(
1194 channelstream.post_message(
1189 channel, message, self._rhodecode_user.username,
1195 channel, message, self._rhodecode_user.username,
1190 registry=self.request.registry)
1196 registry=self.request.registry)
1191 else:
1197 else:
1192 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1198 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1193 warning_reasons = [
1199 warning_reasons = [
1194 UpdateFailureReason.NO_CHANGE,
1200 UpdateFailureReason.NO_CHANGE,
1195 UpdateFailureReason.WRONG_REF_TYPE,
1201 UpdateFailureReason.WRONG_REF_TYPE,
1196 ]
1202 ]
1197 category = 'warning' if resp.reason in warning_reasons else 'error'
1203 category = 'warning' if resp.reason in warning_reasons else 'error'
1198 h.flash(msg, category=category)
1204 h.flash(msg, category=category)
1199
1205
1200 @LoginRequired()
1206 @LoginRequired()
1201 @NotAnonymous()
1207 @NotAnonymous()
1202 @HasRepoPermissionAnyDecorator(
1208 @HasRepoPermissionAnyDecorator(
1203 'repository.read', 'repository.write', 'repository.admin')
1209 'repository.read', 'repository.write', 'repository.admin')
1204 @CSRFRequired()
1210 @CSRFRequired()
1205 @view_config(
1211 @view_config(
1206 route_name='pullrequest_merge', request_method='POST',
1212 route_name='pullrequest_merge', request_method='POST',
1207 renderer='json_ext')
1213 renderer='json_ext')
1208 def pull_request_merge(self):
1214 def pull_request_merge(self):
1209 """
1215 """
1210 Merge will perform a server-side merge of the specified
1216 Merge will perform a server-side merge of the specified
1211 pull request, if the pull request is approved and mergeable.
1217 pull request, if the pull request is approved and mergeable.
1212 After successful merging, the pull request is automatically
1218 After successful merging, the pull request is automatically
1213 closed, with a relevant comment.
1219 closed, with a relevant comment.
1214 """
1220 """
1215 pull_request = PullRequest.get_or_404(
1221 pull_request = PullRequest.get_or_404(
1216 self.request.matchdict['pull_request_id'])
1222 self.request.matchdict['pull_request_id'])
1217 _ = self.request.translate
1223 _ = self.request.translate
1218
1224
1219 if pull_request.is_state_changing():
1225 if pull_request.is_state_changing():
1220 log.debug('show: forbidden because pull request is in state %s',
1226 log.debug('show: forbidden because pull request is in state %s',
1221 pull_request.pull_request_state)
1227 pull_request.pull_request_state)
1222 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1228 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1223 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1229 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1224 pull_request.pull_request_state)
1230 pull_request.pull_request_state)
1225 h.flash(msg, category='error')
1231 h.flash(msg, category='error')
1226 raise HTTPFound(
1232 raise HTTPFound(
1227 h.route_path('pullrequest_show',
1233 h.route_path('pullrequest_show',
1228 repo_name=pull_request.target_repo.repo_name,
1234 repo_name=pull_request.target_repo.repo_name,
1229 pull_request_id=pull_request.pull_request_id))
1235 pull_request_id=pull_request.pull_request_id))
1230
1236
1231 self.load_default_context()
1237 self.load_default_context()
1232
1238
1233 with pull_request.set_state(PullRequest.STATE_UPDATING):
1239 with pull_request.set_state(PullRequest.STATE_UPDATING):
1234 check = MergeCheck.validate(
1240 check = MergeCheck.validate(
1235 pull_request, auth_user=self._rhodecode_user,
1241 pull_request, auth_user=self._rhodecode_user,
1236 translator=self.request.translate)
1242 translator=self.request.translate)
1237 merge_possible = not check.failed
1243 merge_possible = not check.failed
1238
1244
1239 for err_type, error_msg in check.errors:
1245 for err_type, error_msg in check.errors:
1240 h.flash(error_msg, category=err_type)
1246 h.flash(error_msg, category=err_type)
1241
1247
1242 if merge_possible:
1248 if merge_possible:
1243 log.debug("Pre-conditions checked, trying to merge.")
1249 log.debug("Pre-conditions checked, trying to merge.")
1244 extras = vcs_operation_context(
1250 extras = vcs_operation_context(
1245 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1251 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1246 username=self._rhodecode_db_user.username, action='push',
1252 username=self._rhodecode_db_user.username, action='push',
1247 scm=pull_request.target_repo.repo_type)
1253 scm=pull_request.target_repo.repo_type)
1248 with pull_request.set_state(PullRequest.STATE_UPDATING):
1254 with pull_request.set_state(PullRequest.STATE_UPDATING):
1249 self._merge_pull_request(
1255 self._merge_pull_request(
1250 pull_request, self._rhodecode_db_user, extras)
1256 pull_request, self._rhodecode_db_user, extras)
1251 else:
1257 else:
1252 log.debug("Pre-conditions failed, NOT merging.")
1258 log.debug("Pre-conditions failed, NOT merging.")
1253
1259
1254 raise HTTPFound(
1260 raise HTTPFound(
1255 h.route_path('pullrequest_show',
1261 h.route_path('pullrequest_show',
1256 repo_name=pull_request.target_repo.repo_name,
1262 repo_name=pull_request.target_repo.repo_name,
1257 pull_request_id=pull_request.pull_request_id))
1263 pull_request_id=pull_request.pull_request_id))
1258
1264
1259 def _merge_pull_request(self, pull_request, user, extras):
1265 def _merge_pull_request(self, pull_request, user, extras):
1260 _ = self.request.translate
1266 _ = self.request.translate
1261 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1267 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1262
1268
1263 if merge_resp.executed:
1269 if merge_resp.executed:
1264 log.debug("The merge was successful, closing the pull request.")
1270 log.debug("The merge was successful, closing the pull request.")
1265 PullRequestModel().close_pull_request(
1271 PullRequestModel().close_pull_request(
1266 pull_request.pull_request_id, user)
1272 pull_request.pull_request_id, user)
1267 Session().commit()
1273 Session().commit()
1268 msg = _('Pull request was successfully merged and closed.')
1274 msg = _('Pull request was successfully merged and closed.')
1269 h.flash(msg, category='success')
1275 h.flash(msg, category='success')
1270 else:
1276 else:
1271 log.debug(
1277 log.debug(
1272 "The merge was not successful. Merge response: %s", merge_resp)
1278 "The merge was not successful. Merge response: %s", merge_resp)
1273 msg = merge_resp.merge_status_message
1279 msg = merge_resp.merge_status_message
1274 h.flash(msg, category='error')
1280 h.flash(msg, category='error')
1275
1281
1276 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1282 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1277 _ = self.request.translate
1283 _ = self.request.translate
1278
1284
1279 get_default_reviewers_data, validate_default_reviewers = \
1285 get_default_reviewers_data, validate_default_reviewers = \
1280 PullRequestModel().get_reviewer_functions()
1286 PullRequestModel().get_reviewer_functions()
1281
1287
1282 try:
1288 try:
1283 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1289 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1284 except ValueError as e:
1290 except ValueError as e:
1285 log.error('Reviewers Validation: {}'.format(e))
1291 log.error('Reviewers Validation: {}'.format(e))
1286 h.flash(e, category='error')
1292 h.flash(e, category='error')
1287 return
1293 return
1288
1294
1289 old_calculated_status = pull_request.calculated_review_status()
1295 old_calculated_status = pull_request.calculated_review_status()
1290 PullRequestModel().update_reviewers(
1296 PullRequestModel().update_reviewers(
1291 pull_request, reviewers, self._rhodecode_user)
1297 pull_request, reviewers, self._rhodecode_user)
1292 h.flash(_('Pull request reviewers updated.'), category='success')
1298 h.flash(_('Pull request reviewers updated.'), category='success')
1293 Session().commit()
1299 Session().commit()
1294
1300
1295 # trigger status changed if change in reviewers changes the status
1301 # trigger status changed if change in reviewers changes the status
1296 calculated_status = pull_request.calculated_review_status()
1302 calculated_status = pull_request.calculated_review_status()
1297 if old_calculated_status != calculated_status:
1303 if old_calculated_status != calculated_status:
1298 PullRequestModel().trigger_pull_request_hook(
1304 PullRequestModel().trigger_pull_request_hook(
1299 pull_request, self._rhodecode_user, 'review_status_change',
1305 pull_request, self._rhodecode_user, 'review_status_change',
1300 data={'status': calculated_status})
1306 data={'status': calculated_status})
1301
1307
1302 @LoginRequired()
1308 @LoginRequired()
1303 @NotAnonymous()
1309 @NotAnonymous()
1304 @HasRepoPermissionAnyDecorator(
1310 @HasRepoPermissionAnyDecorator(
1305 'repository.read', 'repository.write', 'repository.admin')
1311 'repository.read', 'repository.write', 'repository.admin')
1306 @CSRFRequired()
1312 @CSRFRequired()
1307 @view_config(
1313 @view_config(
1308 route_name='pullrequest_delete', request_method='POST',
1314 route_name='pullrequest_delete', request_method='POST',
1309 renderer='json_ext')
1315 renderer='json_ext')
1310 def pull_request_delete(self):
1316 def pull_request_delete(self):
1311 _ = self.request.translate
1317 _ = self.request.translate
1312
1318
1313 pull_request = PullRequest.get_or_404(
1319 pull_request = PullRequest.get_or_404(
1314 self.request.matchdict['pull_request_id'])
1320 self.request.matchdict['pull_request_id'])
1315 self.load_default_context()
1321 self.load_default_context()
1316
1322
1317 pr_closed = pull_request.is_closed()
1323 pr_closed = pull_request.is_closed()
1318 allowed_to_delete = PullRequestModel().check_user_delete(
1324 allowed_to_delete = PullRequestModel().check_user_delete(
1319 pull_request, self._rhodecode_user) and not pr_closed
1325 pull_request, self._rhodecode_user) and not pr_closed
1320
1326
1321 # only owner can delete it !
1327 # only owner can delete it !
1322 if allowed_to_delete:
1328 if allowed_to_delete:
1323 PullRequestModel().delete(pull_request, self._rhodecode_user)
1329 PullRequestModel().delete(pull_request, self._rhodecode_user)
1324 Session().commit()
1330 Session().commit()
1325 h.flash(_('Successfully deleted pull request'),
1331 h.flash(_('Successfully deleted pull request'),
1326 category='success')
1332 category='success')
1327 raise HTTPFound(h.route_path('pullrequest_show_all',
1333 raise HTTPFound(h.route_path('pullrequest_show_all',
1328 repo_name=self.db_repo_name))
1334 repo_name=self.db_repo_name))
1329
1335
1330 log.warning('user %s tried to delete pull request without access',
1336 log.warning('user %s tried to delete pull request without access',
1331 self._rhodecode_user)
1337 self._rhodecode_user)
1332 raise HTTPNotFound()
1338 raise HTTPNotFound()
1333
1339
1334 @LoginRequired()
1340 @LoginRequired()
1335 @NotAnonymous()
1341 @NotAnonymous()
1336 @HasRepoPermissionAnyDecorator(
1342 @HasRepoPermissionAnyDecorator(
1337 'repository.read', 'repository.write', 'repository.admin')
1343 'repository.read', 'repository.write', 'repository.admin')
1338 @CSRFRequired()
1344 @CSRFRequired()
1339 @view_config(
1345 @view_config(
1340 route_name='pullrequest_comment_create', request_method='POST',
1346 route_name='pullrequest_comment_create', request_method='POST',
1341 renderer='json_ext')
1347 renderer='json_ext')
1342 def pull_request_comment_create(self):
1348 def pull_request_comment_create(self):
1343 _ = self.request.translate
1349 _ = self.request.translate
1344
1350
1345 pull_request = PullRequest.get_or_404(
1351 pull_request = PullRequest.get_or_404(
1346 self.request.matchdict['pull_request_id'])
1352 self.request.matchdict['pull_request_id'])
1347 pull_request_id = pull_request.pull_request_id
1353 pull_request_id = pull_request.pull_request_id
1348
1354
1349 if pull_request.is_closed():
1355 if pull_request.is_closed():
1350 log.debug('comment: forbidden because pull request is closed')
1356 log.debug('comment: forbidden because pull request is closed')
1351 raise HTTPForbidden()
1357 raise HTTPForbidden()
1352
1358
1353 allowed_to_comment = PullRequestModel().check_user_comment(
1359 allowed_to_comment = PullRequestModel().check_user_comment(
1354 pull_request, self._rhodecode_user)
1360 pull_request, self._rhodecode_user)
1355 if not allowed_to_comment:
1361 if not allowed_to_comment:
1356 log.debug(
1362 log.debug(
1357 'comment: forbidden because pull request is from forbidden repo')
1363 'comment: forbidden because pull request is from forbidden repo')
1358 raise HTTPForbidden()
1364 raise HTTPForbidden()
1359
1365
1360 c = self.load_default_context()
1366 c = self.load_default_context()
1361
1367
1362 status = self.request.POST.get('changeset_status', None)
1368 status = self.request.POST.get('changeset_status', None)
1363 text = self.request.POST.get('text')
1369 text = self.request.POST.get('text')
1364 comment_type = self.request.POST.get('comment_type')
1370 comment_type = self.request.POST.get('comment_type')
1365 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1371 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1366 close_pull_request = self.request.POST.get('close_pull_request')
1372 close_pull_request = self.request.POST.get('close_pull_request')
1367
1373
1368 # the logic here should work like following, if we submit close
1374 # the logic here should work like following, if we submit close
1369 # pr comment, use `close_pull_request_with_comment` function
1375 # pr comment, use `close_pull_request_with_comment` function
1370 # else handle regular comment logic
1376 # else handle regular comment logic
1371
1377
1372 if close_pull_request:
1378 if close_pull_request:
1373 # only owner or admin or person with write permissions
1379 # only owner or admin or person with write permissions
1374 allowed_to_close = PullRequestModel().check_user_update(
1380 allowed_to_close = PullRequestModel().check_user_update(
1375 pull_request, self._rhodecode_user)
1381 pull_request, self._rhodecode_user)
1376 if not allowed_to_close:
1382 if not allowed_to_close:
1377 log.debug('comment: forbidden because not allowed to close '
1383 log.debug('comment: forbidden because not allowed to close '
1378 'pull request %s', pull_request_id)
1384 'pull request %s', pull_request_id)
1379 raise HTTPForbidden()
1385 raise HTTPForbidden()
1380
1386
1381 # This also triggers `review_status_change`
1387 # This also triggers `review_status_change`
1382 comment, status = PullRequestModel().close_pull_request_with_comment(
1388 comment, status = PullRequestModel().close_pull_request_with_comment(
1383 pull_request, self._rhodecode_user, self.db_repo, message=text,
1389 pull_request, self._rhodecode_user, self.db_repo, message=text,
1384 auth_user=self._rhodecode_user)
1390 auth_user=self._rhodecode_user)
1385 Session().flush()
1391 Session().flush()
1386
1392
1387 PullRequestModel().trigger_pull_request_hook(
1393 PullRequestModel().trigger_pull_request_hook(
1388 pull_request, self._rhodecode_user, 'comment',
1394 pull_request, self._rhodecode_user, 'comment',
1389 data={'comment': comment})
1395 data={'comment': comment})
1390
1396
1391 else:
1397 else:
1392 # regular comment case, could be inline, or one with status.
1398 # regular comment case, could be inline, or one with status.
1393 # for that one we check also permissions
1399 # for that one we check also permissions
1394
1400
1395 allowed_to_change_status = PullRequestModel().check_user_change_status(
1401 allowed_to_change_status = PullRequestModel().check_user_change_status(
1396 pull_request, self._rhodecode_user)
1402 pull_request, self._rhodecode_user)
1397
1403
1398 if status and allowed_to_change_status:
1404 if status and allowed_to_change_status:
1399 message = (_('Status change %(transition_icon)s %(status)s')
1405 message = (_('Status change %(transition_icon)s %(status)s')
1400 % {'transition_icon': '>',
1406 % {'transition_icon': '>',
1401 'status': ChangesetStatus.get_status_lbl(status)})
1407 'status': ChangesetStatus.get_status_lbl(status)})
1402 text = text or message
1408 text = text or message
1403
1409
1404 comment = CommentsModel().create(
1410 comment = CommentsModel().create(
1405 text=text,
1411 text=text,
1406 repo=self.db_repo.repo_id,
1412 repo=self.db_repo.repo_id,
1407 user=self._rhodecode_user.user_id,
1413 user=self._rhodecode_user.user_id,
1408 pull_request=pull_request,
1414 pull_request=pull_request,
1409 f_path=self.request.POST.get('f_path'),
1415 f_path=self.request.POST.get('f_path'),
1410 line_no=self.request.POST.get('line'),
1416 line_no=self.request.POST.get('line'),
1411 status_change=(ChangesetStatus.get_status_lbl(status)
1417 status_change=(ChangesetStatus.get_status_lbl(status)
1412 if status and allowed_to_change_status else None),
1418 if status and allowed_to_change_status else None),
1413 status_change_type=(status
1419 status_change_type=(status
1414 if status and allowed_to_change_status else None),
1420 if status and allowed_to_change_status else None),
1415 comment_type=comment_type,
1421 comment_type=comment_type,
1416 resolves_comment_id=resolves_comment_id,
1422 resolves_comment_id=resolves_comment_id,
1417 auth_user=self._rhodecode_user
1423 auth_user=self._rhodecode_user
1418 )
1424 )
1419
1425
1420 if allowed_to_change_status:
1426 if allowed_to_change_status:
1421 # calculate old status before we change it
1427 # calculate old status before we change it
1422 old_calculated_status = pull_request.calculated_review_status()
1428 old_calculated_status = pull_request.calculated_review_status()
1423
1429
1424 # get status if set !
1430 # get status if set !
1425 if status:
1431 if status:
1426 ChangesetStatusModel().set_status(
1432 ChangesetStatusModel().set_status(
1427 self.db_repo.repo_id,
1433 self.db_repo.repo_id,
1428 status,
1434 status,
1429 self._rhodecode_user.user_id,
1435 self._rhodecode_user.user_id,
1430 comment,
1436 comment,
1431 pull_request=pull_request
1437 pull_request=pull_request
1432 )
1438 )
1433
1439
1434 Session().flush()
1440 Session().flush()
1435 # this is somehow required to get access to some relationship
1441 # this is somehow required to get access to some relationship
1436 # loaded on comment
1442 # loaded on comment
1437 Session().refresh(comment)
1443 Session().refresh(comment)
1438
1444
1439 PullRequestModel().trigger_pull_request_hook(
1445 PullRequestModel().trigger_pull_request_hook(
1440 pull_request, self._rhodecode_user, 'comment',
1446 pull_request, self._rhodecode_user, 'comment',
1441 data={'comment': comment})
1447 data={'comment': comment})
1442
1448
1443 # we now calculate the status of pull request, and based on that
1449 # we now calculate the status of pull request, and based on that
1444 # calculation we set the commits status
1450 # calculation we set the commits status
1445 calculated_status = pull_request.calculated_review_status()
1451 calculated_status = pull_request.calculated_review_status()
1446 if old_calculated_status != calculated_status:
1452 if old_calculated_status != calculated_status:
1447 PullRequestModel().trigger_pull_request_hook(
1453 PullRequestModel().trigger_pull_request_hook(
1448 pull_request, self._rhodecode_user, 'review_status_change',
1454 pull_request, self._rhodecode_user, 'review_status_change',
1449 data={'status': calculated_status})
1455 data={'status': calculated_status})
1450
1456
1451 Session().commit()
1457 Session().commit()
1452
1458
1453 data = {
1459 data = {
1454 'target_id': h.safeid(h.safe_unicode(
1460 'target_id': h.safeid(h.safe_unicode(
1455 self.request.POST.get('f_path'))),
1461 self.request.POST.get('f_path'))),
1456 }
1462 }
1457 if comment:
1463 if comment:
1458 c.co = comment
1464 c.co = comment
1459 rendered_comment = render(
1465 rendered_comment = render(
1460 'rhodecode:templates/changeset/changeset_comment_block.mako',
1466 'rhodecode:templates/changeset/changeset_comment_block.mako',
1461 self._get_template_context(c), self.request)
1467 self._get_template_context(c), self.request)
1462
1468
1463 data.update(comment.get_dict())
1469 data.update(comment.get_dict())
1464 data.update({'rendered_text': rendered_comment})
1470 data.update({'rendered_text': rendered_comment})
1465
1471
1466 return data
1472 return data
1467
1473
1468 @LoginRequired()
1474 @LoginRequired()
1469 @NotAnonymous()
1475 @NotAnonymous()
1470 @HasRepoPermissionAnyDecorator(
1476 @HasRepoPermissionAnyDecorator(
1471 'repository.read', 'repository.write', 'repository.admin')
1477 'repository.read', 'repository.write', 'repository.admin')
1472 @CSRFRequired()
1478 @CSRFRequired()
1473 @view_config(
1479 @view_config(
1474 route_name='pullrequest_comment_delete', request_method='POST',
1480 route_name='pullrequest_comment_delete', request_method='POST',
1475 renderer='json_ext')
1481 renderer='json_ext')
1476 def pull_request_comment_delete(self):
1482 def pull_request_comment_delete(self):
1477 pull_request = PullRequest.get_or_404(
1483 pull_request = PullRequest.get_or_404(
1478 self.request.matchdict['pull_request_id'])
1484 self.request.matchdict['pull_request_id'])
1479
1485
1480 comment = ChangesetComment.get_or_404(
1486 comment = ChangesetComment.get_or_404(
1481 self.request.matchdict['comment_id'])
1487 self.request.matchdict['comment_id'])
1482 comment_id = comment.comment_id
1488 comment_id = comment.comment_id
1483
1489
1484 if comment.immutable:
1490 if comment.immutable:
1485 # don't allow deleting comments that are immutable
1491 # don't allow deleting comments that are immutable
1486 raise HTTPForbidden()
1492 raise HTTPForbidden()
1487
1493
1488 if pull_request.is_closed():
1494 if pull_request.is_closed():
1489 log.debug('comment: forbidden because pull request is closed')
1495 log.debug('comment: forbidden because pull request is closed')
1490 raise HTTPForbidden()
1496 raise HTTPForbidden()
1491
1497
1492 if not comment:
1498 if not comment:
1493 log.debug('Comment with id:%s not found, skipping', comment_id)
1499 log.debug('Comment with id:%s not found, skipping', comment_id)
1494 # comment already deleted in another call probably
1500 # comment already deleted in another call probably
1495 return True
1501 return True
1496
1502
1497 if comment.pull_request.is_closed():
1503 if comment.pull_request.is_closed():
1498 # don't allow deleting comments on closed pull request
1504 # don't allow deleting comments on closed pull request
1499 raise HTTPForbidden()
1505 raise HTTPForbidden()
1500
1506
1501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1507 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1502 super_admin = h.HasPermissionAny('hg.admin')()
1508 super_admin = h.HasPermissionAny('hg.admin')()
1503 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1509 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1510 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1505 comment_repo_admin = is_repo_admin and is_repo_comment
1511 comment_repo_admin = is_repo_admin and is_repo_comment
1506
1512
1507 if super_admin or comment_owner or comment_repo_admin:
1513 if super_admin or comment_owner or comment_repo_admin:
1508 old_calculated_status = comment.pull_request.calculated_review_status()
1514 old_calculated_status = comment.pull_request.calculated_review_status()
1509 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1515 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1510 Session().commit()
1516 Session().commit()
1511 calculated_status = comment.pull_request.calculated_review_status()
1517 calculated_status = comment.pull_request.calculated_review_status()
1512 if old_calculated_status != calculated_status:
1518 if old_calculated_status != calculated_status:
1513 PullRequestModel().trigger_pull_request_hook(
1519 PullRequestModel().trigger_pull_request_hook(
1514 comment.pull_request, self._rhodecode_user, 'review_status_change',
1520 comment.pull_request, self._rhodecode_user, 'review_status_change',
1515 data={'status': calculated_status})
1521 data={'status': calculated_status})
1516 return True
1522 return True
1517 else:
1523 else:
1518 log.warning('No permissions for user %s to delete comment_id: %s',
1524 log.warning('No permissions for user %s to delete comment_id: %s',
1519 self._rhodecode_db_user, comment_id)
1525 self._rhodecode_db_user, comment_id)
1520 raise HTTPNotFound()
1526 raise HTTPNotFound()
@@ -1,922 +1,922 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 %if c.rhodecode_name:
7 %if c.rhodecode_name:
8 &middot; ${h.branding(c.rhodecode_name)}
8 &middot; ${h.branding(c.rhodecode_name)}
9 %endif
9 %endif
10 </%def>
10 </%def>
11
11
12 <%def name="breadcrumbs_links()">
12 <%def name="breadcrumbs_links()">
13
13
14 </%def>
14 </%def>
15
15
16 <%def name="menu_bar_nav()">
16 <%def name="menu_bar_nav()">
17 ${self.menu_items(active='repositories')}
17 ${self.menu_items(active='repositories')}
18 </%def>
18 </%def>
19
19
20 <%def name="menu_bar_subnav()">
20 <%def name="menu_bar_subnav()">
21 ${self.repo_menu(active='showpullrequest')}
21 ${self.repo_menu(active='showpullrequest')}
22 </%def>
22 </%def>
23
23
24 <%def name="main()">
24 <%def name="main()">
25
25
26 <script type="text/javascript">
26 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
27 // TODO: marcink switch this to pyroutes
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
29 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
29 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
30 </script>
30 </script>
31
31
32 <div class="box">
32 <div class="box">
33
33
34 <div class="box pr-summary">
34 <div class="box pr-summary">
35
35
36 <div class="summary-details block-left">
36 <div class="summary-details block-left">
37 <div id="pr-title">
37 <div id="pr-title">
38 % if c.pull_request.is_closed():
38 % if c.pull_request.is_closed():
39 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
39 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
40 % endif
40 % endif
41 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
41 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
42 </div>
42 </div>
43 <div id="pr-title-edit" class="input" style="display: none;">
43 <div id="pr-title-edit" class="input" style="display: none;">
44 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
44 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
45 </div>
45 </div>
46
46
47 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 <% summary = lambda n:{False:'summary-short'}.get(n) %>
48 <div class="pr-details-title">
48 <div class="pr-details-title">
49 <div class="pull-left">
49 <div class="pull-left">
50 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
50 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
51 ${_('Created on')}
51 ${_('Created on')}
52 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
52 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
53 <span class="pr-details-title-author-pref">${_('by')}</span>
53 <span class="pr-details-title-author-pref">${_('by')}</span>
54 </div>
54 </div>
55
55
56 <div class="pull-left">
56 <div class="pull-left">
57 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
57 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
58 </div>
58 </div>
59
59
60 %if c.allowed_to_update:
60 %if c.allowed_to_update:
61 <div class="pull-right">
61 <div class="pull-right">
62 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
62 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
63 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
63 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
64 % if c.allowed_to_delete:
64 % if c.allowed_to_delete:
65 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
65 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
66 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
66 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
67 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
67 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
68 type="submit" value="${_('Delete pull request')}">
68 type="submit" value="${_('Delete pull request')}">
69 ${h.end_form()}
69 ${h.end_form()}
70 % else:
70 % else:
71 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
71 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
72 % endif
72 % endif
73 </div>
73 </div>
74 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
74 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
75 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
75 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
76 </div>
76 </div>
77
77
78 %endif
78 %endif
79 </div>
79 </div>
80
80
81 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
81 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
82 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
82 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
83 </div>
83 </div>
84
84
85 <div id="pr-desc-edit" class="input textarea" style="display: none;">
85 <div id="pr-desc-edit" class="input textarea" style="display: none;">
86 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
86 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
87 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
87 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
88 </div>
88 </div>
89
89
90 <div id="summary" class="fields pr-details-content">
90 <div id="summary" class="fields pr-details-content">
91
91
92 ## review
92 ## review
93 <div class="field">
93 <div class="field">
94 <div class="label-pr-detail">
94 <div class="label-pr-detail">
95 <label>${_('Review status')}:</label>
95 <label>${_('Review status')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 %if c.pull_request_review_status:
98 %if c.pull_request_review_status:
99 <div class="tag status-tag-${c.pull_request_review_status}">
99 <div class="tag status-tag-${c.pull_request_review_status}">
100 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
100 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
101 <span class="changeset-status-lbl">
101 <span class="changeset-status-lbl">
102 %if c.pull_request.is_closed():
102 %if c.pull_request.is_closed():
103 ${_('Closed')},
103 ${_('Closed')},
104 %endif
104 %endif
105
105
106 ${h.commit_status_lbl(c.pull_request_review_status)}
106 ${h.commit_status_lbl(c.pull_request_review_status)}
107
107
108 </span>
108 </span>
109 </div>
109 </div>
110 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
110 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
111 %endif
111 %endif
112 </div>
112 </div>
113 </div>
113 </div>
114
114
115 ## source
115 ## source
116 <div class="field">
116 <div class="field">
117 <div class="label-pr-detail">
117 <div class="label-pr-detail">
118 <label>${_('Commit flow')}:</label>
118 <label>${_('Commit flow')}:</label>
119 </div>
119 </div>
120 <div class="input">
120 <div class="input">
121 <div class="pr-commit-flow">
121 <div class="pr-commit-flow">
122 ## Source
122 ## Source
123 %if c.pull_request.source_ref_parts.type == 'branch':
123 %if c.pull_request.source_ref_parts.type == 'branch':
124 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
124 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
125 %else:
125 %else:
126 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
126 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
127 %endif
127 %endif
128 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
128 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
129 &rarr;
129 &rarr;
130 ## Target
130 ## Target
131 %if c.pull_request.target_ref_parts.type == 'branch':
131 %if c.pull_request.target_ref_parts.type == 'branch':
132 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
132 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
133 %else:
133 %else:
134 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
134 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
135 %endif
135 %endif
136
136
137 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
137 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
138
138
139 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
139 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
140 <i class="icon-angle-down">more details</i>
140 <i class="icon-angle-down">more details</i>
141 </a>
141 </a>
142
142
143 </div>
143 </div>
144
144
145 <div class="source-details" style="display: none">
145 <div class="source-details" style="display: none">
146
146
147 <ul>
147 <ul>
148
148
149 ## common ancestor
149 ## common ancestor
150 <li>
150 <li>
151 ${_('Common ancestor')}:
151 ${_('Common ancestor')}:
152 % if c.ancestor_commit:
152 % if c.ancestor_commit:
153 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
153 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
154 % else:
154 % else:
155 ${_('not available')}
155 ${_('not available')}
156 % endif
156 % endif
157 </li>
157 </li>
158
158
159 ## pull url
159 ## pull url
160 <li>
160 <li>
161 %if h.is_hg(c.pull_request.source_repo):
161 %if h.is_hg(c.pull_request.source_repo):
162 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
162 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
163 %elif h.is_git(c.pull_request.source_repo):
163 %elif h.is_git(c.pull_request.source_repo):
164 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
164 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
165 %endif
165 %endif
166
166
167 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
167 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
168 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
168 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
169 </li>
169 </li>
170
170
171 ## Shadow repo
171 ## Shadow repo
172 <li>
172 <li>
173 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
173 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
174 %if h.is_hg(c.pull_request.target_repo):
174 %if h.is_hg(c.pull_request.target_repo):
175 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
175 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
176 %elif h.is_git(c.pull_request.target_repo):
176 %elif h.is_git(c.pull_request.target_repo):
177 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
177 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
178 %endif
178 %endif
179
179
180 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
180 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
181 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
181 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
182
182
183 % else:
183 % else:
184 <div class="">
184 <div class="">
185 ${_('Shadow repository data not available')}.
185 ${_('Shadow repository data not available')}.
186 </div>
186 </div>
187 % endif
187 % endif
188 </li>
188 </li>
189
189
190 </ul>
190 </ul>
191
191
192 </div>
192 </div>
193
193
194 </div>
194 </div>
195
195
196 </div>
196 </div>
197
197
198 ## versions
198 ## versions
199 <div class="field">
199 <div class="field">
200 <div class="label-pr-detail">
200 <div class="label-pr-detail">
201 <label>${_('Versions')}:</label>
201 <label>${_('Versions')}:</label>
202 </div>
202 </div>
203
203
204 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
204 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
205 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
205 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
206
206
207 <div class="pr-versions">
207 <div class="pr-versions">
208 % if c.show_version_changes:
208 % if c.show_version_changes:
209 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
209 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
210 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
210 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
211 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
211 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
212 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
212 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
213 data-toggle-on="${_('show versions')}."
213 data-toggle-on="${_('show versions')}."
214 data-toggle-off="${_('hide versions')}.">
214 data-toggle-off="${_('hide versions')}.">
215 ${_('show versions')}.
215 ${_('show versions')}.
216 </a>
216 </a>
217 <table>
217 <table>
218 ## SHOW ALL VERSIONS OF PR
218 ## SHOW ALL VERSIONS OF PR
219 <% ver_pr = None %>
219 <% ver_pr = None %>
220
220
221 % for data in reversed(list(enumerate(c.versions, 1))):
221 % for data in reversed(list(enumerate(c.versions, 1))):
222 <% ver_pos = data[0] %>
222 <% ver_pos = data[0] %>
223 <% ver = data[1] %>
223 <% ver = data[1] %>
224 <% ver_pr = ver.pull_request_version_id %>
224 <% ver_pr = ver.pull_request_version_id %>
225 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
225 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
226
226
227 <tr class="version-pr" style="display: ${display_row}">
227 <tr class="version-pr" style="display: ${display_row}">
228 <td>
228 <td>
229 <code>
229 <code>
230 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
230 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
231 </code>
231 </code>
232 </td>
232 </td>
233 <td>
233 <td>
234 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
234 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
235 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
235 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
236 </td>
236 </td>
237 <td>
237 <td>
238 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
238 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
239 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
239 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
240
240
241 </td>
241 </td>
242 <td>
242 <td>
243 % if c.at_version_num != ver_pr:
243 % if c.at_version_num != ver_pr:
244 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
244 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
245 <code>
245 <code>
246 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
246 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
247 </code>
247 </code>
248 % endif
248 % endif
249 </td>
249 </td>
250 <td>
250 <td>
251 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
251 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
252 </td>
252 </td>
253 <td>
253 <td>
254 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
254 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
255 </td>
255 </td>
256 </tr>
256 </tr>
257 % endfor
257 % endfor
258
258
259 <tr>
259 <tr>
260 <td colspan="6">
260 <td colspan="6">
261 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
261 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
262 data-label-text-locked="${_('select versions to show changes')}"
262 data-label-text-locked="${_('select versions to show changes')}"
263 data-label-text-diff="${_('show changes between versions')}"
263 data-label-text-diff="${_('show changes between versions')}"
264 data-label-text-show="${_('show pull request for this version')}"
264 data-label-text-show="${_('show pull request for this version')}"
265 >
265 >
266 ${_('select versions to show changes')}
266 ${_('select versions to show changes')}
267 </button>
267 </button>
268 </td>
268 </td>
269 </tr>
269 </tr>
270 </table>
270 </table>
271 % else:
271 % else:
272 <div>
272 <div>
273 ${_('Pull request versions not available')}.
273 ${_('Pull request versions not available')}.
274 </div>
274 </div>
275 % endif
275 % endif
276 </div>
276 </div>
277 </div>
277 </div>
278
278
279 </div>
279 </div>
280
280
281 </div>
281 </div>
282
282
283 ## REVIEW RULES
283 ## REVIEW RULES
284 <div id="review_rules" style="display: none" class="reviewers-title block-right">
284 <div id="review_rules" style="display: none" class="reviewers-title block-right">
285 <div class="pr-details-title">
285 <div class="pr-details-title">
286 ${_('Reviewer rules')}
286 ${_('Reviewer rules')}
287 %if c.allowed_to_update:
287 %if c.allowed_to_update:
288 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
288 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
289 %endif
289 %endif
290 </div>
290 </div>
291 <div class="pr-reviewer-rules">
291 <div class="pr-reviewer-rules">
292 ## review rules will be appended here, by default reviewers logic
292 ## review rules will be appended here, by default reviewers logic
293 </div>
293 </div>
294 <input id="review_data" type="hidden" name="review_data" value="">
294 <input id="review_data" type="hidden" name="review_data" value="">
295 </div>
295 </div>
296
296
297 ## REVIEWERS
297 ## REVIEWERS
298 <div class="reviewers-title first-panel block-right">
298 <div class="reviewers-title first-panel block-right">
299 <div class="pr-details-title">
299 <div class="pr-details-title">
300 ${_('Pull request reviewers')}
300 ${_('Pull request reviewers')}
301 %if c.allowed_to_update:
301 %if c.allowed_to_update:
302 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
302 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
303 %endif
303 %endif
304 </div>
304 </div>
305 </div>
305 </div>
306 <div id="reviewers" class="block-right pr-details-content reviewers">
306 <div id="reviewers" class="block-right pr-details-content reviewers">
307
307
308 ## members redering block
308 ## members redering block
309 <input type="hidden" name="__start__" value="review_members:sequence">
309 <input type="hidden" name="__start__" value="review_members:sequence">
310 <ul id="review_members" class="group_members">
310 <ul id="review_members" class="group_members">
311
311
312 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
312 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
313 <script>
313 <script>
314 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
314 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
315 var status = "${(status[0][1].status if status else 'not_reviewed')}";
315 var status = "${(status[0][1].status if status else 'not_reviewed')}";
316 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
316 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
317 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
317 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
318
318
319 var entry = renderTemplate('reviewMemberEntry', {
319 var entry = renderTemplate('reviewMemberEntry', {
320 'member': member,
320 'member': member,
321 'mandatory': member.mandatory,
321 'mandatory': member.mandatory,
322 'reasons': member.reasons,
322 'reasons': member.reasons,
323 'allowed_to_update': allowed_to_update,
323 'allowed_to_update': allowed_to_update,
324 'review_status': status,
324 'review_status': status,
325 'review_status_label': status_lbl,
325 'review_status_label': status_lbl,
326 'user_group': member.user_group,
326 'user_group': member.user_group,
327 'create': false
327 'create': false
328 });
328 });
329 $('#review_members').append(entry)
329 $('#review_members').append(entry)
330 </script>
330 </script>
331
331
332 % endfor
332 % endfor
333
333
334 </ul>
334 </ul>
335
335
336 <input type="hidden" name="__end__" value="review_members:sequence">
336 <input type="hidden" name="__end__" value="review_members:sequence">
337 ## end members redering block
337 ## end members redering block
338
338
339 %if not c.pull_request.is_closed():
339 %if not c.pull_request.is_closed():
340 <div id="add_reviewer" class="ac" style="display: none;">
340 <div id="add_reviewer" class="ac" style="display: none;">
341 %if c.allowed_to_update:
341 %if c.allowed_to_update:
342 % if not c.forbid_adding_reviewers:
342 % if not c.forbid_adding_reviewers:
343 <div id="add_reviewer_input" class="reviewer_ac">
343 <div id="add_reviewer_input" class="reviewer_ac">
344 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
344 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
345 <div id="reviewers_container"></div>
345 <div id="reviewers_container"></div>
346 </div>
346 </div>
347 % endif
347 % endif
348 <div class="pull-right">
348 <div class="pull-right">
349 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
349 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
350 </div>
350 </div>
351 %endif
351 %endif
352 </div>
352 </div>
353 %endif
353 %endif
354 </div>
354 </div>
355
355
356 ## TODOs will be listed here
356 ## TODOs will be listed here
357 <div class="reviewers-title block-right">
357 <div class="reviewers-title block-right">
358 <div class="pr-details-title">
358 <div class="pr-details-title">
359 ## Only show unresolved, that is only what matters
359 ## Only show unresolved, that is only what matters
360 TODO Comments - ${len(c.unresolved_comments)} / ${(len(c.unresolved_comments) + len(c.resolved_comments))}
360 TODO Comments - ${len(c.unresolved_comments)} / ${(len(c.unresolved_comments) + len(c.resolved_comments))}
361
361
362 % if not c.at_version:
362 % if not c.at_version:
363 % if c.resolved_comments:
363 % if c.resolved_comments:
364 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
364 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
365 % else:
365 % else:
366 <span class="block-right last-item noselect">Show resolved</span>
366 <span class="block-right last-item noselect">Show resolved</span>
367 % endif
367 % endif
368 % endif
368 % endif
369 </div>
369 </div>
370 </div>
370 </div>
371 <div class="block-right pr-details-content reviewers">
371 <div class="block-right pr-details-content reviewers">
372
372
373 <table class="todo-table">
373 <table class="todo-table">
374 <%
374 <%
375 def sorter(entry):
375 def sorter(entry):
376 user_id = entry.author.user_id
376 user_id = entry.author.user_id
377 resolved = '1' if entry.resolved else '0'
377 resolved = '1' if entry.resolved else '0'
378 if user_id == c.rhodecode_user.user_id:
378 if user_id == c.rhodecode_user.user_id:
379 # own comments first
379 # own comments first
380 user_id = 0
380 user_id = 0
381 return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
381 return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
382 %>
382 %>
383
383
384 % if c.at_version:
384 % if c.at_version:
385 <tr>
385 <tr>
386 <td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
386 <td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
387 </tr>
387 </tr>
388 % else:
388 % else:
389 % for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
389 % for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
390 <% resolved = todo_comment.resolved %>
390 <% resolved = todo_comment.resolved %>
391 % if inline:
391 % if inline:
392 <% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
392 <% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
393 % else:
393 % else:
394 <% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
394 <% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
395 % endif
395 % endif
396
396
397 <tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
397 <tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
398
398
399 <td class="td-todo-number">
399 <td class="td-todo-number">
400 % if resolved:
400 % if resolved:
401 <a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
401 <a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
402 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
402 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
403 % else:
403 % else:
404 <a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
404 <a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
405 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
405 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
406 % endif
406 % endif
407 </td>
407 </td>
408 <td class="td-todo-gravatar">
408 <td class="td-todo-gravatar">
409 ${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
409 ${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
410 </td>
410 </td>
411 <td class="todo-comment-text-wrapper">
411 <td class="todo-comment-text-wrapper">
412 <div class="todo-comment-text">
412 <div class="todo-comment-text">
413 <code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
413 <code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
414 </div>
414 </div>
415 </td>
415 </td>
416
416
417 </tr>
417 </tr>
418 % endfor
418 % endfor
419
419
420 % if len(c.unresolved_comments) == 0:
420 % if len(c.unresolved_comments) == 0:
421 <tr>
421 <tr>
422 <td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
422 <td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
423 </tr>
423 </tr>
424 % endif
424 % endif
425
425
426 % endif
426 % endif
427
427
428 </table>
428 </table>
429
429
430 </div>
430 </div>
431 </div>
431 </div>
432
432
433 </div>
433 </div>
434
434
435 <div class="box">
435 <div class="box">
436
436
437 % if c.state_progressing:
437 % if c.state_progressing:
438
438
439 <h2 style="text-align: center">
439 <h2 style="text-align: center">
440 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
440 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
441
441
442 % if c.is_super_admin:
442 % if c.is_super_admin:
443 <br/>
443 <br/>
444 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
444 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
445 % endif
445 % endif
446 </h2>
446 </h2>
447
447
448 % else:
448 % else:
449
449
450 ## Diffs rendered here
450 ## Diffs rendered here
451 <div class="table" >
451 <div class="table" >
452 <div id="changeset_compare_view_content">
452 <div id="changeset_compare_view_content">
453 ##CS
453 ##CS
454 % if c.missing_requirements:
454 % if c.missing_requirements:
455 <div class="box">
455 <div class="box">
456 <div class="alert alert-warning">
456 <div class="alert alert-warning">
457 <div>
457 <div>
458 <strong>${_('Missing requirements:')}</strong>
458 <strong>${_('Missing requirements:')}</strong>
459 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
459 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
460 </div>
460 </div>
461 </div>
461 </div>
462 </div>
462 </div>
463 % elif c.missing_commits:
463 % elif c.missing_commits:
464 <div class="box">
464 <div class="box">
465 <div class="alert alert-warning">
465 <div class="alert alert-warning">
466 <div>
466 <div>
467 <strong>${_('Missing commits')}:</strong>
467 <strong>${_('Missing commits')}:</strong>
468 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
468 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
469 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
469 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
470 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
470 ${_('Consider doing a `force update commits` in case you think this is an error.')}
471 </div>
471 </div>
472 </div>
472 </div>
473 </div>
473 </div>
474 % elif c.pr_merge_source_commit.changed:
474 % elif c.pr_merge_source_commit.changed:
475 <div class="box">
475 <div class="box">
476 <div class="alert alert-info">
476 <div class="alert alert-info">
477 <div>
477 <div>
478 % if c.pr_merge_source_commit.changed:
478 % if c.pr_merge_source_commit.changed:
479 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
479 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
480 % endif
480 % endif
481 </div>
481 </div>
482 </div>
482 </div>
483 </div>
483 </div>
484 % endif
484 % endif
485
485
486 <div class="compare_view_commits_title">
486 <div class="compare_view_commits_title">
487 % if not c.compare_mode:
487 % if not c.compare_mode:
488
488
489 % if c.at_version_pos:
489 % if c.at_version_pos:
490 <h4>
490 <h4>
491 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
491 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
492 </h4>
492 </h4>
493 % endif
493 % endif
494
494
495 <div class="pull-left">
495 <div class="pull-left">
496 <div class="btn-group">
496 <div class="btn-group">
497 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
497 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
498 % if c.collapse_all_commits:
498 % if c.collapse_all_commits:
499 <i class="icon-plus-squared-alt icon-no-margin"></i>
499 <i class="icon-plus-squared-alt icon-no-margin"></i>
500 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
500 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
501 % else:
501 % else:
502 <i class="icon-minus-squared-alt icon-no-margin"></i>
502 <i class="icon-minus-squared-alt icon-no-margin"></i>
503 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
503 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
504 % endif
504 % endif
505 </a>
505 </a>
506 </div>
506 </div>
507 </div>
507 </div>
508
508
509 <div class="pull-right">
509 <div class="pull-right">
510 % if c.allowed_to_update and not c.pull_request.is_closed():
510 % if c.allowed_to_update and not c.pull_request.is_closed():
511
511
512 <div class="btn-group btn-group-actions">
512 <div class="btn-group btn-group-actions">
513 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
513 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
514 ${_('Update commits')}
514 ${_('Update commits')}
515 </a>
515 </a>
516
516
517 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
517 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
518 <i class="icon-down"></i>
518 <i class="icon-down"></i>
519 </a>
519 </a>
520
520
521 <div class="btn-action-switcher-container" id="update-commits-switcher">
521 <div class="btn-action-switcher-container" id="update-commits-switcher">
522 <ul class="btn-action-switcher" role="menu">
522 <ul class="btn-action-switcher" role="menu">
523 <li>
523 <li>
524 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
524 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
525 ${_('Force update commits')}
525 ${_('Force update commits')}
526 </a>
526 </a>
527 <div class="action-help-block">
527 <div class="action-help-block">
528 ${_('Update commits and force refresh this pull request.')}
528 ${_('Update commits and force refresh this pull request.')}
529 </div>
529 </div>
530 </li>
530 </li>
531 </ul>
531 </ul>
532 </div>
532 </div>
533 </div>
533 </div>
534
534
535 % else:
535 % else:
536 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
536 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
537 % endif
537 % endif
538
538
539 </div>
539 </div>
540 % endif
540 % endif
541 </div>
541 </div>
542
542
543 % if not c.missing_commits:
543 % if not c.missing_commits:
544 % if c.compare_mode:
544 % if c.compare_mode:
545 % if c.at_version:
545 % if c.at_version:
546 <h4>
546 <h4>
547 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
547 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
548 </h4>
548 </h4>
549
549
550 <div class="subtitle-compare">
550 <div class="subtitle-compare">
551 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
551 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
552 </div>
552 </div>
553
553
554 <div class="container">
554 <div class="container">
555 <table class="rctable compare_view_commits">
555 <table class="rctable compare_view_commits">
556 <tr>
556 <tr>
557 <th></th>
557 <th></th>
558 <th>${_('Time')}</th>
558 <th>${_('Time')}</th>
559 <th>${_('Author')}</th>
559 <th>${_('Author')}</th>
560 <th>${_('Commit')}</th>
560 <th>${_('Commit')}</th>
561 <th></th>
561 <th></th>
562 <th>${_('Description')}</th>
562 <th>${_('Description')}</th>
563 </tr>
563 </tr>
564
564
565 % for c_type, commit in c.commit_changes:
565 % for c_type, commit in c.commit_changes:
566 % if c_type in ['a', 'r']:
566 % if c_type in ['a', 'r']:
567 <%
567 <%
568 if c_type == 'a':
568 if c_type == 'a':
569 cc_title = _('Commit added in displayed changes')
569 cc_title = _('Commit added in displayed changes')
570 elif c_type == 'r':
570 elif c_type == 'r':
571 cc_title = _('Commit removed in displayed changes')
571 cc_title = _('Commit removed in displayed changes')
572 else:
572 else:
573 cc_title = ''
573 cc_title = ''
574 %>
574 %>
575 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
575 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
576 <td>
576 <td>
577 <div class="commit-change-indicator color-${c_type}-border">
577 <div class="commit-change-indicator color-${c_type}-border">
578 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
578 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
579 ${c_type.upper()}
579 ${c_type.upper()}
580 </div>
580 </div>
581 </div>
581 </div>
582 </td>
582 </td>
583 <td class="td-time">
583 <td class="td-time">
584 ${h.age_component(commit.date)}
584 ${h.age_component(commit.date)}
585 </td>
585 </td>
586 <td class="td-user">
586 <td class="td-user">
587 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
587 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
588 </td>
588 </td>
589 <td class="td-hash">
589 <td class="td-hash">
590 <code>
590 <code>
591 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
591 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
592 r${commit.idx}:${h.short_id(commit.raw_id)}
592 r${commit.idx}:${h.short_id(commit.raw_id)}
593 </a>
593 </a>
594 ${h.hidden('revisions', commit.raw_id)}
594 ${h.hidden('revisions', commit.raw_id)}
595 </code>
595 </code>
596 </td>
596 </td>
597 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
597 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
598 <i class="icon-expand-linked"></i>
598 <i class="icon-expand-linked"></i>
599 </td>
599 </td>
600 <td class="mid td-description">
600 <td class="mid td-description">
601 <div class="log-container truncate-wrap">
601 <div class="log-container truncate-wrap">
602 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
602 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
603 </div>
603 </div>
604 </td>
604 </td>
605 </tr>
605 </tr>
606 % endif
606 % endif
607 % endfor
607 % endfor
608 </table>
608 </table>
609 </div>
609 </div>
610
610
611 % endif
611 % endif
612
612
613 % else:
613 % else:
614 <%include file="/compare/compare_commits.mako" />
614 <%include file="/compare/compare_commits.mako" />
615 % endif
615 % endif
616
616
617 <div class="cs_files">
617 <div class="cs_files">
618 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
618 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
619 % if c.at_version:
619 % if c.at_version:
620 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
620 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
621 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
621 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
622 % else:
622 % else:
623 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
623 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
624 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
624 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
625 % endif
625 % endif
626
626
627 <%
627 <%
628 pr_menu_data = {
628 pr_menu_data = {
629 'outdated_comm_count_ver': outdated_comm_count_ver
629 'outdated_comm_count_ver': outdated_comm_count_ver
630 }
630 }
631 %>
631 %>
632
632
633 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
633 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
634
634
635 % if c.range_diff_on:
635 % if c.range_diff_on:
636 % for commit in c.commit_ranges:
636 % for commit in c.commit_ranges:
637 ${cbdiffs.render_diffset(
637 ${cbdiffs.render_diffset(
638 c.changes[commit.raw_id],
638 c.changes[commit.raw_id],
639 commit=commit, use_comments=True,
639 commit=commit, use_comments=True,
640 collapse_when_files_over=5,
640 collapse_when_files_over=5,
641 disable_new_comments=True,
641 disable_new_comments=True,
642 deleted_files_comments=c.deleted_files_comments,
642 deleted_files_comments=c.deleted_files_comments,
643 inline_comments=c.inline_comments,
643 inline_comments=c.inline_comments,
644 pull_request_menu=pr_menu_data, show_todos=False)}
644 pull_request_menu=pr_menu_data, show_todos=False)}
645 % endfor
645 % endfor
646 % else:
646 % else:
647 ${cbdiffs.render_diffset(
647 ${cbdiffs.render_diffset(
648 c.diffset, use_comments=True,
648 c.diffset, use_comments=True,
649 collapse_when_files_over=30,
649 collapse_when_files_over=30,
650 disable_new_comments=not c.allowed_to_comment,
650 disable_new_comments=not c.allowed_to_comment,
651 deleted_files_comments=c.deleted_files_comments,
651 deleted_files_comments=c.deleted_files_comments,
652 inline_comments=c.inline_comments,
652 inline_comments=c.inline_comments,
653 pull_request_menu=pr_menu_data, show_todos=False)}
653 pull_request_menu=pr_menu_data, show_todos=False)}
654 % endif
654 % endif
655
655
656 </div>
656 </div>
657 % else:
657 % else:
658 ## skipping commits we need to clear the view for missing commits
658 ## skipping commits we need to clear the view for missing commits
659 <div style="clear:both;"></div>
659 <div style="clear:both;"></div>
660 % endif
660 % endif
661
661
662 </div>
662 </div>
663 </div>
663 </div>
664
664
665 ## template for inline comment form
665 ## template for inline comment form
666 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
666 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
667
667
668 ## comments heading with count
668 ## comments heading with count
669 <div class="comments-heading">
669 <div class="comments-heading">
670 <i class="icon-comment"></i>
670 <i class="icon-comment"></i>
671 ${_('Comments')} ${len(c.comments)}
671 ${_('Comments')} ${len(c.comments)}
672 </div>
672 </div>
673
673
674 ## render general comments
674 ## render general comments
675 <div id="comment-tr-show">
675 <div id="comment-tr-show">
676 % if general_outdated_comm_count_ver:
676 % if general_outdated_comm_count_ver:
677 <div class="info-box">
677 <div class="info-box">
678 % if general_outdated_comm_count_ver == 1:
678 % if general_outdated_comm_count_ver == 1:
679 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
679 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
680 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
680 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
681 % else:
681 % else:
682 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
682 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
683 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
683 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
684 % endif
684 % endif
685 </div>
685 </div>
686 % endif
686 % endif
687 </div>
687 </div>
688
688
689 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
689 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
690
690
691 % if not c.pull_request.is_closed():
691 % if not c.pull_request.is_closed():
692 ## main comment form and it status
692 ## main comment form and it status
693 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
693 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
694 pull_request_id=c.pull_request.pull_request_id),
694 pull_request_id=c.pull_request.pull_request_id),
695 c.pull_request_review_status,
695 c.pull_request_review_status,
696 is_pull_request=True, change_status=c.allowed_to_change_status)}
696 is_pull_request=True, change_status=c.allowed_to_change_status)}
697
697
698 ## merge status, and merge action
698 ## merge status, and merge action
699 <div class="pull-request-merge">
699 <div class="pull-request-merge">
700 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
700 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
701 </div>
701 </div>
702
702
703 %endif
703 %endif
704
704
705 % endif
705 % endif
706 </div>
706 </div>
707
707
708 <script type="text/javascript">
708 <script type="text/javascript">
709
709
710 versionController = new VersionController();
710 versionController = new VersionController();
711 versionController.init();
711 versionController.init();
712
712
713 reviewersController = new ReviewersController();
713 reviewersController = new ReviewersController();
714 commitsController = new CommitsController();
714 commitsController = new CommitsController();
715
715
716 updateController = new UpdatePrController();
716 updateController = new UpdatePrController();
717
717
718 $(function () {
718 $(function () {
719
719
720 // custom code mirror
720 // custom code mirror
721 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
721 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
722
722
723 var PRDetails = {
723 var PRDetails = {
724 editButton: $('#open_edit_pullrequest'),
724 editButton: $('#open_edit_pullrequest'),
725 closeButton: $('#close_edit_pullrequest'),
725 closeButton: $('#close_edit_pullrequest'),
726 deleteButton: $('#delete_pullrequest'),
726 deleteButton: $('#delete_pullrequest'),
727 viewFields: $('#pr-desc, #pr-title'),
727 viewFields: $('#pr-desc, #pr-title'),
728 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
728 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
729
729
730 init: function () {
730 init: function () {
731 var that = this;
731 var that = this;
732 this.editButton.on('click', function (e) {
732 this.editButton.on('click', function (e) {
733 that.edit();
733 that.edit();
734 });
734 });
735 this.closeButton.on('click', function (e) {
735 this.closeButton.on('click', function (e) {
736 that.view();
736 that.view();
737 });
737 });
738 },
738 },
739
739
740 edit: function (event) {
740 edit: function (event) {
741 this.viewFields.hide();
741 this.viewFields.hide();
742 this.editButton.hide();
742 this.editButton.hide();
743 this.deleteButton.hide();
743 this.deleteButton.hide();
744 this.closeButton.show();
744 this.closeButton.show();
745 this.editFields.show();
745 this.editFields.show();
746 codeMirrorInstance.refresh();
746 codeMirrorInstance.refresh();
747 },
747 },
748
748
749 view: function (event) {
749 view: function (event) {
750 this.editButton.show();
750 this.editButton.show();
751 this.deleteButton.show();
751 this.deleteButton.show();
752 this.editFields.hide();
752 this.editFields.hide();
753 this.closeButton.hide();
753 this.closeButton.hide();
754 this.viewFields.show();
754 this.viewFields.show();
755 }
755 }
756 };
756 };
757
757
758 var ReviewersPanel = {
758 var ReviewersPanel = {
759 editButton: $('#open_edit_reviewers'),
759 editButton: $('#open_edit_reviewers'),
760 closeButton: $('#close_edit_reviewers'),
760 closeButton: $('#close_edit_reviewers'),
761 addButton: $('#add_reviewer'),
761 addButton: $('#add_reviewer'),
762 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
762 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
763
763
764 init: function () {
764 init: function () {
765 var self = this;
765 var self = this;
766 this.editButton.on('click', function (e) {
766 this.editButton.on('click', function (e) {
767 self.edit();
767 self.edit();
768 });
768 });
769 this.closeButton.on('click', function (e) {
769 this.closeButton.on('click', function (e) {
770 self.close();
770 self.close();
771 });
771 });
772 },
772 },
773
773
774 edit: function (event) {
774 edit: function (event) {
775 this.editButton.hide();
775 this.editButton.hide();
776 this.closeButton.show();
776 this.closeButton.show();
777 this.addButton.show();
777 this.addButton.show();
778 this.removeButtons.css('visibility', 'visible');
778 this.removeButtons.css('visibility', 'visible');
779 // review rules
779 // review rules
780 reviewersController.loadReviewRules(
780 reviewersController.loadReviewRules(
781 ${c.pull_request.reviewer_data_json | n});
781 ${c.pull_request.reviewer_data_json | n});
782 },
782 },
783
783
784 close: function (event) {
784 close: function (event) {
785 this.editButton.show();
785 this.editButton.show();
786 this.closeButton.hide();
786 this.closeButton.hide();
787 this.addButton.hide();
787 this.addButton.hide();
788 this.removeButtons.css('visibility', 'hidden');
788 this.removeButtons.css('visibility', 'hidden');
789 // hide review rules
789 // hide review rules
790 reviewersController.hideReviewRules()
790 reviewersController.hideReviewRules()
791 }
791 }
792 };
792 };
793
793
794 PRDetails.init();
794 PRDetails.init();
795 ReviewersPanel.init();
795 ReviewersPanel.init();
796
796
797 showOutdated = function (self) {
797 showOutdated = function (self) {
798 $('.comment-inline.comment-outdated').show();
798 $('.comment-inline.comment-outdated').show();
799 $('.filediff-outdated').show();
799 $('.filediff-outdated').show();
800 $('.showOutdatedComments').hide();
800 $('.showOutdatedComments').hide();
801 $('.hideOutdatedComments').show();
801 $('.hideOutdatedComments').show();
802 };
802 };
803
803
804 hideOutdated = function (self) {
804 hideOutdated = function (self) {
805 $('.comment-inline.comment-outdated').hide();
805 $('.comment-inline.comment-outdated').hide();
806 $('.filediff-outdated').hide();
806 $('.filediff-outdated').hide();
807 $('.hideOutdatedComments').hide();
807 $('.hideOutdatedComments').hide();
808 $('.showOutdatedComments').show();
808 $('.showOutdatedComments').show();
809 };
809 };
810
810
811 refreshMergeChecks = function () {
811 refreshMergeChecks = function () {
812 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
812 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
813 $('.pull-request-merge').css('opacity', 0.3);
813 $('.pull-request-merge').css('opacity', 0.3);
814 $('.action-buttons-extra').css('opacity', 0.3);
814 $('.action-buttons-extra').css('opacity', 0.3);
815
815
816 $('.pull-request-merge').load(
816 $('.pull-request-merge').load(
817 loadUrl, function () {
817 loadUrl, function () {
818 $('.pull-request-merge').css('opacity', 1);
818 $('.pull-request-merge').css('opacity', 1);
819
819
820 $('.action-buttons-extra').css('opacity', 1);
820 $('.action-buttons-extra').css('opacity', 1);
821 }
821 }
822 );
822 );
823 };
823 };
824
824
825 closePullRequest = function (status) {
825 closePullRequest = function (status) {
826 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
826 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
827 return false;
827 return false;
828 }
828 }
829 // inject closing flag
829 // inject closing flag
830 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
830 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
831 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
831 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
832 $(generalCommentForm.submitForm).submit();
832 $(generalCommentForm.submitForm).submit();
833 };
833 };
834
834
835 $('#show-outdated-comments').on('click', function (e) {
835 $('#show-outdated-comments').on('click', function (e) {
836 var button = $(this);
836 var button = $(this);
837 var outdated = $('.comment-outdated');
837 var outdated = $('.comment-outdated');
838
838
839 if (button.html() === "(Show)") {
839 if (button.html() === "(Show)") {
840 button.html("(Hide)");
840 button.html("(Hide)");
841 outdated.show();
841 outdated.show();
842 } else {
842 } else {
843 button.html("(Show)");
843 button.html("(Show)");
844 outdated.hide();
844 outdated.hide();
845 }
845 }
846 });
846 });
847
847
848 $('.show-inline-comments').on('change', function (e) {
848 $('.show-inline-comments').on('change', function (e) {
849 var show = 'none';
849 var show = 'none';
850 var target = e.currentTarget;
850 var target = e.currentTarget;
851 if (target.checked) {
851 if (target.checked) {
852 show = ''
852 show = ''
853 }
853 }
854 var boxid = $(target).attr('id_for');
854 var boxid = $(target).attr('id_for');
855 var comments = $('#{0} .inline-comments'.format(boxid));
855 var comments = $('#{0} .inline-comments'.format(boxid));
856 var fn_display = function (idx) {
856 var fn_display = function (idx) {
857 $(this).css('display', show);
857 $(this).css('display', show);
858 };
858 };
859 $(comments).each(fn_display);
859 $(comments).each(fn_display);
860 var btns = $('#{0} .inline-comments-button'.format(boxid));
860 var btns = $('#{0} .inline-comments-button'.format(boxid));
861 $(btns).each(fn_display);
861 $(btns).each(fn_display);
862 });
862 });
863
863
864 $('#merge_pull_request_form').submit(function () {
864 $('#merge_pull_request_form').submit(function () {
865 if (!$('#merge_pull_request').attr('disabled')) {
865 if (!$('#merge_pull_request').attr('disabled')) {
866 $('#merge_pull_request').attr('disabled', 'disabled');
866 $('#merge_pull_request').attr('disabled', 'disabled');
867 }
867 }
868 return true;
868 return true;
869 });
869 });
870
870
871 $('#edit_pull_request').on('click', function (e) {
871 $('#edit_pull_request').on('click', function (e) {
872 var title = $('#pr-title-input').val();
872 var title = $('#pr-title-input').val();
873 var description = codeMirrorInstance.getValue();
873 var description = codeMirrorInstance.getValue();
874 var renderer = $('#pr-renderer-input').val();
874 var renderer = $('#pr-renderer-input').val();
875 editPullRequest(
875 editPullRequest(
876 "${c.repo_name}", "${c.pull_request.pull_request_id}",
876 "${c.repo_name}", "${c.pull_request.pull_request_id}",
877 title, description, renderer);
877 title, description, renderer);
878 });
878 });
879
879
880 $('#update_pull_request').on('click', function (e) {
880 $('#update_pull_request').on('click', function (e) {
881 $(this).attr('disabled', 'disabled');
881 $(this).attr('disabled', 'disabled');
882 $(this).addClass('disabled');
882 $(this).addClass('disabled');
883 $(this).html(_gettext('Saving...'));
883 $(this).html(_gettext('Saving...'));
884 reviewersController.updateReviewers(
884 reviewersController.updateReviewers(
885 "${c.repo_name}", "${c.pull_request.pull_request_id}");
885 "${c.repo_name}", "${c.pull_request.pull_request_id}");
886 });
886 });
887
887
888
888
889 // fixing issue with caches on firefox
889 // fixing issue with caches on firefox
890 $('#update_commits').removeAttr("disabled");
890 $('#update_commits').removeAttr("disabled");
891
891
892 $('.show-inline-comments').on('click', function (e) {
892 $('.show-inline-comments').on('click', function (e) {
893 var boxid = $(this).attr('data-comment-id');
893 var boxid = $(this).attr('data-comment-id');
894 var button = $(this);
894 var button = $(this);
895
895
896 if (button.hasClass("comments-visible")) {
896 if (button.hasClass("comments-visible")) {
897 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
897 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
898 $(this).hide();
898 $(this).hide();
899 });
899 });
900 button.removeClass("comments-visible");
900 button.removeClass("comments-visible");
901 } else {
901 } else {
902 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
902 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
903 $(this).show();
903 $(this).show();
904 });
904 });
905 button.addClass("comments-visible");
905 button.addClass("comments-visible");
906 }
906 }
907 });
907 });
908
908
909 // register submit callback on commentForm form to track TODOs
909 // register submit callback on commentForm form to track TODOs
910 window.commentFormGlobalSubmitSuccessCallback = function () {
910 window.commentFormGlobalSubmitSuccessCallback = function () {
911 refreshMergeChecks();
911 refreshMergeChecks();
912 };
912 };
913
913
914 ReviewerAutoComplete('#user');
914 ReviewerAutoComplete('#user');
915
915
916 })
916 })
917
917
918 </script>
918 </script>
919
919
920 </div>
920 </div>
921
921
922 </%def>
922 </%def>
General Comments 0
You need to be logged in to leave comments. Login now