##// END OF EJS Templates
diffs: add new diffs to pull request page
dan -
r1159:ac92afc0 default
parent child Browse files
Show More
@@ -1,464 +1,464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 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 """
21 """
22 commit controller for RhodeCode showing changes between commits
22 commit controller for RhodeCode showing changes between commits
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from collections import defaultdict
27 from collections import defaultdict
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29
29
30 from pylons import tmpl_context as c, request, response
30 from pylons import tmpl_context as c, request, response
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33
33
34 from rhodecode.lib import auth
34 from rhodecode.lib import auth
35 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib import diffs, codeblocks
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import action_logger, jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, GET):
57 def _update_with_GET(params, GET):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += GET.getall(k)
59 params[k] += GET.getall(k)
60
60
61
61
62 def get_ignore_ws(fid, GET):
62 def get_ignore_ws(fid, GET):
63 ig_ws_global = GET.get('ignorews')
63 ig_ws_global = GET.get('ignorews')
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 if ig_ws:
65 if ig_ws:
66 try:
66 try:
67 return int(ig_ws[0].split(':')[-1])
67 return int(ig_ws[0].split(':')[-1])
68 except Exception:
68 except Exception:
69 pass
69 pass
70 return ig_ws_global
70 return ig_ws_global
71
71
72
72
73 def _ignorews_url(GET, fileid=None):
73 def _ignorews_url(GET, fileid=None):
74 fileid = str(fileid) if fileid else None
74 fileid = str(fileid) if fileid else None
75 params = defaultdict(list)
75 params = defaultdict(list)
76 _update_with_GET(params, GET)
76 _update_with_GET(params, GET)
77 label = _('Show whitespace')
77 label = _('Show whitespace')
78 tooltiplbl = _('Show whitespace for all diffs')
78 tooltiplbl = _('Show whitespace for all diffs')
79 ig_ws = get_ignore_ws(fileid, GET)
79 ig_ws = get_ignore_ws(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
80 ln_ctx = get_line_ctx(fileid, GET)
81
81
82 if ig_ws is None:
82 if ig_ws is None:
83 params['ignorews'] += [1]
83 params['ignorews'] += [1]
84 label = _('Ignore whitespace')
84 label = _('Ignore whitespace')
85 tooltiplbl = _('Ignore whitespace for all diffs')
85 tooltiplbl = _('Ignore whitespace for all diffs')
86 ctx_key = 'context'
86 ctx_key = 'context'
87 ctx_val = ln_ctx
87 ctx_val = ln_ctx
88
88
89 # if we have passed in ln_ctx pass it along to our params
89 # if we have passed in ln_ctx pass it along to our params
90 if ln_ctx:
90 if ln_ctx:
91 params[ctx_key] += [ctx_val]
91 params[ctx_key] += [ctx_val]
92
92
93 if fileid:
93 if fileid:
94 params['anchor'] = 'a_' + fileid
94 params['anchor'] = 'a_' + fileid
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96
96
97
97
98 def get_line_ctx(fid, GET):
98 def get_line_ctx(fid, GET):
99 ln_ctx_global = GET.get('context')
99 ln_ctx_global = GET.get('context')
100 if fid:
100 if fid:
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 else:
102 else:
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 if ln_ctx:
105 if ln_ctx:
106 ln_ctx = [ln_ctx]
106 ln_ctx = [ln_ctx]
107
107
108 if ln_ctx:
108 if ln_ctx:
109 retval = ln_ctx[0].split(':')[-1]
109 retval = ln_ctx[0].split(':')[-1]
110 else:
110 else:
111 retval = ln_ctx_global
111 retval = ln_ctx_global
112
112
113 try:
113 try:
114 return int(retval)
114 return int(retval)
115 except Exception:
115 except Exception:
116 return 3
116 return 3
117
117
118
118
119 def _context_url(GET, fileid=None):
119 def _context_url(GET, fileid=None):
120 """
120 """
121 Generates a url for context lines.
121 Generates a url for context lines.
122
122
123 :param fileid:
123 :param fileid:
124 """
124 """
125
125
126 fileid = str(fileid) if fileid else None
126 fileid = str(fileid) if fileid else None
127 ig_ws = get_ignore_ws(fileid, GET)
127 ig_ws = get_ignore_ws(fileid, GET)
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129
129
130 params = defaultdict(list)
130 params = defaultdict(list)
131 _update_with_GET(params, GET)
131 _update_with_GET(params, GET)
132
132
133 if ln_ctx > 0:
133 if ln_ctx > 0:
134 params['context'] += [ln_ctx]
134 params['context'] += [ln_ctx]
135
135
136 if ig_ws:
136 if ig_ws:
137 ig_ws_key = 'ignorews'
137 ig_ws_key = 'ignorews'
138 ig_ws_val = 1
138 ig_ws_val = 1
139 params[ig_ws_key] += [ig_ws_val]
139 params[ig_ws_key] += [ig_ws_val]
140
140
141 lbl = _('Increase context')
141 lbl = _('Increase context')
142 tooltiplbl = _('Increase context for all diffs')
142 tooltiplbl = _('Increase context for all diffs')
143
143
144 if fileid:
144 if fileid:
145 params['anchor'] = 'a_' + fileid
145 params['anchor'] = 'a_' + fileid
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147
147
148
148
149 class ChangesetController(BaseRepoController):
149 class ChangesetController(BaseRepoController):
150
150
151 def __before__(self):
151 def __before__(self):
152 super(ChangesetController, self).__before__()
152 super(ChangesetController, self).__before__()
153 c.affected_files_cut_off = 60
153 c.affected_files_cut_off = 60
154
154
155 def _index(self, commit_id_range, method):
155 def _index(self, commit_id_range, method):
156 c.ignorews_url = _ignorews_url
156 c.ignorews_url = _ignorews_url
157 c.context_url = _context_url
157 c.context_url = _context_url
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159
159
160 # fetch global flags of ignore ws or context lines
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
163
164 # diff_limit will cut off the whole diff if the limit is applied
164 # diff_limit will cut off the whole diff if the limit is applied
165 # otherwise it will just hide the big files from the front-end
165 # otherwise it will just hide the big files from the front-end
166 diff_limit = self.cut_off_limit_diff
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
167 file_limit = self.cut_off_limit_file
168
168
169 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
170 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
171
171
172 try:
172 try:
173 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 'message', 'parents']
174 'message', 'parents']
175
175
176 if len(commit_range) == 2:
176 if len(commit_range) == 2:
177 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
178 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
179 pre_load=pre_load)
179 pre_load=pre_load)
180 commits = list(commits)
180 commits = list(commits)
181 else:
181 else:
182 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
183 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
184
184
185 c.commit_ranges = commits
185 c.commit_ranges = commits
186 if not c.commit_ranges:
186 if not c.commit_ranges:
187 raise RepositoryError(
187 raise RepositoryError(
188 'The commit range returned an empty result')
188 'The commit range returned an empty result')
189 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
190 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
191 h.flash(msg, category='error')
191 h.flash(msg, category='error')
192 raise HTTPNotFound()
192 raise HTTPNotFound()
193 except Exception:
193 except Exception:
194 log.exception("General failure")
194 log.exception("General failure")
195 raise HTTPNotFound()
195 raise HTTPNotFound()
196
196
197 c.changes = OrderedDict()
197 c.changes = OrderedDict()
198 c.lines_added = 0
198 c.lines_added = 0
199 c.lines_deleted = 0
199 c.lines_deleted = 0
200
200
201 c.commit_statuses = ChangesetStatus.STATUSES
201 c.commit_statuses = ChangesetStatus.STATUSES
202 c.inline_comments = []
202 c.inline_comments = []
203 c.inline_cnt = 0
203 c.inline_cnt = 0
204 c.files = []
204 c.files = []
205
205
206 c.statuses = []
206 c.statuses = []
207 c.comments = []
207 c.comments = []
208 if len(c.commit_ranges) == 1:
208 if len(c.commit_ranges) == 1:
209 commit = c.commit_ranges[0]
209 commit = c.commit_ranges[0]
210 c.comments = ChangesetCommentsModel().get_comments(
210 c.comments = ChangesetCommentsModel().get_comments(
211 c.rhodecode_db_repo.repo_id,
211 c.rhodecode_db_repo.repo_id,
212 revision=commit.raw_id)
212 revision=commit.raw_id)
213 c.statuses.append(ChangesetStatusModel().get_status(
213 c.statuses.append(ChangesetStatusModel().get_status(
214 c.rhodecode_db_repo.repo_id, commit.raw_id))
214 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 # comments from PR
215 # comments from PR
216 statuses = ChangesetStatusModel().get_statuses(
216 statuses = ChangesetStatusModel().get_statuses(
217 c.rhodecode_db_repo.repo_id, commit.raw_id,
217 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 with_revisions=True)
218 with_revisions=True)
219 prs = set(st.pull_request for st in statuses
219 prs = set(st.pull_request for st in statuses
220 if st.pull_request is not None)
220 if st.pull_request is not None)
221 # from associated statuses, check the pull requests, and
221 # from associated statuses, check the pull requests, and
222 # show comments from them
222 # show comments from them
223 for pr in prs:
223 for pr in prs:
224 c.comments.extend(pr.comments)
224 c.comments.extend(pr.comments)
225
225
226 # Iterate over ranges (default commit view is always one commit)
226 # Iterate over ranges (default commit view is always one commit)
227 for commit in c.commit_ranges:
227 for commit in c.commit_ranges:
228 c.changes[commit.raw_id] = []
228 c.changes[commit.raw_id] = []
229
229
230 commit2 = commit
230 commit2 = commit
231 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
231 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
232
232
233 _diff = c.rhodecode_repo.get_diff(
233 _diff = c.rhodecode_repo.get_diff(
234 commit1, commit2,
234 commit1, commit2,
235 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
235 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
236 diff_processor = diffs.DiffProcessor(
236 diff_processor = diffs.DiffProcessor(
237 _diff, format='newdiff', diff_limit=diff_limit,
237 _diff, format='newdiff', diff_limit=diff_limit,
238 file_limit=file_limit, show_full_diff=fulldiff)
238 file_limit=file_limit, show_full_diff=fulldiff)
239
239
240 commit_changes = OrderedDict()
240 commit_changes = OrderedDict()
241 if method == 'show':
241 if method == 'show':
242 _parsed = diff_processor.prepare()
242 _parsed = diff_processor.prepare()
243 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
243 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
244
244
245 _parsed = diff_processor.prepare()
245 _parsed = diff_processor.prepare()
246
246
247 def _node_getter(commit):
247 def _node_getter(commit):
248 def get_node(fname):
248 def get_node(fname):
249 try:
249 try:
250 return commit.get_node(fname)
250 return commit.get_node(fname)
251 except NodeDoesNotExistError:
251 except NodeDoesNotExistError:
252 return None
252 return None
253 return get_node
253 return get_node
254
254
255 inline_comments = ChangesetCommentsModel().get_inline_comments(
255 inline_comments = ChangesetCommentsModel().get_inline_comments(
256 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
256 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
257 c.inline_cnt += len(inline_comments)
257 c.inline_cnt += len(inline_comments)
258
258
259 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
260 repo_name=c.repo_name,
260 repo_name=c.repo_name,
261 source_node_getter=_node_getter(commit1),
261 source_node_getter=_node_getter(commit1),
262 target_node_getter=_node_getter(commit2),
262 target_node_getter=_node_getter(commit2),
263 comments=inline_comments
263 comments=inline_comments
264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
265 c.changes[commit.raw_id] = diffset
265 c.changes[commit.raw_id] = diffset
266 else:
266 else:
267 # downloads/raw we only need RAW diff nothing else
267 # downloads/raw we only need RAW diff nothing else
268 diff = diff_processor.as_raw()
268 diff = diff_processor.as_raw()
269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
270
270
271 # sort comments by how they were generated
271 # sort comments by how they were generated
272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
273
273
274
274
275 if len(c.commit_ranges) == 1:
275 if len(c.commit_ranges) == 1:
276 c.commit = c.commit_ranges[0]
276 c.commit = c.commit_ranges[0]
277 c.parent_tmpl = ''.join(
277 c.parent_tmpl = ''.join(
278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 if method == 'download':
279 if method == 'download':
280 response.content_type = 'text/plain'
280 response.content_type = 'text/plain'
281 response.content_disposition = (
281 response.content_disposition = (
282 'attachment; filename=%s.diff' % commit_id_range[:12])
282 'attachment; filename=%s.diff' % commit_id_range[:12])
283 return diff
283 return diff
284 elif method == 'patch':
284 elif method == 'patch':
285 response.content_type = 'text/plain'
285 response.content_type = 'text/plain'
286 c.diff = safe_unicode(diff)
286 c.diff = safe_unicode(diff)
287 return render('changeset/patch_changeset.html')
287 return render('changeset/patch_changeset.html')
288 elif method == 'raw':
288 elif method == 'raw':
289 response.content_type = 'text/plain'
289 response.content_type = 'text/plain'
290 return diff
290 return diff
291 elif method == 'show':
291 elif method == 'show':
292 if len(c.commit_ranges) == 1:
292 if len(c.commit_ranges) == 1:
293 return render('changeset/changeset.html')
293 return render('changeset/changeset.html')
294 else:
294 else:
295 c.ancestor = None
295 c.ancestor = None
296 c.target_repo = c.rhodecode_db_repo
296 c.target_repo = c.rhodecode_db_repo
297 return render('changeset/changeset_range.html')
297 return render('changeset/changeset_range.html')
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 'repository.admin')
301 'repository.admin')
302 def index(self, revision, method='show'):
302 def index(self, revision, method='show'):
303 return self._index(revision, method=method)
303 return self._index(revision, method=method)
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 'repository.admin')
307 'repository.admin')
308 def changeset_raw(self, revision):
308 def changeset_raw(self, revision):
309 return self._index(revision, method='raw')
309 return self._index(revision, method='raw')
310
310
311 @LoginRequired()
311 @LoginRequired()
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 'repository.admin')
313 'repository.admin')
314 def changeset_patch(self, revision):
314 def changeset_patch(self, revision):
315 return self._index(revision, method='patch')
315 return self._index(revision, method='patch')
316
316
317 @LoginRequired()
317 @LoginRequired()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
319 'repository.admin')
320 def changeset_download(self, revision):
320 def changeset_download(self, revision):
321 return self._index(revision, method='download')
321 return self._index(revision, method='download')
322
322
323 @LoginRequired()
323 @LoginRequired()
324 @NotAnonymous()
324 @NotAnonymous()
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 'repository.admin')
326 'repository.admin')
327 @auth.CSRFRequired()
327 @auth.CSRFRequired()
328 @jsonify
328 @jsonify
329 def comment(self, repo_name, revision):
329 def comment(self, repo_name, revision):
330 commit_id = revision
330 commit_id = revision
331 status = request.POST.get('changeset_status', None)
331 status = request.POST.get('changeset_status', None)
332 text = request.POST.get('text')
332 text = request.POST.get('text')
333 if status:
333 if status:
334 text = text or (_('Status change %(transition_icon)s %(status)s')
334 text = text or (_('Status change %(transition_icon)s %(status)s')
335 % {'transition_icon': '>',
335 % {'transition_icon': '>',
336 'status': ChangesetStatus.get_status_lbl(status)})
336 'status': ChangesetStatus.get_status_lbl(status)})
337
337
338 multi_commit_ids = filter(
338 multi_commit_ids = filter(
339 lambda s: s not in ['', None],
339 lambda s: s not in ['', None],
340 request.POST.get('commit_ids', '').split(','),)
340 request.POST.get('commit_ids', '').split(','),)
341
341
342 commit_ids = multi_commit_ids or [commit_id]
342 commit_ids = multi_commit_ids or [commit_id]
343 comment = None
343 comment = None
344 for current_id in filter(None, commit_ids):
344 for current_id in filter(None, commit_ids):
345 c.co = comment = ChangesetCommentsModel().create(
345 c.co = comment = ChangesetCommentsModel().create(
346 text=text,
346 text=text,
347 repo=c.rhodecode_db_repo.repo_id,
347 repo=c.rhodecode_db_repo.repo_id,
348 user=c.rhodecode_user.user_id,
348 user=c.rhodecode_user.user_id,
349 revision=current_id,
349 revision=current_id,
350 f_path=request.POST.get('f_path'),
350 f_path=request.POST.get('f_path'),
351 line_no=request.POST.get('line'),
351 line_no=request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
352 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
353 if status else None),
354 status_change_type=status
354 status_change_type=status
355 )
355 )
356 # get status if set !
356 # get status if set !
357 if status:
357 if status:
358 # if latest status was from pull request and it's closed
358 # if latest status was from pull request and it's closed
359 # disallow changing status !
359 # disallow changing status !
360 # dont_allow_on_closed_pull_request = True !
360 # dont_allow_on_closed_pull_request = True !
361
361
362 try:
362 try:
363 ChangesetStatusModel().set_status(
363 ChangesetStatusModel().set_status(
364 c.rhodecode_db_repo.repo_id,
364 c.rhodecode_db_repo.repo_id,
365 status,
365 status,
366 c.rhodecode_user.user_id,
366 c.rhodecode_user.user_id,
367 comment,
367 comment,
368 revision=current_id,
368 revision=current_id,
369 dont_allow_on_closed_pull_request=True
369 dont_allow_on_closed_pull_request=True
370 )
370 )
371 except StatusChangeOnClosedPullRequestError:
371 except StatusChangeOnClosedPullRequestError:
372 msg = _('Changing the status of a commit associated with '
372 msg = _('Changing the status of a commit associated with '
373 'a closed pull request is not allowed')
373 'a closed pull request is not allowed')
374 log.exception(msg)
374 log.exception(msg)
375 h.flash(msg, category='warning')
375 h.flash(msg, category='warning')
376 return redirect(h.url(
376 return redirect(h.url(
377 'changeset_home', repo_name=repo_name,
377 'changeset_home', repo_name=repo_name,
378 revision=current_id))
378 revision=current_id))
379
379
380 # finalize, commit and redirect
380 # finalize, commit and redirect
381 Session().commit()
381 Session().commit()
382
382
383 data = {
383 data = {
384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 }
385 }
386 if comment:
386 if comment:
387 data.update(comment.get_dict())
387 data.update(comment.get_dict())
388 data.update({'rendered_text':
388 data.update({'rendered_text':
389 render('changeset/changeset_comment_block.html')})
389 render('changeset/changeset_comment_block.html')})
390
390
391 return data
391 return data
392
392
393 @LoginRequired()
393 @LoginRequired()
394 @NotAnonymous()
394 @NotAnonymous()
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 'repository.admin')
396 'repository.admin')
397 @auth.CSRFRequired()
397 @auth.CSRFRequired()
398 def preview_comment(self):
398 def preview_comment(self):
399 # Technically a CSRF token is not needed as no state changes with this
399 # Technically a CSRF token is not needed as no state changes with this
400 # call. However, as this is a POST is better to have it, so automated
400 # call. However, as this is a POST is better to have it, so automated
401 # tools don't flag it as potential CSRF.
401 # tools don't flag it as potential CSRF.
402 # Post is required because the payload could be bigger than the maximum
402 # Post is required because the payload could be bigger than the maximum
403 # allowed by GET.
403 # allowed by GET.
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 raise HTTPBadRequest()
405 raise HTTPBadRequest()
406 text = request.POST.get('text')
406 text = request.POST.get('text')
407 renderer = request.POST.get('renderer') or 'rst'
407 renderer = request.POST.get('renderer') or 'rst'
408 if text:
408 if text:
409 return h.render(text, renderer=renderer, mentions=True)
409 return h.render(text, renderer=renderer, mentions=True)
410 return ''
410 return ''
411
411
412 @LoginRequired()
412 @LoginRequired()
413 @NotAnonymous()
413 @NotAnonymous()
414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 'repository.admin')
415 'repository.admin')
416 @auth.CSRFRequired()
416 @auth.CSRFRequired()
417 @jsonify
417 @jsonify
418 def delete_comment(self, repo_name, comment_id):
418 def delete_comment(self, repo_name, comment_id):
419 comment = ChangesetComment.get(comment_id)
419 comment = ChangesetComment.get(comment_id)
420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 ChangesetCommentsModel().delete(comment=comment)
423 ChangesetCommentsModel().delete(comment=comment)
424 Session().commit()
424 Session().commit()
425 return True
425 return True
426 else:
426 else:
427 raise HTTPForbidden()
427 raise HTTPForbidden()
428
428
429 @LoginRequired()
429 @LoginRequired()
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 'repository.admin')
431 'repository.admin')
432 @jsonify
432 @jsonify
433 def changeset_info(self, repo_name, revision):
433 def changeset_info(self, repo_name, revision):
434 if request.is_xhr:
434 if request.is_xhr:
435 try:
435 try:
436 return c.rhodecode_repo.get_commit(commit_id=revision)
436 return c.rhodecode_repo.get_commit(commit_id=revision)
437 except CommitDoesNotExistError as e:
437 except CommitDoesNotExistError as e:
438 return EmptyCommit(message=str(e))
438 return EmptyCommit(message=str(e))
439 else:
439 else:
440 raise HTTPBadRequest()
440 raise HTTPBadRequest()
441
441
442 @LoginRequired()
442 @LoginRequired()
443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 'repository.admin')
444 'repository.admin')
445 @jsonify
445 @jsonify
446 def changeset_children(self, repo_name, revision):
446 def changeset_children(self, repo_name, revision):
447 if request.is_xhr:
447 if request.is_xhr:
448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 result = {"results": commit.children}
449 result = {"results": commit.children}
450 return result
450 return result
451 else:
451 else:
452 raise HTTPBadRequest()
452 raise HTTPBadRequest()
453
453
454 @LoginRequired()
454 @LoginRequired()
455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 'repository.admin')
456 'repository.admin')
457 @jsonify
457 @jsonify
458 def changeset_parents(self, repo_name, revision):
458 def changeset_parents(self, repo_name, revision):
459 if request.is_xhr:
459 if request.is_xhr:
460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 result = {"results": commit.parents}
461 result = {"results": commit.parents}
462 return result
462 return result
463 else:
463 else:
464 raise HTTPBadRequest()
464 raise HTTPBadRequest()
@@ -1,889 +1,911 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import peppercorn
25 import peppercorn
26 import formencode
26 import formencode
27 import logging
27 import logging
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from sqlalchemy.sql import func
34 from sqlalchemy.sql import func
35 from sqlalchemy.sql.expression import or_
35 from sqlalchemy.sql.expression import or_
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.base import (
40 from rhodecode.lib.base import (
41 BaseRepoController, render, vcs_operation_context)
41 BaseRepoController, render, vcs_operation_context)
42 from rhodecode.lib.auth import (
42 from rhodecode.lib.auth import (
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 HasAcceptedRepoType, XHRRequired)
44 HasAcceptedRepoType, XHRRequired)
45 from rhodecode.lib.channelstream import channelstream_request
45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.compat import OrderedDict
46 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 NodeDoesNotExistError)
51 from rhodecode.lib.diffs import LimitedDiffContainer
53 from rhodecode.lib.diffs import LimitedDiffContainer
52 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.comment import ChangesetCommentsModel
55 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 Repository
57 Repository
56 from rhodecode.model.forms import PullRequestForm
58 from rhodecode.model.forms import PullRequestForm
57 from rhodecode.model.meta import Session
59 from rhodecode.model.meta import Session
58 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.pull_request import PullRequestModel
59
61
60 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
61
63
62
64
63 class PullrequestsController(BaseRepoController):
65 class PullrequestsController(BaseRepoController):
64 def __before__(self):
66 def __before__(self):
65 super(PullrequestsController, self).__before__()
67 super(PullrequestsController, self).__before__()
66
68
67 def _load_compare_data(self, pull_request, enable_comments=True):
69 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
68 """
70 """
69 Load context data needed for generating compare diff
71 Load context data needed for generating compare diff
70
72
71 :param pull_request: object related to the request
73 :param pull_request: object related to the request
72 :param enable_comments: flag to determine if comments are included
74 :param enable_comments: flag to determine if comments are included
73 """
75 """
74 source_repo = pull_request.source_repo
76 source_repo = pull_request.source_repo
75 source_ref_id = pull_request.source_ref_parts.commit_id
77 source_ref_id = pull_request.source_ref_parts.commit_id
76
78
77 target_repo = pull_request.target_repo
79 target_repo = pull_request.target_repo
78 target_ref_id = pull_request.target_ref_parts.commit_id
80 target_ref_id = pull_request.target_ref_parts.commit_id
79
81
80 # despite opening commits for bookmarks/branches/tags, we always
82 # despite opening commits for bookmarks/branches/tags, we always
81 # convert this to rev to prevent changes after bookmark or branch change
83 # convert this to rev to prevent changes after bookmark or branch change
82 c.source_ref_type = 'rev'
84 c.source_ref_type = 'rev'
83 c.source_ref = source_ref_id
85 c.source_ref = source_ref_id
84
86
85 c.target_ref_type = 'rev'
87 c.target_ref_type = 'rev'
86 c.target_ref = target_ref_id
88 c.target_ref = target_ref_id
87
89
88 c.source_repo = source_repo
90 c.source_repo = source_repo
89 c.target_repo = target_repo
91 c.target_repo = target_repo
90
92
91 c.fulldiff = bool(request.GET.get('fulldiff'))
93 c.fulldiff = bool(request.GET.get('fulldiff'))
92
94
93 # diff_limit is the old behavior, will cut off the whole diff
95 # diff_limit is the old behavior, will cut off the whole diff
94 # if the limit is applied otherwise will just hide the
96 # if the limit is applied otherwise will just hide the
95 # big files from the front-end
97 # big files from the front-end
96 diff_limit = self.cut_off_limit_diff
98 diff_limit = self.cut_off_limit_diff
97 file_limit = self.cut_off_limit_file
99 file_limit = self.cut_off_limit_file
98
100
99 pre_load = ["author", "branch", "date", "message"]
101 pre_load = ["author", "branch", "date", "message"]
100
102
101 c.commit_ranges = []
103 c.commit_ranges = []
102 source_commit = EmptyCommit()
104 source_commit = EmptyCommit()
103 target_commit = EmptyCommit()
105 target_commit = EmptyCommit()
104 c.missing_requirements = False
106 c.missing_requirements = False
105 try:
107 try:
106 c.commit_ranges = [
108 c.commit_ranges = [
107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
109 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 for rev in pull_request.revisions]
110 for rev in pull_request.revisions]
109
111
110 c.statuses = source_repo.statuses(
112 c.statuses = source_repo.statuses(
111 [x.raw_id for x in c.commit_ranges])
113 [x.raw_id for x in c.commit_ranges])
112
114
113 target_commit = source_repo.get_commit(
115 target_commit = source_repo.get_commit(
114 commit_id=safe_str(target_ref_id))
116 commit_id=safe_str(target_ref_id))
115 source_commit = source_repo.get_commit(
117 source_commit = source_repo.get_commit(
116 commit_id=safe_str(source_ref_id))
118 commit_id=safe_str(source_ref_id))
117 except RepositoryRequirementError:
119 except RepositoryRequirementError:
118 c.missing_requirements = True
120 c.missing_requirements = True
119
121
122 c.changes = {}
120 c.missing_commits = False
123 c.missing_commits = False
121 if (c.missing_requirements or
124 if (c.missing_requirements or
122 isinstance(source_commit, EmptyCommit) or
125 isinstance(source_commit, EmptyCommit) or
123 source_commit == target_commit):
126 source_commit == target_commit):
124 _parsed = []
127 _parsed = []
125 c.missing_commits = True
128 c.missing_commits = True
126 else:
129 else:
127 vcs_diff = PullRequestModel().get_diff(pull_request)
130 vcs_diff = PullRequestModel().get_diff(pull_request)
128 diff_processor = diffs.DiffProcessor(
131 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
132 vcs_diff, format='newdiff', diff_limit=diff_limit,
130 file_limit=file_limit, show_full_diff=c.fulldiff)
133 file_limit=file_limit, show_full_diff=c.fulldiff)
131 _parsed = diff_processor.prepare()
134 _parsed = diff_processor.prepare()
132
135
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
136 commit_changes = OrderedDict()
137 _parsed = diff_processor.prepare()
138 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139
140 _parsed = diff_processor.prepare()
141
142 def _node_getter(commit):
143 def get_node(fname):
144 try:
145 return commit.get_node(fname)
146 except NodeDoesNotExistError:
147 return None
148 return get_node
149
150 c.diffset = codeblocks.DiffSet(
151 repo_name=c.repo_name,
152 source_node_getter=_node_getter(target_commit),
153 target_node_getter=_node_getter(source_commit),
154 comments=inline_comments
155 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
156
134
157
135 c.files = []
158 c.files = []
136 c.changes = {}
159 c.changes = {}
137 c.lines_added = 0
160 c.lines_added = 0
138 c.lines_deleted = 0
161 c.lines_deleted = 0
139 c.included_files = []
162 c.included_files = []
140 c.deleted_files = []
163 c.deleted_files = []
141
164
142 for f in _parsed:
165 # for f in _parsed:
143 st = f['stats']
166 # st = f['stats']
144 c.lines_added += st['added']
167 # c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
168 # c.lines_deleted += st['deleted']
146
169
147 fid = h.FID('', f['filename'])
170 # fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
171 # c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
172 # c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
173 # html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
174 # parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
175 # c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153
176
154 def _extract_ordering(self, request):
177 def _extract_ordering(self, request):
155 column_index = safe_int(request.GET.get('order[0][column]'))
178 column_index = safe_int(request.GET.get('order[0][column]'))
156 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_dir = request.GET.get('order[0][dir]', 'desc')
157 order_by = request.GET.get(
180 order_by = request.GET.get(
158 'columns[%s][data][sort]' % column_index, 'name_raw')
181 'columns[%s][data][sort]' % column_index, 'name_raw')
159 return order_by, order_dir
182 return order_by, order_dir
160
183
161 @LoginRequired()
184 @LoginRequired()
162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 'repository.admin')
186 'repository.admin')
164 @HasAcceptedRepoType('git', 'hg')
187 @HasAcceptedRepoType('git', 'hg')
165 def show_all(self, repo_name):
188 def show_all(self, repo_name):
166 # filter types
189 # filter types
167 c.active = 'open'
190 c.active = 'open'
168 c.source = str2bool(request.GET.get('source'))
191 c.source = str2bool(request.GET.get('source'))
169 c.closed = str2bool(request.GET.get('closed'))
192 c.closed = str2bool(request.GET.get('closed'))
170 c.my = str2bool(request.GET.get('my'))
193 c.my = str2bool(request.GET.get('my'))
171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 c.repo_name = repo_name
196 c.repo_name = repo_name
174
197
175 opened_by = None
198 opened_by = None
176 if c.my:
199 if c.my:
177 c.active = 'my'
200 c.active = 'my'
178 opened_by = [c.rhodecode_user.user_id]
201 opened_by = [c.rhodecode_user.user_id]
179
202
180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 if c.closed:
204 if c.closed:
182 c.active = 'closed'
205 c.active = 'closed'
183 statuses = [PullRequest.STATUS_CLOSED]
206 statuses = [PullRequest.STATUS_CLOSED]
184
207
185 if c.awaiting_review and not c.source:
208 if c.awaiting_review and not c.source:
186 c.active = 'awaiting'
209 c.active = 'awaiting'
187 if c.source and not c.awaiting_review:
210 if c.source and not c.awaiting_review:
188 c.active = 'source'
211 c.active = 'source'
189 if c.awaiting_my_review:
212 if c.awaiting_my_review:
190 c.active = 'awaiting_my'
213 c.active = 'awaiting_my'
191
214
192 data = self._get_pull_requests_list(
215 data = self._get_pull_requests_list(
193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 if not request.is_xhr:
217 if not request.is_xhr:
195 c.data = json.dumps(data['data'])
218 c.data = json.dumps(data['data'])
196 c.records_total = data['recordsTotal']
219 c.records_total = data['recordsTotal']
197 return render('/pullrequests/pullrequests.html')
220 return render('/pullrequests/pullrequests.html')
198 else:
221 else:
199 return json.dumps(data)
222 return json.dumps(data)
200
223
201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 # pagination
225 # pagination
203 start = safe_int(request.GET.get('start'), 0)
226 start = safe_int(request.GET.get('start'), 0)
204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 order_by, order_dir = self._extract_ordering(request)
228 order_by, order_dir = self._extract_ordering(request)
206
229
207 if c.awaiting_review:
230 if c.awaiting_review:
208 pull_requests = PullRequestModel().get_awaiting_review(
231 pull_requests = PullRequestModel().get_awaiting_review(
209 repo_name, source=c.source, opened_by=opened_by,
232 repo_name, source=c.source, opened_by=opened_by,
210 statuses=statuses, offset=start, length=length,
233 statuses=statuses, offset=start, length=length,
211 order_by=order_by, order_dir=order_dir)
234 order_by=order_by, order_dir=order_dir)
212 pull_requests_total_count = PullRequestModel(
235 pull_requests_total_count = PullRequestModel(
213 ).count_awaiting_review(
236 ).count_awaiting_review(
214 repo_name, source=c.source, statuses=statuses,
237 repo_name, source=c.source, statuses=statuses,
215 opened_by=opened_by)
238 opened_by=opened_by)
216 elif c.awaiting_my_review:
239 elif c.awaiting_my_review:
217 pull_requests = PullRequestModel().get_awaiting_my_review(
240 pull_requests = PullRequestModel().get_awaiting_my_review(
218 repo_name, source=c.source, opened_by=opened_by,
241 repo_name, source=c.source, opened_by=opened_by,
219 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 offset=start, length=length, order_by=order_by,
243 offset=start, length=length, order_by=order_by,
221 order_dir=order_dir)
244 order_dir=order_dir)
222 pull_requests_total_count = PullRequestModel(
245 pull_requests_total_count = PullRequestModel(
223 ).count_awaiting_my_review(
246 ).count_awaiting_my_review(
224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 statuses=statuses, opened_by=opened_by)
248 statuses=statuses, opened_by=opened_by)
226 else:
249 else:
227 pull_requests = PullRequestModel().get_all(
250 pull_requests = PullRequestModel().get_all(
228 repo_name, source=c.source, opened_by=opened_by,
251 repo_name, source=c.source, opened_by=opened_by,
229 statuses=statuses, offset=start, length=length,
252 statuses=statuses, offset=start, length=length,
230 order_by=order_by, order_dir=order_dir)
253 order_by=order_by, order_dir=order_dir)
231 pull_requests_total_count = PullRequestModel().count_all(
254 pull_requests_total_count = PullRequestModel().count_all(
232 repo_name, source=c.source, statuses=statuses,
255 repo_name, source=c.source, statuses=statuses,
233 opened_by=opened_by)
256 opened_by=opened_by)
234
257
235 from rhodecode.lib.utils import PartialRenderer
258 from rhodecode.lib.utils import PartialRenderer
236 _render = PartialRenderer('data_table/_dt_elements.html')
259 _render = PartialRenderer('data_table/_dt_elements.html')
237 data = []
260 data = []
238 for pr in pull_requests:
261 for pr in pull_requests:
239 comments = ChangesetCommentsModel().get_all_comments(
262 comments = ChangesetCommentsModel().get_all_comments(
240 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 c.rhodecode_db_repo.repo_id, pull_request=pr)
241
264
242 data.append({
265 data.append({
243 'name': _render('pullrequest_name',
266 'name': _render('pullrequest_name',
244 pr.pull_request_id, pr.target_repo.repo_name),
267 pr.pull_request_id, pr.target_repo.repo_name),
245 'name_raw': pr.pull_request_id,
268 'name_raw': pr.pull_request_id,
246 'status': _render('pullrequest_status',
269 'status': _render('pullrequest_status',
247 pr.calculated_review_status()),
270 pr.calculated_review_status()),
248 'title': _render(
271 'title': _render(
249 'pullrequest_title', pr.title, pr.description),
272 'pullrequest_title', pr.title, pr.description),
250 'description': h.escape(pr.description),
273 'description': h.escape(pr.description),
251 'updated_on': _render('pullrequest_updated_on',
274 'updated_on': _render('pullrequest_updated_on',
252 h.datetime_to_time(pr.updated_on)),
275 h.datetime_to_time(pr.updated_on)),
253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 'created_on': _render('pullrequest_updated_on',
277 'created_on': _render('pullrequest_updated_on',
255 h.datetime_to_time(pr.created_on)),
278 h.datetime_to_time(pr.created_on)),
256 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'created_on_raw': h.datetime_to_time(pr.created_on),
257 'author': _render('pullrequest_author',
280 'author': _render('pullrequest_author',
258 pr.author.full_contact, ),
281 pr.author.full_contact, ),
259 'author_raw': pr.author.full_name,
282 'author_raw': pr.author.full_name,
260 'comments': _render('pullrequest_comments', len(comments)),
283 'comments': _render('pullrequest_comments', len(comments)),
261 'comments_raw': len(comments),
284 'comments_raw': len(comments),
262 'closed': pr.is_closed(),
285 'closed': pr.is_closed(),
263 })
286 })
264 # json used to render the grid
287 # json used to render the grid
265 data = ({
288 data = ({
266 'data': data,
289 'data': data,
267 'recordsTotal': pull_requests_total_count,
290 'recordsTotal': pull_requests_total_count,
268 'recordsFiltered': pull_requests_total_count,
291 'recordsFiltered': pull_requests_total_count,
269 })
292 })
270 return data
293 return data
271
294
272 @LoginRequired()
295 @LoginRequired()
273 @NotAnonymous()
296 @NotAnonymous()
274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 'repository.admin')
298 'repository.admin')
276 @HasAcceptedRepoType('git', 'hg')
299 @HasAcceptedRepoType('git', 'hg')
277 def index(self):
300 def index(self):
278 source_repo = c.rhodecode_db_repo
301 source_repo = c.rhodecode_db_repo
279
302
280 try:
303 try:
281 source_repo.scm_instance().get_commit()
304 source_repo.scm_instance().get_commit()
282 except EmptyRepositoryError:
305 except EmptyRepositoryError:
283 h.flash(h.literal(_('There are no commits yet')),
306 h.flash(h.literal(_('There are no commits yet')),
284 category='warning')
307 category='warning')
285 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 redirect(url('summary_home', repo_name=source_repo.repo_name))
286
309
287 commit_id = request.GET.get('commit')
310 commit_id = request.GET.get('commit')
288 branch_ref = request.GET.get('branch')
311 branch_ref = request.GET.get('branch')
289 bookmark_ref = request.GET.get('bookmark')
312 bookmark_ref = request.GET.get('bookmark')
290
313
291 try:
314 try:
292 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo_data = PullRequestModel().generate_repo_data(
293 source_repo, commit_id=commit_id,
316 source_repo, commit_id=commit_id,
294 branch=branch_ref, bookmark=bookmark_ref)
317 branch=branch_ref, bookmark=bookmark_ref)
295 except CommitDoesNotExistError as e:
318 except CommitDoesNotExistError as e:
296 log.exception(e)
319 log.exception(e)
297 h.flash(_('Commit does not exist'), 'error')
320 h.flash(_('Commit does not exist'), 'error')
298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299
322
300 default_target_repo = source_repo
323 default_target_repo = source_repo
301
324
302 if source_repo.parent:
325 if source_repo.parent:
303 parent_vcs_obj = source_repo.parent.scm_instance()
326 parent_vcs_obj = source_repo.parent.scm_instance()
304 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 # change default if we have a parent repo
328 # change default if we have a parent repo
306 default_target_repo = source_repo.parent
329 default_target_repo = source_repo.parent
307
330
308 target_repo_data = PullRequestModel().generate_repo_data(
331 target_repo_data = PullRequestModel().generate_repo_data(
309 default_target_repo)
332 default_target_repo)
310
333
311 selected_source_ref = source_repo_data['refs']['selected_ref']
334 selected_source_ref = source_repo_data['refs']['selected_ref']
312
335
313 title_source_ref = selected_source_ref.split(':', 2)[1]
336 title_source_ref = selected_source_ref.split(':', 2)[1]
314 c.default_title = PullRequestModel().generate_pullrequest_title(
337 c.default_title = PullRequestModel().generate_pullrequest_title(
315 source=source_repo.repo_name,
338 source=source_repo.repo_name,
316 source_ref=title_source_ref,
339 source_ref=title_source_ref,
317 target=default_target_repo.repo_name
340 target=default_target_repo.repo_name
318 )
341 )
319
342
320 c.default_repo_data = {
343 c.default_repo_data = {
321 'source_repo_name': source_repo.repo_name,
344 'source_repo_name': source_repo.repo_name,
322 'source_refs_json': json.dumps(source_repo_data),
345 'source_refs_json': json.dumps(source_repo_data),
323 'target_repo_name': default_target_repo.repo_name,
346 'target_repo_name': default_target_repo.repo_name,
324 'target_refs_json': json.dumps(target_repo_data),
347 'target_refs_json': json.dumps(target_repo_data),
325 }
348 }
326 c.default_source_ref = selected_source_ref
349 c.default_source_ref = selected_source_ref
327
350
328 return render('/pullrequests/pullrequest.html')
351 return render('/pullrequests/pullrequest.html')
329
352
330 @LoginRequired()
353 @LoginRequired()
331 @NotAnonymous()
354 @NotAnonymous()
332 @XHRRequired()
355 @XHRRequired()
333 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 'repository.admin')
357 'repository.admin')
335 @jsonify
358 @jsonify
336 def get_repo_refs(self, repo_name, target_repo_name):
359 def get_repo_refs(self, repo_name, target_repo_name):
337 repo = Repository.get_by_repo_name(target_repo_name)
360 repo = Repository.get_by_repo_name(target_repo_name)
338 if not repo:
361 if not repo:
339 raise HTTPNotFound
362 raise HTTPNotFound
340 return PullRequestModel().generate_repo_data(repo)
363 return PullRequestModel().generate_repo_data(repo)
341
364
342 @LoginRequired()
365 @LoginRequired()
343 @NotAnonymous()
366 @NotAnonymous()
344 @XHRRequired()
367 @XHRRequired()
345 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 'repository.admin')
369 'repository.admin')
347 @jsonify
370 @jsonify
348 def get_repo_destinations(self, repo_name):
371 def get_repo_destinations(self, repo_name):
349 repo = Repository.get_by_repo_name(repo_name)
372 repo = Repository.get_by_repo_name(repo_name)
350 if not repo:
373 if not repo:
351 raise HTTPNotFound
374 raise HTTPNotFound
352 filter_query = request.GET.get('query')
375 filter_query = request.GET.get('query')
353
376
354 query = Repository.query() \
377 query = Repository.query() \
355 .order_by(func.length(Repository.repo_name)) \
378 .order_by(func.length(Repository.repo_name)) \
356 .filter(or_(
379 .filter(or_(
357 Repository.repo_name == repo.repo_name,
380 Repository.repo_name == repo.repo_name,
358 Repository.fork_id == repo.repo_id))
381 Repository.fork_id == repo.repo_id))
359
382
360 if filter_query:
383 if filter_query:
361 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 query = query.filter(
385 query = query.filter(
363 Repository.repo_name.ilike(ilike_expression))
386 Repository.repo_name.ilike(ilike_expression))
364
387
365 add_parent = False
388 add_parent = False
366 if repo.parent:
389 if repo.parent:
367 if filter_query in repo.parent.repo_name:
390 if filter_query in repo.parent.repo_name:
368 parent_vcs_obj = repo.parent.scm_instance()
391 parent_vcs_obj = repo.parent.scm_instance()
369 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 add_parent = True
393 add_parent = True
371
394
372 limit = 20 - 1 if add_parent else 20
395 limit = 20 - 1 if add_parent else 20
373 all_repos = query.limit(limit).all()
396 all_repos = query.limit(limit).all()
374 if add_parent:
397 if add_parent:
375 all_repos += [repo.parent]
398 all_repos += [repo.parent]
376
399
377 repos = []
400 repos = []
378 for obj in self.scm_model.get_repos(all_repos):
401 for obj in self.scm_model.get_repos(all_repos):
379 repos.append({
402 repos.append({
380 'id': obj['name'],
403 'id': obj['name'],
381 'text': obj['name'],
404 'text': obj['name'],
382 'type': 'repo',
405 'type': 'repo',
383 'obj': obj['dbrepo']
406 'obj': obj['dbrepo']
384 })
407 })
385
408
386 data = {
409 data = {
387 'more': False,
410 'more': False,
388 'results': [{
411 'results': [{
389 'text': _('Repositories'),
412 'text': _('Repositories'),
390 'children': repos
413 'children': repos
391 }] if repos else []
414 }] if repos else []
392 }
415 }
393 return data
416 return data
394
417
395 @LoginRequired()
418 @LoginRequired()
396 @NotAnonymous()
419 @NotAnonymous()
397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 'repository.admin')
421 'repository.admin')
399 @HasAcceptedRepoType('git', 'hg')
422 @HasAcceptedRepoType('git', 'hg')
400 @auth.CSRFRequired()
423 @auth.CSRFRequired()
401 def create(self, repo_name):
424 def create(self, repo_name):
402 repo = Repository.get_by_repo_name(repo_name)
425 repo = Repository.get_by_repo_name(repo_name)
403 if not repo:
426 if not repo:
404 raise HTTPNotFound
427 raise HTTPNotFound
405
428
406 controls = peppercorn.parse(request.POST.items())
429 controls = peppercorn.parse(request.POST.items())
407
430
408 try:
431 try:
409 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 except formencode.Invalid as errors:
433 except formencode.Invalid as errors:
411 if errors.error_dict.get('revisions'):
434 if errors.error_dict.get('revisions'):
412 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 elif errors.error_dict.get('pullrequest_title'):
436 elif errors.error_dict.get('pullrequest_title'):
414 msg = _('Pull request requires a title with min. 3 chars')
437 msg = _('Pull request requires a title with min. 3 chars')
415 else:
438 else:
416 msg = _('Error creating pull request: {}').format(errors)
439 msg = _('Error creating pull request: {}').format(errors)
417 log.exception(msg)
440 log.exception(msg)
418 h.flash(msg, 'error')
441 h.flash(msg, 'error')
419
442
420 # would rather just go back to form ...
443 # would rather just go back to form ...
421 return redirect(url('pullrequest_home', repo_name=repo_name))
444 return redirect(url('pullrequest_home', repo_name=repo_name))
422
445
423 source_repo = _form['source_repo']
446 source_repo = _form['source_repo']
424 source_ref = _form['source_ref']
447 source_ref = _form['source_ref']
425 target_repo = _form['target_repo']
448 target_repo = _form['target_repo']
426 target_ref = _form['target_ref']
449 target_ref = _form['target_ref']
427 commit_ids = _form['revisions'][::-1]
450 commit_ids = _form['revisions'][::-1]
428 reviewers = [
451 reviewers = [
429 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 (r['user_id'], r['reasons']) for r in _form['review_members']]
430
453
431 # find the ancestor for this pr
454 # find the ancestor for this pr
432 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434
457
435 source_scm = source_db_repo.scm_instance()
458 source_scm = source_db_repo.scm_instance()
436 target_scm = target_db_repo.scm_instance()
459 target_scm = target_db_repo.scm_instance()
437
460
438 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440
463
441 ancestor = source_scm.get_common_ancestor(
464 ancestor = source_scm.get_common_ancestor(
442 source_commit.raw_id, target_commit.raw_id, target_scm)
465 source_commit.raw_id, target_commit.raw_id, target_scm)
443
466
444 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446
469
447 pullrequest_title = _form['pullrequest_title']
470 pullrequest_title = _form['pullrequest_title']
448 title_source_ref = source_ref.split(':', 2)[1]
471 title_source_ref = source_ref.split(':', 2)[1]
449 if not pullrequest_title:
472 if not pullrequest_title:
450 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 source=source_repo,
474 source=source_repo,
452 source_ref=title_source_ref,
475 source_ref=title_source_ref,
453 target=target_repo
476 target=target_repo
454 )
477 )
455
478
456 description = _form['pullrequest_desc']
479 description = _form['pullrequest_desc']
457 try:
480 try:
458 pull_request = PullRequestModel().create(
481 pull_request = PullRequestModel().create(
459 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 target_ref, commit_ids, reviewers, pullrequest_title,
483 target_ref, commit_ids, reviewers, pullrequest_title,
461 description
484 description
462 )
485 )
463 Session().commit()
486 Session().commit()
464 h.flash(_('Successfully opened new pull request'),
487 h.flash(_('Successfully opened new pull request'),
465 category='success')
488 category='success')
466 except Exception as e:
489 except Exception as e:
467 msg = _('Error occurred during sending pull request')
490 msg = _('Error occurred during sending pull request')
468 log.exception(msg)
491 log.exception(msg)
469 h.flash(msg, category='error')
492 h.flash(msg, category='error')
470 return redirect(url('pullrequest_home', repo_name=repo_name))
493 return redirect(url('pullrequest_home', repo_name=repo_name))
471
494
472 return redirect(url('pullrequest_show', repo_name=target_repo,
495 return redirect(url('pullrequest_show', repo_name=target_repo,
473 pull_request_id=pull_request.pull_request_id))
496 pull_request_id=pull_request.pull_request_id))
474
497
475 @LoginRequired()
498 @LoginRequired()
476 @NotAnonymous()
499 @NotAnonymous()
477 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 'repository.admin')
501 'repository.admin')
479 @auth.CSRFRequired()
502 @auth.CSRFRequired()
480 @jsonify
503 @jsonify
481 def update(self, repo_name, pull_request_id):
504 def update(self, repo_name, pull_request_id):
482 pull_request_id = safe_int(pull_request_id)
505 pull_request_id = safe_int(pull_request_id)
483 pull_request = PullRequest.get_or_404(pull_request_id)
506 pull_request = PullRequest.get_or_404(pull_request_id)
484 # only owner or admin can update it
507 # only owner or admin can update it
485 allowed_to_update = PullRequestModel().check_user_update(
508 allowed_to_update = PullRequestModel().check_user_update(
486 pull_request, c.rhodecode_user)
509 pull_request, c.rhodecode_user)
487 if allowed_to_update:
510 if allowed_to_update:
488 controls = peppercorn.parse(request.POST.items())
511 controls = peppercorn.parse(request.POST.items())
489
512
490 if 'review_members' in controls:
513 if 'review_members' in controls:
491 self._update_reviewers(
514 self._update_reviewers(
492 pull_request_id, controls['review_members'])
515 pull_request_id, controls['review_members'])
493 elif str2bool(request.POST.get('update_commits', 'false')):
516 elif str2bool(request.POST.get('update_commits', 'false')):
494 self._update_commits(pull_request)
517 self._update_commits(pull_request)
495 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 self._reject_close(pull_request)
519 self._reject_close(pull_request)
497 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 self._edit_pull_request(pull_request)
521 self._edit_pull_request(pull_request)
499 else:
522 else:
500 raise HTTPBadRequest()
523 raise HTTPBadRequest()
501 return True
524 return True
502 raise HTTPForbidden()
525 raise HTTPForbidden()
503
526
504 def _edit_pull_request(self, pull_request):
527 def _edit_pull_request(self, pull_request):
505 try:
528 try:
506 PullRequestModel().edit(
529 PullRequestModel().edit(
507 pull_request, request.POST.get('title'),
530 pull_request, request.POST.get('title'),
508 request.POST.get('description'))
531 request.POST.get('description'))
509 except ValueError:
532 except ValueError:
510 msg = _(u'Cannot update closed pull requests.')
533 msg = _(u'Cannot update closed pull requests.')
511 h.flash(msg, category='error')
534 h.flash(msg, category='error')
512 return
535 return
513 else:
536 else:
514 Session().commit()
537 Session().commit()
515
538
516 msg = _(u'Pull request title & description updated.')
539 msg = _(u'Pull request title & description updated.')
517 h.flash(msg, category='success')
540 h.flash(msg, category='success')
518 return
541 return
519
542
520 def _update_commits(self, pull_request):
543 def _update_commits(self, pull_request):
521 resp = PullRequestModel().update_commits(pull_request)
544 resp = PullRequestModel().update_commits(pull_request)
522
545
523 if resp.executed:
546 if resp.executed:
524 msg = _(
547 msg = _(
525 u'Pull request updated to "{source_commit_id}" with '
548 u'Pull request updated to "{source_commit_id}" with '
526 u'{count_added} added, {count_removed} removed commits.')
549 u'{count_added} added, {count_removed} removed commits.')
527 msg = msg.format(
550 msg = msg.format(
528 source_commit_id=pull_request.source_ref_parts.commit_id,
551 source_commit_id=pull_request.source_ref_parts.commit_id,
529 count_added=len(resp.changes.added),
552 count_added=len(resp.changes.added),
530 count_removed=len(resp.changes.removed))
553 count_removed=len(resp.changes.removed))
531 h.flash(msg, category='success')
554 h.flash(msg, category='success')
532
555
533 registry = get_current_registry()
556 registry = get_current_registry()
534 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
535 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 channelstream_config = rhodecode_plugins.get('channelstream', {})
536 if channelstream_config.get('enabled'):
559 if channelstream_config.get('enabled'):
537 message = msg + (
560 message = msg + (
538 ' - <a onclick="window.location.reload()">'
561 ' - <a onclick="window.location.reload()">'
539 '<strong>{}</strong></a>'.format(_('Reload page')))
562 '<strong>{}</strong></a>'.format(_('Reload page')))
540 channel = '/repo${}$/pr/{}'.format(
563 channel = '/repo${}$/pr/{}'.format(
541 pull_request.target_repo.repo_name,
564 pull_request.target_repo.repo_name,
542 pull_request.pull_request_id
565 pull_request.pull_request_id
543 )
566 )
544 payload = {
567 payload = {
545 'type': 'message',
568 'type': 'message',
546 'user': 'system',
569 'user': 'system',
547 'exclude_users': [request.user.username],
570 'exclude_users': [request.user.username],
548 'channel': channel,
571 'channel': channel,
549 'message': {
572 'message': {
550 'message': message,
573 'message': message,
551 'level': 'success',
574 'level': 'success',
552 'topic': '/notifications'
575 'topic': '/notifications'
553 }
576 }
554 }
577 }
555 channelstream_request(
578 channelstream_request(
556 channelstream_config, [payload], '/message',
579 channelstream_config, [payload], '/message',
557 raise_exc=False)
580 raise_exc=False)
558 else:
581 else:
559 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
560 warning_reasons = [
583 warning_reasons = [
561 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.NO_CHANGE,
562 UpdateFailureReason.WRONG_REF_TPYE,
585 UpdateFailureReason.WRONG_REF_TPYE,
563 ]
586 ]
564 category = 'warning' if resp.reason in warning_reasons else 'error'
587 category = 'warning' if resp.reason in warning_reasons else 'error'
565 h.flash(msg, category=category)
588 h.flash(msg, category=category)
566
589
567 @auth.CSRFRequired()
590 @auth.CSRFRequired()
568 @LoginRequired()
591 @LoginRequired()
569 @NotAnonymous()
592 @NotAnonymous()
570 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
571 'repository.admin')
594 'repository.admin')
572 def merge(self, repo_name, pull_request_id):
595 def merge(self, repo_name, pull_request_id):
573 """
596 """
574 POST /{repo_name}/pull-request/{pull_request_id}
597 POST /{repo_name}/pull-request/{pull_request_id}
575
598
576 Merge will perform a server-side merge of the specified
599 Merge will perform a server-side merge of the specified
577 pull request, if the pull request is approved and mergeable.
600 pull request, if the pull request is approved and mergeable.
578 After succesfull merging, the pull request is automatically
601 After succesfull merging, the pull request is automatically
579 closed, with a relevant comment.
602 closed, with a relevant comment.
580 """
603 """
581 pull_request_id = safe_int(pull_request_id)
604 pull_request_id = safe_int(pull_request_id)
582 pull_request = PullRequest.get_or_404(pull_request_id)
605 pull_request = PullRequest.get_or_404(pull_request_id)
583 user = c.rhodecode_user
606 user = c.rhodecode_user
584
607
585 if self._meets_merge_pre_conditions(pull_request, user):
608 if self._meets_merge_pre_conditions(pull_request, user):
586 log.debug("Pre-conditions checked, trying to merge.")
609 log.debug("Pre-conditions checked, trying to merge.")
587 extras = vcs_operation_context(
610 extras = vcs_operation_context(
588 request.environ, repo_name=pull_request.target_repo.repo_name,
611 request.environ, repo_name=pull_request.target_repo.repo_name,
589 username=user.username, action='push',
612 username=user.username, action='push',
590 scm=pull_request.target_repo.repo_type)
613 scm=pull_request.target_repo.repo_type)
591 self._merge_pull_request(pull_request, user, extras)
614 self._merge_pull_request(pull_request, user, extras)
592
615
593 return redirect(url(
616 return redirect(url(
594 'pullrequest_show',
617 'pullrequest_show',
595 repo_name=pull_request.target_repo.repo_name,
618 repo_name=pull_request.target_repo.repo_name,
596 pull_request_id=pull_request.pull_request_id))
619 pull_request_id=pull_request.pull_request_id))
597
620
598 def _meets_merge_pre_conditions(self, pull_request, user):
621 def _meets_merge_pre_conditions(self, pull_request, user):
599 if not PullRequestModel().check_user_merge(pull_request, user):
622 if not PullRequestModel().check_user_merge(pull_request, user):
600 raise HTTPForbidden()
623 raise HTTPForbidden()
601
624
602 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 merge_status, msg = PullRequestModel().merge_status(pull_request)
603 if not merge_status:
626 if not merge_status:
604 log.debug("Cannot merge, not mergeable.")
627 log.debug("Cannot merge, not mergeable.")
605 h.flash(msg, category='error')
628 h.flash(msg, category='error')
606 return False
629 return False
607
630
608 if (pull_request.calculated_review_status()
631 if (pull_request.calculated_review_status()
609 is not ChangesetStatus.STATUS_APPROVED):
632 is not ChangesetStatus.STATUS_APPROVED):
610 log.debug("Cannot merge, approval is pending.")
633 log.debug("Cannot merge, approval is pending.")
611 msg = _('Pull request reviewer approval is pending.')
634 msg = _('Pull request reviewer approval is pending.')
612 h.flash(msg, category='error')
635 h.flash(msg, category='error')
613 return False
636 return False
614 return True
637 return True
615
638
616 def _merge_pull_request(self, pull_request, user, extras):
639 def _merge_pull_request(self, pull_request, user, extras):
617 merge_resp = PullRequestModel().merge(
640 merge_resp = PullRequestModel().merge(
618 pull_request, user, extras=extras)
641 pull_request, user, extras=extras)
619
642
620 if merge_resp.executed:
643 if merge_resp.executed:
621 log.debug("The merge was successful, closing the pull request.")
644 log.debug("The merge was successful, closing the pull request.")
622 PullRequestModel().close_pull_request(
645 PullRequestModel().close_pull_request(
623 pull_request.pull_request_id, user)
646 pull_request.pull_request_id, user)
624 Session().commit()
647 Session().commit()
625 msg = _('Pull request was successfully merged and closed.')
648 msg = _('Pull request was successfully merged and closed.')
626 h.flash(msg, category='success')
649 h.flash(msg, category='success')
627 else:
650 else:
628 log.debug(
651 log.debug(
629 "The merge was not successful. Merge response: %s",
652 "The merge was not successful. Merge response: %s",
630 merge_resp)
653 merge_resp)
631 msg = PullRequestModel().merge_status_message(
654 msg = PullRequestModel().merge_status_message(
632 merge_resp.failure_reason)
655 merge_resp.failure_reason)
633 h.flash(msg, category='error')
656 h.flash(msg, category='error')
634
657
635 def _update_reviewers(self, pull_request_id, review_members):
658 def _update_reviewers(self, pull_request_id, review_members):
636 reviewers = [
659 reviewers = [
637 (int(r['user_id']), r['reasons']) for r in review_members]
660 (int(r['user_id']), r['reasons']) for r in review_members]
638 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 PullRequestModel().update_reviewers(pull_request_id, reviewers)
639 Session().commit()
662 Session().commit()
640
663
641 def _reject_close(self, pull_request):
664 def _reject_close(self, pull_request):
642 if pull_request.is_closed():
665 if pull_request.is_closed():
643 raise HTTPForbidden()
666 raise HTTPForbidden()
644
667
645 PullRequestModel().close_pull_request_with_comment(
668 PullRequestModel().close_pull_request_with_comment(
646 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
647 Session().commit()
670 Session().commit()
648
671
649 @LoginRequired()
672 @LoginRequired()
650 @NotAnonymous()
673 @NotAnonymous()
651 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
652 'repository.admin')
675 'repository.admin')
653 @auth.CSRFRequired()
676 @auth.CSRFRequired()
654 @jsonify
677 @jsonify
655 def delete(self, repo_name, pull_request_id):
678 def delete(self, repo_name, pull_request_id):
656 pull_request_id = safe_int(pull_request_id)
679 pull_request_id = safe_int(pull_request_id)
657 pull_request = PullRequest.get_or_404(pull_request_id)
680 pull_request = PullRequest.get_or_404(pull_request_id)
658 # only owner can delete it !
681 # only owner can delete it !
659 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 if pull_request.author.user_id == c.rhodecode_user.user_id:
660 PullRequestModel().delete(pull_request)
683 PullRequestModel().delete(pull_request)
661 Session().commit()
684 Session().commit()
662 h.flash(_('Successfully deleted pull request'),
685 h.flash(_('Successfully deleted pull request'),
663 category='success')
686 category='success')
664 return redirect(url('my_account_pullrequests'))
687 return redirect(url('my_account_pullrequests'))
665 raise HTTPForbidden()
688 raise HTTPForbidden()
666
689
667 @LoginRequired()
690 @LoginRequired()
668 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
691 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
669 'repository.admin')
692 'repository.admin')
670 def show(self, repo_name, pull_request_id):
693 def show(self, repo_name, pull_request_id):
671 pull_request_id = safe_int(pull_request_id)
694 pull_request_id = safe_int(pull_request_id)
672 c.pull_request = PullRequest.get_or_404(pull_request_id)
695 c.pull_request = PullRequest.get_or_404(pull_request_id)
673
696
674 c.template_context['pull_request_data']['pull_request_id'] = \
697 c.template_context['pull_request_data']['pull_request_id'] = \
675 pull_request_id
698 pull_request_id
676
699
677 # pull_requests repo_name we opened it against
700 # pull_requests repo_name we opened it against
678 # ie. target_repo must match
701 # ie. target_repo must match
679 if repo_name != c.pull_request.target_repo.repo_name:
702 if repo_name != c.pull_request.target_repo.repo_name:
680 raise HTTPNotFound
703 raise HTTPNotFound
681
704
682 c.allowed_to_change_status = PullRequestModel(). \
705 c.allowed_to_change_status = PullRequestModel(). \
683 check_user_change_status(c.pull_request, c.rhodecode_user)
706 check_user_change_status(c.pull_request, c.rhodecode_user)
684 c.allowed_to_update = PullRequestModel().check_user_update(
707 c.allowed_to_update = PullRequestModel().check_user_update(
685 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
708 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
686 c.allowed_to_merge = PullRequestModel().check_user_merge(
709 c.allowed_to_merge = PullRequestModel().check_user_merge(
687 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
710 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
711 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
689 c.pull_request)
712 c.pull_request)
690 c.allowed_to_delete = PullRequestModel().check_user_delete(
713 c.allowed_to_delete = PullRequestModel().check_user_delete(
691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
714 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
692
715
693 cc_model = ChangesetCommentsModel()
716 cc_model = ChangesetCommentsModel()
694
717
695 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
718 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
696
719
697 c.pull_request_review_status = c.pull_request.calculated_review_status()
720 c.pull_request_review_status = c.pull_request.calculated_review_status()
698 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
721 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
699 c.pull_request)
722 c.pull_request)
700 c.approval_msg = None
723 c.approval_msg = None
701 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
724 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
702 c.approval_msg = _('Reviewer approval is pending.')
725 c.approval_msg = _('Reviewer approval is pending.')
703 c.pr_merge_status = False
726 c.pr_merge_status = False
704 # load compare data into template context
727 # load compare data into template context
705 enable_comments = not c.pull_request.is_closed()
728 enable_comments = not c.pull_request.is_closed()
706 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
707
729
708 # this is a hack to properly display links, when creating PR, the
709 # compare view and others uses different notation, and
710 # compare_commits.html renders links based on the target_repo.
711 # We need to swap that here to generate it properly on the html side
712 c.target_repo = c.source_repo
713
730
714 # inline comments
731 # inline comments
715 c.inline_cnt = 0
716 c.inline_comments = cc_model.get_inline_comments(
732 c.inline_comments = cc_model.get_inline_comments(
717 c.rhodecode_db_repo.repo_id,
733 c.rhodecode_db_repo.repo_id,
718 pull_request=pull_request_id).items()
734 pull_request=pull_request_id)
719 # count inline comments
735 c.inline_cnt = len(c.inline_comments)
720 for __, lines in c.inline_comments:
721 for comments in lines.values():
722 c.inline_cnt += len(comments)
723
736
724 # outdated comments
737 # outdated comments
725 c.outdated_cnt = 0
738 c.outdated_cnt = 0
726 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
739 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
727 c.outdated_comments = cc_model.get_outdated_comments(
740 c.outdated_comments = cc_model.get_outdated_comments(
728 c.rhodecode_db_repo.repo_id,
741 c.rhodecode_db_repo.repo_id,
729 pull_request=c.pull_request)
742 pull_request=c.pull_request)
730 # Count outdated comments and check for deleted files
743 # Count outdated comments and check for deleted files
731 for file_name, lines in c.outdated_comments.iteritems():
744 for file_name, lines in c.outdated_comments.iteritems():
732 for comments in lines.values():
745 for comments in lines.values():
733 c.outdated_cnt += len(comments)
746 c.outdated_cnt += len(comments)
734 if file_name not in c.included_files:
747 if file_name not in c.included_files:
735 c.deleted_files.append(file_name)
748 c.deleted_files.append(file_name)
736 else:
749 else:
737 c.outdated_comments = {}
750 c.outdated_comments = {}
738
751
752 self._load_compare_data(
753 c.pull_request, c.inline_comments, enable_comments=enable_comments)
754
755 # this is a hack to properly display links, when creating PR, the
756 # compare view and others uses different notation, and
757 # compare_commits.html renders links based on the target_repo.
758 # We need to swap that here to generate it properly on the html side
759 c.target_repo = c.source_repo
760
739 # comments
761 # comments
740 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
762 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
741 pull_request=pull_request_id)
763 pull_request=pull_request_id)
742
764
743 if c.allowed_to_update:
765 if c.allowed_to_update:
744 force_close = ('forced_closed', _('Close Pull Request'))
766 force_close = ('forced_closed', _('Close Pull Request'))
745 statuses = ChangesetStatus.STATUSES + [force_close]
767 statuses = ChangesetStatus.STATUSES + [force_close]
746 else:
768 else:
747 statuses = ChangesetStatus.STATUSES
769 statuses = ChangesetStatus.STATUSES
748 c.commit_statuses = statuses
770 c.commit_statuses = statuses
749
771
750 c.ancestor = None # TODO: add ancestor here
772 c.ancestor = None # TODO: add ancestor here
751
773
752 return render('/pullrequests/pullrequest_show.html')
774 return render('/pullrequests/pullrequest_show.html')
753
775
754 @LoginRequired()
776 @LoginRequired()
755 @NotAnonymous()
777 @NotAnonymous()
756 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
778 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
757 'repository.admin')
779 'repository.admin')
758 @auth.CSRFRequired()
780 @auth.CSRFRequired()
759 @jsonify
781 @jsonify
760 def comment(self, repo_name, pull_request_id):
782 def comment(self, repo_name, pull_request_id):
761 pull_request_id = safe_int(pull_request_id)
783 pull_request_id = safe_int(pull_request_id)
762 pull_request = PullRequest.get_or_404(pull_request_id)
784 pull_request = PullRequest.get_or_404(pull_request_id)
763 if pull_request.is_closed():
785 if pull_request.is_closed():
764 raise HTTPForbidden()
786 raise HTTPForbidden()
765
787
766 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
788 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
767 # as a changeset status, still we want to send it in one value.
789 # as a changeset status, still we want to send it in one value.
768 status = request.POST.get('changeset_status', None)
790 status = request.POST.get('changeset_status', None)
769 text = request.POST.get('text')
791 text = request.POST.get('text')
770 if status and '_closed' in status:
792 if status and '_closed' in status:
771 close_pr = True
793 close_pr = True
772 status = status.replace('_closed', '')
794 status = status.replace('_closed', '')
773 else:
795 else:
774 close_pr = False
796 close_pr = False
775
797
776 forced = (status == 'forced')
798 forced = (status == 'forced')
777 if forced:
799 if forced:
778 status = 'rejected'
800 status = 'rejected'
779
801
780 allowed_to_change_status = PullRequestModel().check_user_change_status(
802 allowed_to_change_status = PullRequestModel().check_user_change_status(
781 pull_request, c.rhodecode_user)
803 pull_request, c.rhodecode_user)
782
804
783 if status and allowed_to_change_status:
805 if status and allowed_to_change_status:
784 message = (_('Status change %(transition_icon)s %(status)s')
806 message = (_('Status change %(transition_icon)s %(status)s')
785 % {'transition_icon': '>',
807 % {'transition_icon': '>',
786 'status': ChangesetStatus.get_status_lbl(status)})
808 'status': ChangesetStatus.get_status_lbl(status)})
787 if close_pr:
809 if close_pr:
788 message = _('Closing with') + ' ' + message
810 message = _('Closing with') + ' ' + message
789 text = text or message
811 text = text or message
790 comm = ChangesetCommentsModel().create(
812 comm = ChangesetCommentsModel().create(
791 text=text,
813 text=text,
792 repo=c.rhodecode_db_repo.repo_id,
814 repo=c.rhodecode_db_repo.repo_id,
793 user=c.rhodecode_user.user_id,
815 user=c.rhodecode_user.user_id,
794 pull_request=pull_request_id,
816 pull_request=pull_request_id,
795 f_path=request.POST.get('f_path'),
817 f_path=request.POST.get('f_path'),
796 line_no=request.POST.get('line'),
818 line_no=request.POST.get('line'),
797 status_change=(ChangesetStatus.get_status_lbl(status)
819 status_change=(ChangesetStatus.get_status_lbl(status)
798 if status and allowed_to_change_status else None),
820 if status and allowed_to_change_status else None),
799 status_change_type=(status
821 status_change_type=(status
800 if status and allowed_to_change_status else None),
822 if status and allowed_to_change_status else None),
801 closing_pr=close_pr
823 closing_pr=close_pr
802 )
824 )
803
825
804
826
805
827
806 if allowed_to_change_status:
828 if allowed_to_change_status:
807 old_calculated_status = pull_request.calculated_review_status()
829 old_calculated_status = pull_request.calculated_review_status()
808 # get status if set !
830 # get status if set !
809 if status:
831 if status:
810 ChangesetStatusModel().set_status(
832 ChangesetStatusModel().set_status(
811 c.rhodecode_db_repo.repo_id,
833 c.rhodecode_db_repo.repo_id,
812 status,
834 status,
813 c.rhodecode_user.user_id,
835 c.rhodecode_user.user_id,
814 comm,
836 comm,
815 pull_request=pull_request_id
837 pull_request=pull_request_id
816 )
838 )
817
839
818 Session().flush()
840 Session().flush()
819 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
841 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
820 # we now calculate the status of pull request, and based on that
842 # we now calculate the status of pull request, and based on that
821 # calculation we set the commits status
843 # calculation we set the commits status
822 calculated_status = pull_request.calculated_review_status()
844 calculated_status = pull_request.calculated_review_status()
823 if old_calculated_status != calculated_status:
845 if old_calculated_status != calculated_status:
824 PullRequestModel()._trigger_pull_request_hook(
846 PullRequestModel()._trigger_pull_request_hook(
825 pull_request, c.rhodecode_user, 'review_status_change')
847 pull_request, c.rhodecode_user, 'review_status_change')
826
848
827 calculated_status_lbl = ChangesetStatus.get_status_lbl(
849 calculated_status_lbl = ChangesetStatus.get_status_lbl(
828 calculated_status)
850 calculated_status)
829
851
830 if close_pr:
852 if close_pr:
831 status_completed = (
853 status_completed = (
832 calculated_status in [ChangesetStatus.STATUS_APPROVED,
854 calculated_status in [ChangesetStatus.STATUS_APPROVED,
833 ChangesetStatus.STATUS_REJECTED])
855 ChangesetStatus.STATUS_REJECTED])
834 if forced or status_completed:
856 if forced or status_completed:
835 PullRequestModel().close_pull_request(
857 PullRequestModel().close_pull_request(
836 pull_request_id, c.rhodecode_user)
858 pull_request_id, c.rhodecode_user)
837 else:
859 else:
838 h.flash(_('Closing pull request on other statuses than '
860 h.flash(_('Closing pull request on other statuses than '
839 'rejected or approved is forbidden. '
861 'rejected or approved is forbidden. '
840 'Calculated status from all reviewers '
862 'Calculated status from all reviewers '
841 'is currently: %s') % calculated_status_lbl,
863 'is currently: %s') % calculated_status_lbl,
842 category='warning')
864 category='warning')
843
865
844 Session().commit()
866 Session().commit()
845
867
846 if not request.is_xhr:
868 if not request.is_xhr:
847 return redirect(h.url('pullrequest_show', repo_name=repo_name,
869 return redirect(h.url('pullrequest_show', repo_name=repo_name,
848 pull_request_id=pull_request_id))
870 pull_request_id=pull_request_id))
849
871
850 data = {
872 data = {
851 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
873 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
852 }
874 }
853 if comm:
875 if comm:
854 c.co = comm
876 c.co = comm
855 data.update(comm.get_dict())
877 data.update(comm.get_dict())
856 data.update({'rendered_text':
878 data.update({'rendered_text':
857 render('changeset/changeset_comment_block.html')})
879 render('changeset/changeset_comment_block.html')})
858
880
859 return data
881 return data
860
882
861 @LoginRequired()
883 @LoginRequired()
862 @NotAnonymous()
884 @NotAnonymous()
863 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
885 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
864 'repository.admin')
886 'repository.admin')
865 @auth.CSRFRequired()
887 @auth.CSRFRequired()
866 @jsonify
888 @jsonify
867 def delete_comment(self, repo_name, comment_id):
889 def delete_comment(self, repo_name, comment_id):
868 return self._delete_comment(comment_id)
890 return self._delete_comment(comment_id)
869
891
870 def _delete_comment(self, comment_id):
892 def _delete_comment(self, comment_id):
871 comment_id = safe_int(comment_id)
893 comment_id = safe_int(comment_id)
872 co = ChangesetComment.get_or_404(comment_id)
894 co = ChangesetComment.get_or_404(comment_id)
873 if co.pull_request.is_closed():
895 if co.pull_request.is_closed():
874 # don't allow deleting comments on closed pull request
896 # don't allow deleting comments on closed pull request
875 raise HTTPForbidden()
897 raise HTTPForbidden()
876
898
877 is_owner = co.author.user_id == c.rhodecode_user.user_id
899 is_owner = co.author.user_id == c.rhodecode_user.user_id
878 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
900 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
879 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
901 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
880 old_calculated_status = co.pull_request.calculated_review_status()
902 old_calculated_status = co.pull_request.calculated_review_status()
881 ChangesetCommentsModel().delete(comment=co)
903 ChangesetCommentsModel().delete(comment=co)
882 Session().commit()
904 Session().commit()
883 calculated_status = co.pull_request.calculated_review_status()
905 calculated_status = co.pull_request.calculated_review_status()
884 if old_calculated_status != calculated_status:
906 if old_calculated_status != calculated_status:
885 PullRequestModel()._trigger_pull_request_hook(
907 PullRequestModel()._trigger_pull_request_hook(
886 co.pull_request, c.rhodecode_user, 'review_status_change')
908 co.pull_request, c.rhodecode_user, 'review_status_change')
887 return True
909 return True
888 else:
910 else:
889 raise HTTPForbidden()
911 raise HTTPForbidden()
@@ -1,1167 +1,1174 b''
1 // Default styles
1 // Default styles
2
2
3 .diff-collapse {
3 .diff-collapse {
4 margin: @padding 0;
4 margin: @padding 0;
5 text-align: right;
5 text-align: right;
6 }
6 }
7
7
8 .diff-container {
8 .diff-container {
9 margin-bottom: @space;
9 margin-bottom: @space;
10
10
11 .diffblock {
11 .diffblock {
12 margin-bottom: @space;
12 margin-bottom: @space;
13 }
13 }
14
14
15 &.hidden {
15 &.hidden {
16 display: none;
16 display: none;
17 overflow: hidden;
17 overflow: hidden;
18 }
18 }
19 }
19 }
20
20
21 .compare_view_files {
21 .compare_view_files {
22
22
23 .diff-container {
23 .diff-container {
24
24
25 .diffblock {
25 .diffblock {
26 margin-bottom: 0;
26 margin-bottom: 0;
27 }
27 }
28 }
28 }
29 }
29 }
30
30
31 div.diffblock .sidebyside {
31 div.diffblock .sidebyside {
32 background: #ffffff;
32 background: #ffffff;
33 }
33 }
34
34
35 div.diffblock {
35 div.diffblock {
36 overflow-x: auto;
36 overflow-x: auto;
37 overflow-y: hidden;
37 overflow-y: hidden;
38 clear: both;
38 clear: both;
39 padding: 0px;
39 padding: 0px;
40 background: @grey6;
40 background: @grey6;
41 border: @border-thickness solid @grey5;
41 border: @border-thickness solid @grey5;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
43 border-radius: @border-radius @border-radius 0px 0px;
44
44
45
45
46 .comments-number {
46 .comments-number {
47 float: right;
47 float: right;
48 }
48 }
49
49
50 // BEGIN CODE-HEADER STYLES
50 // BEGIN CODE-HEADER STYLES
51
51
52 .code-header {
52 .code-header {
53 background: @grey6;
53 background: @grey6;
54 padding: 10px 0 10px 0;
54 padding: 10px 0 10px 0;
55 height: auto;
55 height: auto;
56 width: 100%;
56 width: 100%;
57
57
58 .hash {
58 .hash {
59 float: left;
59 float: left;
60 padding: 2px 0 0 2px;
60 padding: 2px 0 0 2px;
61 }
61 }
62
62
63 .date {
63 .date {
64 float: left;
64 float: left;
65 text-transform: uppercase;
65 text-transform: uppercase;
66 padding: 4px 0px 0px 2px;
66 padding: 4px 0px 0px 2px;
67 }
67 }
68
68
69 div {
69 div {
70 margin-left: 4px;
70 margin-left: 4px;
71 }
71 }
72
72
73 div.compare_header {
73 div.compare_header {
74 min-height: 40px;
74 min-height: 40px;
75 margin: 0;
75 margin: 0;
76 padding: 0 @padding;
76 padding: 0 @padding;
77
77
78 .drop-menu {
78 .drop-menu {
79 float:left;
79 float:left;
80 display: block;
80 display: block;
81 margin:0 0 @padding 0;
81 margin:0 0 @padding 0;
82 }
82 }
83
83
84 .compare-label {
84 .compare-label {
85 float: left;
85 float: left;
86 clear: both;
86 clear: both;
87 display: inline-block;
87 display: inline-block;
88 min-width: 5em;
88 min-width: 5em;
89 margin: 0;
89 margin: 0;
90 padding: @button-padding @button-padding @button-padding 0;
90 padding: @button-padding @button-padding @button-padding 0;
91 font-family: @text-semibold;
91 font-family: @text-semibold;
92 }
92 }
93
93
94 .compare-buttons {
94 .compare-buttons {
95 float: left;
95 float: left;
96 margin: 0;
96 margin: 0;
97 padding: 0 0 @padding;
97 padding: 0 0 @padding;
98
98
99 .btn {
99 .btn {
100 margin: 0 @padding 0 0;
100 margin: 0 @padding 0 0;
101 }
101 }
102 }
102 }
103 }
103 }
104
104
105 }
105 }
106
106
107 .parents {
107 .parents {
108 float: left;
108 float: left;
109 width: 100px;
109 width: 100px;
110 font-weight: 400;
110 font-weight: 400;
111 vertical-align: middle;
111 vertical-align: middle;
112 padding: 0px 2px 0px 2px;
112 padding: 0px 2px 0px 2px;
113 background-color: @grey6;
113 background-color: @grey6;
114
114
115 #parent_link {
115 #parent_link {
116 margin: 00px 2px;
116 margin: 00px 2px;
117
117
118 &.double {
118 &.double {
119 margin: 0px 2px;
119 margin: 0px 2px;
120 }
120 }
121
121
122 &.disabled{
122 &.disabled{
123 margin-right: @padding;
123 margin-right: @padding;
124 }
124 }
125 }
125 }
126 }
126 }
127
127
128 .children {
128 .children {
129 float: right;
129 float: right;
130 width: 100px;
130 width: 100px;
131 font-weight: 400;
131 font-weight: 400;
132 vertical-align: middle;
132 vertical-align: middle;
133 text-align: right;
133 text-align: right;
134 padding: 0px 2px 0px 2px;
134 padding: 0px 2px 0px 2px;
135 background-color: @grey6;
135 background-color: @grey6;
136
136
137 #child_link {
137 #child_link {
138 margin: 0px 2px;
138 margin: 0px 2px;
139
139
140 &.double {
140 &.double {
141 margin: 0px 2px;
141 margin: 0px 2px;
142 }
142 }
143
143
144 &.disabled{
144 &.disabled{
145 margin-right: @padding;
145 margin-right: @padding;
146 }
146 }
147 }
147 }
148 }
148 }
149
149
150 .changeset_header {
150 .changeset_header {
151 height: 16px;
151 height: 16px;
152
152
153 & > div{
153 & > div{
154 margin-right: @padding;
154 margin-right: @padding;
155 }
155 }
156 }
156 }
157
157
158 .changeset_file {
158 .changeset_file {
159 text-align: left;
159 text-align: left;
160 float: left;
160 float: left;
161 padding: 0;
161 padding: 0;
162
162
163 a{
163 a{
164 display: inline-block;
164 display: inline-block;
165 margin-right: 0.5em;
165 margin-right: 0.5em;
166 }
166 }
167
167
168 #selected_mode{
168 #selected_mode{
169 margin-left: 0;
169 margin-left: 0;
170 }
170 }
171 }
171 }
172
172
173 .diff-menu-wrapper {
173 .diff-menu-wrapper {
174 float: left;
174 float: left;
175 }
175 }
176
176
177 .diff-menu {
177 .diff-menu {
178 position: absolute;
178 position: absolute;
179 background: none repeat scroll 0 0 #FFFFFF;
179 background: none repeat scroll 0 0 #FFFFFF;
180 border-color: #003367 @grey3 @grey3;
180 border-color: #003367 @grey3 @grey3;
181 border-right: 1px solid @grey3;
181 border-right: 1px solid @grey3;
182 border-style: solid solid solid;
182 border-style: solid solid solid;
183 border-width: @border-thickness;
183 border-width: @border-thickness;
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 margin-top: 5px;
185 margin-top: 5px;
186 margin-left: 1px;
186 margin-left: 1px;
187 }
187 }
188
188
189 .diff-actions, .editor-actions {
189 .diff-actions, .editor-actions {
190 float: left;
190 float: left;
191
191
192 input{
192 input{
193 margin: 0 0.5em 0 0;
193 margin: 0 0.5em 0 0;
194 }
194 }
195 }
195 }
196
196
197 // END CODE-HEADER STYLES
197 // END CODE-HEADER STYLES
198
198
199 // BEGIN CODE-BODY STYLES
199 // BEGIN CODE-BODY STYLES
200
200
201 .code-body {
201 .code-body {
202 background: white;
202 background: white;
203 padding: 0;
203 padding: 0;
204 background-color: #ffffff;
204 background-color: #ffffff;
205 position: relative;
205 position: relative;
206 max-width: none;
206 max-width: none;
207 box-sizing: border-box;
207 box-sizing: border-box;
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 // to have the intended size and to scroll. Should be simplified.
209 // to have the intended size and to scroll. Should be simplified.
210 width: 100%;
210 width: 100%;
211 overflow-x: auto;
211 overflow-x: auto;
212 }
212 }
213
213
214 pre.raw {
214 pre.raw {
215 background: white;
215 background: white;
216 color: @grey1;
216 color: @grey1;
217 }
217 }
218 // END CODE-BODY STYLES
218 // END CODE-BODY STYLES
219
219
220 }
220 }
221
221
222
222
223 table.code-difftable {
223 table.code-difftable {
224 border-collapse: collapse;
224 border-collapse: collapse;
225 width: 99%;
225 width: 99%;
226 border-radius: 0px !important;
226 border-radius: 0px !important;
227
227
228 td {
228 td {
229 padding: 0 !important;
229 padding: 0 !important;
230 background: none !important;
230 background: none !important;
231 border: 0 !important;
231 border: 0 !important;
232 }
232 }
233
233
234 .context {
234 .context {
235 background: none repeat scroll 0 0 #DDE7EF;
235 background: none repeat scroll 0 0 #DDE7EF;
236 }
236 }
237
237
238 .add {
238 .add {
239 background: none repeat scroll 0 0 #DDFFDD;
239 background: none repeat scroll 0 0 #DDFFDD;
240
240
241 ins {
241 ins {
242 background: none repeat scroll 0 0 #AAFFAA;
242 background: none repeat scroll 0 0 #AAFFAA;
243 text-decoration: none;
243 text-decoration: none;
244 }
244 }
245 }
245 }
246
246
247 .del {
247 .del {
248 background: none repeat scroll 0 0 #FFDDDD;
248 background: none repeat scroll 0 0 #FFDDDD;
249
249
250 del {
250 del {
251 background: none repeat scroll 0 0 #FFAAAA;
251 background: none repeat scroll 0 0 #FFAAAA;
252 text-decoration: none;
252 text-decoration: none;
253 }
253 }
254 }
254 }
255
255
256 /** LINE NUMBERS **/
256 /** LINE NUMBERS **/
257 .lineno {
257 .lineno {
258 padding-left: 2px !important;
258 padding-left: 2px !important;
259 padding-right: 2px;
259 padding-right: 2px;
260 text-align: right;
260 text-align: right;
261 width: 32px;
261 width: 32px;
262 -moz-user-select: none;
262 -moz-user-select: none;
263 -webkit-user-select: none;
263 -webkit-user-select: none;
264 border-right: @border-thickness solid @grey5 !important;
264 border-right: @border-thickness solid @grey5 !important;
265 border-left: 0px solid #CCC !important;
265 border-left: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
266 border-top: 0px solid #CCC !important;
267 border-bottom: none !important;
267 border-bottom: none !important;
268
268
269 a {
269 a {
270 &:extend(pre);
270 &:extend(pre);
271 text-align: right;
271 text-align: right;
272 padding-right: 2px;
272 padding-right: 2px;
273 cursor: pointer;
273 cursor: pointer;
274 display: block;
274 display: block;
275 width: 32px;
275 width: 32px;
276 }
276 }
277 }
277 }
278
278
279 .context {
279 .context {
280 cursor: auto;
280 cursor: auto;
281 &:extend(pre);
281 &:extend(pre);
282 }
282 }
283
283
284 .lineno-inline {
284 .lineno-inline {
285 background: none repeat scroll 0 0 #FFF !important;
285 background: none repeat scroll 0 0 #FFF !important;
286 padding-left: 2px;
286 padding-left: 2px;
287 padding-right: 2px;
287 padding-right: 2px;
288 text-align: right;
288 text-align: right;
289 width: 30px;
289 width: 30px;
290 -moz-user-select: none;
290 -moz-user-select: none;
291 -webkit-user-select: none;
291 -webkit-user-select: none;
292 }
292 }
293
293
294 /** CODE **/
294 /** CODE **/
295 .code {
295 .code {
296 display: block;
296 display: block;
297 width: 100%;
297 width: 100%;
298
298
299 td {
299 td {
300 margin: 0;
300 margin: 0;
301 padding: 0;
301 padding: 0;
302 }
302 }
303
303
304 pre {
304 pre {
305 margin: 0;
305 margin: 0;
306 padding: 0;
306 padding: 0;
307 margin-left: .5em;
307 margin-left: .5em;
308 }
308 }
309 }
309 }
310 }
310 }
311
311
312
312
313 // Comments
313 // Comments
314
314
315 div.comment:target {
315 div.comment:target {
316 border-left: 6px solid @comment-highlight-color;
316 border-left: 6px solid @comment-highlight-color;
317 padding-left: 3px;
317 padding-left: 3px;
318 margin-left: -9px;
318 margin-left: -9px;
319 }
319 }
320
320
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 //current values that might change. But to make it clear I put as a calculation
322 //current values that might change. But to make it clear I put as a calculation
323 @comment-max-width: 1065px;
323 @comment-max-width: 1065px;
324 @pr-extra-margin: 34px;
324 @pr-extra-margin: 34px;
325 @pr-border-spacing: 4px;
325 @pr-border-spacing: 4px;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327
327
328 // Pull Request
328 // Pull Request
329 .cs_files .code-difftable {
329 .cs_files .code-difftable {
330 border: @border-thickness solid @grey5; //borders only on PRs
330 border: @border-thickness solid @grey5; //borders only on PRs
331
331
332 .comment-inline-form,
332 .comment-inline-form,
333 div.comment {
333 div.comment {
334 width: @pr-comment-width;
334 width: @pr-comment-width;
335 }
335 }
336 }
336 }
337
337
338 // Changeset
338 // Changeset
339 .code-difftable {
339 .code-difftable {
340 .comment-inline-form,
340 .comment-inline-form,
341 div.comment {
341 div.comment {
342 width: @comment-max-width;
342 width: @comment-max-width;
343 }
343 }
344 }
344 }
345
345
346 //Style page
346 //Style page
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 #style-page .code-difftable{
348 #style-page .code-difftable{
349 .comment-inline-form,
349 .comment-inline-form,
350 div.comment {
350 div.comment {
351 width: @comment-max-width - @style-extra-margin;
351 width: @comment-max-width - @style-extra-margin;
352 }
352 }
353 }
353 }
354
354
355 #context-bar > h2 {
355 #context-bar > h2 {
356 font-size: 20px;
356 font-size: 20px;
357 }
357 }
358
358
359 #context-bar > h2> a {
359 #context-bar > h2> a {
360 font-size: 20px;
360 font-size: 20px;
361 }
361 }
362 // end of defaults
362 // end of defaults
363
363
364 .file_diff_buttons {
364 .file_diff_buttons {
365 padding: 0 0 @padding;
365 padding: 0 0 @padding;
366
366
367 .drop-menu {
367 .drop-menu {
368 float: left;
368 float: left;
369 margin: 0 @padding 0 0;
369 margin: 0 @padding 0 0;
370 }
370 }
371 .btn {
371 .btn {
372 margin: 0 @padding 0 0;
372 margin: 0 @padding 0 0;
373 }
373 }
374 }
374 }
375
375
376 .code-body.textarea.editor {
376 .code-body.textarea.editor {
377 max-width: none;
377 max-width: none;
378 padding: 15px;
378 padding: 15px;
379 }
379 }
380
380
381 td.injected_diff{
381 td.injected_diff{
382 max-width: 1178px;
382 max-width: 1178px;
383 overflow-x: auto;
383 overflow-x: auto;
384 overflow-y: hidden;
384 overflow-y: hidden;
385
385
386 div.diff-container,
386 div.diff-container,
387 div.diffblock{
387 div.diffblock{
388 max-width: 100%;
388 max-width: 100%;
389 }
389 }
390
390
391 div.code-body {
391 div.code-body {
392 max-width: 1124px;
392 max-width: 1124px;
393 overflow-x: auto;
393 overflow-x: auto;
394 overflow-y: hidden;
394 overflow-y: hidden;
395 padding: 0;
395 padding: 0;
396 }
396 }
397 div.diffblock {
397 div.diffblock {
398 border: none;
398 border: none;
399 }
399 }
400
400
401 &.inline-form {
401 &.inline-form {
402 width: 99%
402 width: 99%
403 }
403 }
404 }
404 }
405
405
406
406
407 table.code-difftable {
407 table.code-difftable {
408 width: 100%;
408 width: 100%;
409 }
409 }
410
410
411 /** PYGMENTS COLORING **/
411 /** PYGMENTS COLORING **/
412 div.codeblock {
412 div.codeblock {
413
413
414 // TODO: johbo: Added interim to get rid of the margin around
414 // TODO: johbo: Added interim to get rid of the margin around
415 // Select2 widgets. This needs further cleanup.
415 // Select2 widgets. This needs further cleanup.
416 margin-top: @padding;
416 margin-top: @padding;
417
417
418 overflow: auto;
418 overflow: auto;
419 padding: 0px;
419 padding: 0px;
420 border: @border-thickness solid @grey5;
420 border: @border-thickness solid @grey5;
421 background: @grey6;
421 background: @grey6;
422 .border-radius(@border-radius);
422 .border-radius(@border-radius);
423
423
424 #remove_gist {
424 #remove_gist {
425 float: right;
425 float: right;
426 }
426 }
427
427
428 .author {
428 .author {
429 clear: both;
429 clear: both;
430 vertical-align: middle;
430 vertical-align: middle;
431 font-family: @text-bold;
431 font-family: @text-bold;
432 }
432 }
433
433
434 .btn-mini {
434 .btn-mini {
435 float: left;
435 float: left;
436 margin: 0 5px 0 0;
436 margin: 0 5px 0 0;
437 }
437 }
438
438
439 .code-header {
439 .code-header {
440 padding: @padding;
440 padding: @padding;
441 border-bottom: @border-thickness solid @grey5;
441 border-bottom: @border-thickness solid @grey5;
442
442
443 .rc-user {
443 .rc-user {
444 min-width: 0;
444 min-width: 0;
445 margin-right: .5em;
445 margin-right: .5em;
446 }
446 }
447
447
448 .stats {
448 .stats {
449 clear: both;
449 clear: both;
450 margin: 0 0 @padding 0;
450 margin: 0 0 @padding 0;
451 padding: 0;
451 padding: 0;
452 .left {
452 .left {
453 float: left;
453 float: left;
454 clear: left;
454 clear: left;
455 max-width: 75%;
455 max-width: 75%;
456 margin: 0 0 @padding 0;
456 margin: 0 0 @padding 0;
457
457
458 &.item {
458 &.item {
459 margin-right: @padding;
459 margin-right: @padding;
460 &.last { border-right: none; }
460 &.last { border-right: none; }
461 }
461 }
462 }
462 }
463 .buttons { float: right; }
463 .buttons { float: right; }
464 .author {
464 .author {
465 height: 25px; margin-left: 15px; font-weight: bold;
465 height: 25px; margin-left: 15px; font-weight: bold;
466 }
466 }
467 }
467 }
468
468
469 .commit {
469 .commit {
470 margin: 5px 0 0 26px;
470 margin: 5px 0 0 26px;
471 font-weight: normal;
471 font-weight: normal;
472 white-space: pre-wrap;
472 white-space: pre-wrap;
473 }
473 }
474 }
474 }
475
475
476 .message {
476 .message {
477 position: relative;
477 position: relative;
478 margin: @padding;
478 margin: @padding;
479
479
480 .codeblock-label {
480 .codeblock-label {
481 margin: 0 0 1em 0;
481 margin: 0 0 1em 0;
482 }
482 }
483 }
483 }
484
484
485 .code-body {
485 .code-body {
486 padding: @padding;
486 padding: @padding;
487 background-color: #ffffff;
487 background-color: #ffffff;
488 min-width: 100%;
488 min-width: 100%;
489 box-sizing: border-box;
489 box-sizing: border-box;
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 // to have the intended size and to scroll. Should be simplified.
491 // to have the intended size and to scroll. Should be simplified.
492 width: 100%;
492 width: 100%;
493 overflow-x: auto;
493 overflow-x: auto;
494 }
494 }
495 }
495 }
496
496
497 .code-highlighttable,
497 .code-highlighttable,
498 div.codeblock {
498 div.codeblock {
499
499
500 &.readme {
500 &.readme {
501 background-color: white;
501 background-color: white;
502 }
502 }
503
503
504 .markdown-block table {
504 .markdown-block table {
505 border-collapse: collapse;
505 border-collapse: collapse;
506
506
507 th,
507 th,
508 td {
508 td {
509 padding: .5em;
509 padding: .5em;
510 border: @border-thickness solid @border-default-color;
510 border: @border-thickness solid @border-default-color;
511 }
511 }
512 }
512 }
513
513
514 table {
514 table {
515 border: 0px;
515 border: 0px;
516 margin: 0;
516 margin: 0;
517 letter-spacing: normal;
517 letter-spacing: normal;
518
518
519
519
520 td {
520 td {
521 border: 0px;
521 border: 0px;
522 vertical-align: top;
522 vertical-align: top;
523 }
523 }
524 }
524 }
525 }
525 }
526
526
527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 div.search-code-body {
528 div.search-code-body {
529 background-color: #ffffff; padding: 5px 0 5px 10px;
529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 pre {
530 pre {
531 .match { background-color: #faffa6;}
531 .match { background-color: #faffa6;}
532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 }
533 }
534 .code-highlighttable {
534 .code-highlighttable {
535 border-collapse: collapse;
535 border-collapse: collapse;
536
536
537 tr:hover {
537 tr:hover {
538 background: #fafafa;
538 background: #fafafa;
539 }
539 }
540 td.code {
540 td.code {
541 padding-left: 10px;
541 padding-left: 10px;
542 }
542 }
543 td.line {
543 td.line {
544 border-right: 1px solid #ccc !important;
544 border-right: 1px solid #ccc !important;
545 padding-right: 10px;
545 padding-right: 10px;
546 text-align: right;
546 text-align: right;
547 font-family: "Lucida Console",Monaco,monospace;
547 font-family: "Lucida Console",Monaco,monospace;
548 span {
548 span {
549 white-space: pre-wrap;
549 white-space: pre-wrap;
550 color: #666666;
550 color: #666666;
551 }
551 }
552 }
552 }
553 }
553 }
554 }
554 }
555
555
556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 .code-highlight {
557 .code-highlight {
558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 pre div:target {background-color: @comment-highlight-color !important;}
560 pre div:target {background-color: @comment-highlight-color !important;}
561 }
561 }
562
562
563 .linenos a { text-decoration: none; }
563 .linenos a { text-decoration: none; }
564
564
565 .CodeMirror-selected { background: @rchighlightblue; }
565 .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 .CodeMirror ::selection { background: @rchighlightblue; }
567 .CodeMirror ::selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569
569
570 .code { display: block; border:0px !important; }
570 .code { display: block; border:0px !important; }
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 .codehilite {
572 .codehilite {
573 .hll { background-color: #ffffcc }
573 .hll { background-color: #ffffcc }
574 .c { color: #408080; font-style: italic } /* Comment */
574 .c { color: #408080; font-style: italic } /* Comment */
575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 .k { color: #008000; font-weight: bold } /* Keyword */
576 .k { color: #008000; font-weight: bold } /* Keyword */
577 .o { color: #666666 } /* Operator */
577 .o { color: #666666 } /* Operator */
578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 .cp { color: #BC7A00 } /* Comment.Preproc */
579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 .cs { color: #408080; font-style: italic } /* Comment.Special */
581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 .gd { color: #A00000 } /* Generic.Deleted */
582 .gd { color: #A00000 } /* Generic.Deleted */
583 .ge { font-style: italic } /* Generic.Emph */
583 .ge { font-style: italic } /* Generic.Emph */
584 .gr { color: #FF0000 } /* Generic.Error */
584 .gr { color: #FF0000 } /* Generic.Error */
585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 .gi { color: #00A000 } /* Generic.Inserted */
586 .gi { color: #00A000 } /* Generic.Inserted */
587 .go { color: #808080 } /* Generic.Output */
587 .go { color: #808080 } /* Generic.Output */
588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 .gs { font-weight: bold } /* Generic.Strong */
589 .gs { font-weight: bold } /* Generic.Strong */
590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 .gt { color: #0040D0 } /* Generic.Traceback */
591 .gt { color: #0040D0 } /* Generic.Traceback */
592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 .kp { color: #008000 } /* Keyword.Pseudo */
595 .kp { color: #008000 } /* Keyword.Pseudo */
596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 .kt { color: #B00040 } /* Keyword.Type */
597 .kt { color: #B00040 } /* Keyword.Type */
598 .m { color: #666666 } /* Literal.Number */
598 .m { color: #666666 } /* Literal.Number */
599 .s { color: #BA2121 } /* Literal.String */
599 .s { color: #BA2121 } /* Literal.String */
600 .na { color: #7D9029 } /* Name.Attribute */
600 .na { color: #7D9029 } /* Name.Attribute */
601 .nb { color: #008000 } /* Name.Builtin */
601 .nb { color: #008000 } /* Name.Builtin */
602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 .no { color: #880000 } /* Name.Constant */
603 .no { color: #880000 } /* Name.Constant */
604 .nd { color: #AA22FF } /* Name.Decorator */
604 .nd { color: #AA22FF } /* Name.Decorator */
605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 .nf { color: #0000FF } /* Name.Function */
607 .nf { color: #0000FF } /* Name.Function */
608 .nl { color: #A0A000 } /* Name.Label */
608 .nl { color: #A0A000 } /* Name.Label */
609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 .nv { color: #19177C } /* Name.Variable */
611 .nv { color: #19177C } /* Name.Variable */
612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 .w { color: #bbbbbb } /* Text.Whitespace */
613 .w { color: #bbbbbb } /* Text.Whitespace */
614 .mf { color: #666666 } /* Literal.Number.Float */
614 .mf { color: #666666 } /* Literal.Number.Float */
615 .mh { color: #666666 } /* Literal.Number.Hex */
615 .mh { color: #666666 } /* Literal.Number.Hex */
616 .mi { color: #666666 } /* Literal.Number.Integer */
616 .mi { color: #666666 } /* Literal.Number.Integer */
617 .mo { color: #666666 } /* Literal.Number.Oct */
617 .mo { color: #666666 } /* Literal.Number.Oct */
618 .sb { color: #BA2121 } /* Literal.String.Backtick */
618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 .sc { color: #BA2121 } /* Literal.String.Char */
619 .sc { color: #BA2121 } /* Literal.String.Char */
620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 .s2 { color: #BA2121 } /* Literal.String.Double */
621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 .sx { color: #008000 } /* Literal.String.Other */
625 .sx { color: #008000 } /* Literal.String.Other */
626 .sr { color: #BB6688 } /* Literal.String.Regex */
626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 .s1 { color: #BA2121 } /* Literal.String.Single */
627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 .ss { color: #19177C } /* Literal.String.Symbol */
628 .ss { color: #19177C } /* Literal.String.Symbol */
629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 .vc { color: #19177C } /* Name.Variable.Class */
630 .vc { color: #19177C } /* Name.Variable.Class */
631 .vg { color: #19177C } /* Name.Variable.Global */
631 .vg { color: #19177C } /* Name.Variable.Global */
632 .vi { color: #19177C } /* Name.Variable.Instance */
632 .vi { color: #19177C } /* Name.Variable.Instance */
633 .il { color: #666666 } /* Literal.Number.Integer.Long */
633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 }
634 }
635
635
636 /* customized pre blocks for markdown/rst */
636 /* customized pre blocks for markdown/rst */
637 pre.literal-block, .codehilite pre{
637 pre.literal-block, .codehilite pre{
638 padding: @padding;
638 padding: @padding;
639 border: 1px solid @grey6;
639 border: 1px solid @grey6;
640 .border-radius(@border-radius);
640 .border-radius(@border-radius);
641 background-color: @grey7;
641 background-color: @grey7;
642 }
642 }
643
643
644
644
645 /* START NEW CODE BLOCK CSS */
645 /* START NEW CODE BLOCK CSS */
646
646
647 @cb-line-height: 18px;
647 @cb-line-height: 18px;
648 @cb-line-code-padding: 10px;
648 @cb-line-code-padding: 10px;
649 @cb-text-padding: 5px;
649 @cb-text-padding: 5px;
650
650
651 @pill-padding: 2px 7px;
651 @pill-padding: 2px 7px;
652
652
653 input.filediff-collapse-state {
653 input.filediff-collapse-state {
654 display: none;
654 display: none;
655
655
656 &:checked + .filediff { /* file diff is collapsed */
656 &:checked + .filediff { /* file diff is collapsed */
657 .cb {
657 .cb {
658 display: none
658 display: none
659 }
659 }
660 .filediff-collapse-indicator {
660 .filediff-collapse-indicator {
661 border-width: 9px 0 9px 15.6px;
661 border-width: 9px 0 9px 15.6px;
662 border-color: transparent transparent transparent #ccc;
662 border-color: transparent transparent transparent #ccc;
663 }
663 }
664 .filediff-menu {
664 .filediff-menu {
665 display: none;
665 display: none;
666 }
666 }
667 margin: -1px 0 0 0;
667 margin: -1px 0 0 0;
668 }
668 }
669
669
670 &+ .filediff { /* file diff is expanded */
670 &+ .filediff { /* file diff is expanded */
671 .filediff-collapse-indicator {
671 .filediff-collapse-indicator {
672 border-width: 15.6px 9px 0 9px;
672 border-width: 15.6px 9px 0 9px;
673 border-color: #ccc transparent transparent transparent;
673 border-color: #ccc transparent transparent transparent;
674 }
674 }
675 .filediff-menu {
675 .filediff-menu {
676 display: block;
676 display: block;
677 }
677 }
678 margin: 20px 0;
678 margin: 20px 0;
679 &:nth-child(2) {
679 &:nth-child(2) {
680 margin: 0;
680 margin: 0;
681 }
681 }
682 }
682 }
683 }
683 }
684 .cs_files {
684 .cs_files {
685 clear: both;
685 clear: both;
686 }
686 }
687
687
688 .diffset-menu {
688 .diffset-menu {
689 margin-bottom: 20px;
689 margin-bottom: 20px;
690 }
690 }
691 .diffset {
691 .diffset {
692 margin: 20px auto;
692 margin: 20px auto;
693 .diffset-heading {
693 .diffset-heading {
694 border: 1px solid @grey5;
694 border: 1px solid @grey5;
695 margin-bottom: -1px;
695 margin-bottom: -1px;
696 // margin-top: 20px;
696 // margin-top: 20px;
697 h2 {
697 h2 {
698 margin: 0;
698 margin: 0;
699 line-height: 38px;
699 line-height: 38px;
700 padding-left: 10px;
700 padding-left: 10px;
701 }
701 }
702 .btn {
702 .btn {
703 margin: 0;
703 margin: 0;
704 }
704 }
705 background: @grey6;
705 background: @grey6;
706 display: block;
706 display: block;
707 padding: 5px;
707 padding: 5px;
708 }
708 }
709 .diffset-heading-warning {
709 .diffset-heading-warning {
710 background: @alert3-inner;
710 background: @alert3-inner;
711 border: 1px solid @alert3;
711 border: 1px solid @alert3;
712 }
712 }
713 &.diffset-comments-disabled {
714 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
715 display: none !important;
713 }
716 }
717 }
718 }
719
714 .pill {
720 .pill {
715 display: block;
721 display: block;
716 float: left;
722 float: left;
717 padding: @pill-padding;
723 padding: @pill-padding;
718 }
724 }
719 .pill-group {
725 .pill-group {
720 .pill {
726 .pill {
721 opacity: .8;
727 opacity: .8;
722 &:first-child {
728 &:first-child {
723 border-radius: @border-radius 0 0 @border-radius;
729 border-radius: @border-radius 0 0 @border-radius;
724 }
730 }
725 &:last-child {
731 &:last-child {
726 border-radius: 0 @border-radius @border-radius 0;
732 border-radius: 0 @border-radius @border-radius 0;
727 }
733 }
728 &:only-child {
734 &:only-child {
729 border-radius: @border-radius;
735 border-radius: @border-radius;
730 }
736 }
731 }
737 }
732 }
738 }
733
739
734 .filediff {
740 .filediff {
735 border: 1px solid @grey5;
741 border: 1px solid @grey5;
736
742
737 /* START OVERRIDES */
743 /* START OVERRIDES */
738 .code-highlight {
744 .code-highlight {
739 border: none; // TODO: remove this border from the global
745 border: none; // TODO: remove this border from the global
740 // .code-highlight, it doesn't belong there
746 // .code-highlight, it doesn't belong there
741 }
747 }
742 label {
748 label {
743 margin: 0; // TODO: remove this margin definition from global label
749 margin: 0; // TODO: remove this margin definition from global label
744 // it doesn't belong there - if margin on labels
750 // it doesn't belong there - if margin on labels
745 // are needed for a form they should be defined
751 // are needed for a form they should be defined
746 // in the form's class
752 // in the form's class
747 }
753 }
748 /* END OVERRIDES */
754 /* END OVERRIDES */
749
755
750 * {
756 * {
751 box-sizing: border-box;
757 box-sizing: border-box;
752 }
758 }
753 .filediff-anchor {
759 .filediff-anchor {
754 visibility: hidden;
760 visibility: hidden;
755 }
761 }
756 &:hover {
762 &:hover {
757 .filediff-anchor {
763 .filediff-anchor {
758 visibility: visible;
764 visibility: visible;
759 }
765 }
760 }
766 }
761
767
762 .filediff-collapse-indicator {
768 .filediff-collapse-indicator {
763 width: 0;
769 width: 0;
764 height: 0;
770 height: 0;
765 border-style: solid;
771 border-style: solid;
766 float: left;
772 float: left;
767 margin: 2px 2px 0 0;
773 margin: 2px 2px 0 0;
768 cursor: pointer;
774 cursor: pointer;
769 }
775 }
770
776
771 .filediff-heading {
777 .filediff-heading {
772 background: @grey7;
778 background: @grey7;
773 cursor: pointer;
779 cursor: pointer;
774 display: block;
780 display: block;
775 padding: 5px 10px;
781 padding: 5px 10px;
776 }
782 }
777 .filediff-heading:after {
783 .filediff-heading:after {
778 content: "";
784 content: "";
779 display: table;
785 display: table;
780 clear: both;
786 clear: both;
781 }
787 }
782 .filediff-heading:hover {
788 .filediff-heading:hover {
783 background: #e1e9f4 !important;
789 background: #e1e9f4 !important;
784 }
790 }
785
791
786 .filediff-menu {
792 .filediff-menu {
787 float: right;
793 float: right;
788
794
789 &> a, &> span {
795 &> a, &> span {
790 padding: 5px;
796 padding: 5px;
791 display: block;
797 display: block;
792 float: left
798 float: left
793 }
799 }
794 }
800 }
795
801
796 .pill {
802 .pill {
797 &[op="name"] {
803 &[op="name"] {
798 background: none;
804 background: none;
799 color: @grey2;
805 color: @grey2;
800 opacity: 1;
806 opacity: 1;
801 color: white;
807 color: white;
802 }
808 }
803 &[op="limited"] {
809 &[op="limited"] {
804 background: @grey2;
810 background: @grey2;
805 color: white;
811 color: white;
806 }
812 }
807 &[op="binary"] {
813 &[op="binary"] {
808 background: @color7;
814 background: @color7;
809 color: white;
815 color: white;
810 }
816 }
811 &[op="modified"] {
817 &[op="modified"] {
812 background: @alert1;
818 background: @alert1;
813 color: white;
819 color: white;
814 }
820 }
815 &[op="renamed"] {
821 &[op="renamed"] {
816 background: @color4;
822 background: @color4;
817 color: white;
823 color: white;
818 }
824 }
819 &[op="mode"] {
825 &[op="mode"] {
820 background: @grey3;
826 background: @grey3;
821 color: white;
827 color: white;
822 }
828 }
823 &[op="symlink"] {
829 &[op="symlink"] {
824 background: @color8;
830 background: @color8;
825 color: white;
831 color: white;
826 }
832 }
827
833
828 &[op="added"] { /* added lines */
834 &[op="added"] { /* added lines */
829 background: @alert1;
835 background: @alert1;
830 color: white;
836 color: white;
831 }
837 }
832 &[op="deleted"] { /* deleted lines */
838 &[op="deleted"] { /* deleted lines */
833 background: @alert2;
839 background: @alert2;
834 color: white;
840 color: white;
835 }
841 }
836
842
837 &[op="created"] { /* created file */
843 &[op="created"] { /* created file */
838 background: @alert1;
844 background: @alert1;
839 color: white;
845 color: white;
840 }
846 }
841 &[op="removed"] { /* deleted file */
847 &[op="removed"] { /* deleted file */
842 background: @color5;
848 background: @color5;
843 color: white;
849 color: white;
844 }
850 }
845 }
851 }
846
852
847 .filediff-collapse-button, .filediff-expand-button {
853 .filediff-collapse-button, .filediff-expand-button {
848 cursor: pointer;
854 cursor: pointer;
849 }
855 }
850 .filediff-collapse-button {
856 .filediff-collapse-button {
851 display: inline;
857 display: inline;
852 }
858 }
853 .filediff-expand-button {
859 .filediff-expand-button {
854 display: none;
860 display: none;
855 }
861 }
856 .filediff-collapsed .filediff-collapse-button {
862 .filediff-collapsed .filediff-collapse-button {
857 display: none;
863 display: none;
858 }
864 }
859 .filediff-collapsed .filediff-expand-button {
865 .filediff-collapsed .filediff-expand-button {
860 display: inline;
866 display: inline;
861 }
867 }
862
868
863 @comment-padding: 5px;
869 @comment-padding: 5px;
864
870
865 /**** COMMENTS ****/
871 /**** COMMENTS ****/
866
872
867 .filediff-menu {
873 .filediff-menu {
868 .show-comment-button {
874 .show-comment-button {
869 display: none;
875 display: none;
870 }
876 }
871 }
877 }
872 &.hide-comments {
878 &.hide-comments {
873 .inline-comments {
879 .inline-comments {
874 display: none;
880 display: none;
875 }
881 }
876 .filediff-menu {
882 .filediff-menu {
877 .show-comment-button {
883 .show-comment-button {
878 display: inline;
884 display: inline;
879 }
885 }
880 .hide-comment-button {
886 .hide-comment-button {
881 display: none;
887 display: none;
882 }
888 }
883 }
889 }
884 }
890 }
885
891
886 .hide-line-comments {
892 .hide-line-comments {
887 .inline-comments {
893 .inline-comments {
888 display: none;
894 display: none;
889 }
895 }
890 }
896 }
891 .inline-comments {
897 .inline-comments {
892 border-radius: @border-radius;
898 border-radius: @border-radius;
893 background: @grey6;
899 background: @grey6;
894 .comment {
900 .comment {
895 margin: 0;
901 margin: 0;
896 border-radius: @border-radius;
902 border-radius: @border-radius;
897 }
903 }
898 .comment-outdated {
904 .comment-outdated {
899 opacity: 0.5;
905 opacity: 0.5;
900 }
906 }
901 .comment-inline {
907 .comment-inline {
902 background: white;
908 background: white;
903 padding: (@comment-padding + 3px) @comment-padding;
909 padding: (@comment-padding + 3px) @comment-padding;
904 border: @comment-padding solid @grey6;
910 border: @comment-padding solid @grey6;
905
911
906 .text {
912 .text {
907 border: none;
913 border: none;
908 }
914 }
909 .meta {
915 .meta {
910 border-bottom: 1px solid @grey6;
916 border-bottom: 1px solid @grey6;
911 padding-bottom: 10px;
917 padding-bottom: 10px;
912 }
918 }
913 }
919 }
914 .comment-selected {
920 .comment-selected {
915 border-left: 6px solid @comment-highlight-color;
921 border-left: 6px solid @comment-highlight-color;
916 }
922 }
917 .comment-inline-form {
923 .comment-inline-form {
918 padding: @comment-padding;
924 padding: @comment-padding;
919 display: none;
925 display: none;
920 }
926 }
921 .cb-comment-add-button {
927 .cb-comment-add-button {
922 margin: @comment-padding;
928 margin: @comment-padding;
923 }
929 }
924 /* hide add comment button when form is open */
930 /* hide add comment button when form is open */
925 .comment-inline-form-open + .cb-comment-add-button {
931 .comment-inline-form-open + .cb-comment-add-button {
926 display: none;
932 display: none;
927 }
933 }
928 .comment-inline-form-open {
934 .comment-inline-form-open {
929 display: block;
935 display: block;
930 }
936 }
931 /* hide add comment button when form but no comments */
937 /* hide add comment button when form but no comments */
932 .comment-inline-form:first-child + .cb-comment-add-button {
938 .comment-inline-form:first-child + .cb-comment-add-button {
933 display: none;
939 display: none;
934 }
940 }
935 /* hide add comment button when no comments or form */
941 /* hide add comment button when no comments or form */
936 .cb-comment-add-button:first-child {
942 .cb-comment-add-button:first-child {
937 display: none;
943 display: none;
938 }
944 }
939 /* hide add comment button when only comment is being deleted */
945 /* hide add comment button when only comment is being deleted */
940 .comment-deleting:first-child + .cb-comment-add-button {
946 .comment-deleting:first-child + .cb-comment-add-button {
941 display: none;
947 display: none;
942 }
948 }
943 }
949 }
944 /**** END COMMENTS ****/
950 /**** END COMMENTS ****/
945
951
946 }
952 }
947
953
948
954
949 table.cb {
955 table.cb {
950 width: 100%;
956 width: 100%;
951 border-collapse: collapse;
957 border-collapse: collapse;
952
958
953 .cb-text {
959 .cb-text {
954 padding: @cb-text-padding;
960 padding: @cb-text-padding;
955 }
961 }
956 .cb-hunk {
962 .cb-hunk {
957 padding: @cb-text-padding;
963 padding: @cb-text-padding;
958 }
964 }
959 .cb-expand {
965 .cb-expand {
960 display: none;
966 display: none;
961 }
967 }
962 .cb-collapse {
968 .cb-collapse {
963 display: inline;
969 display: inline;
964 }
970 }
965 &.cb-collapsed {
971 &.cb-collapsed {
966 .cb-line {
972 .cb-line {
967 display: none;
973 display: none;
968 }
974 }
969 .cb-expand {
975 .cb-expand {
970 display: inline;
976 display: inline;
971 }
977 }
972 .cb-collapse {
978 .cb-collapse {
973 display: none;
979 display: none;
974 }
980 }
975 }
981 }
976
982
977 /* intentionally general selector since .cb-line-selected must override it
983 /* intentionally general selector since .cb-line-selected must override it
978 and they both use !important since the td itself may have a random color
984 and they both use !important since the td itself may have a random color
979 generated by annotation blocks. TLDR: if you change it, make sure
985 generated by annotation blocks. TLDR: if you change it, make sure
980 annotated block selection and line selection in file view still work */
986 annotated block selection and line selection in file view still work */
981 .cb-line-fresh .cb-content {
987 .cb-line-fresh .cb-content {
982 background: white !important;
988 background: white !important;
983 }
989 }
984 .cb-warning {
990 .cb-warning {
985 background: #fff4dd;
991 background: #fff4dd;
986 }
992 }
987
993
988 &.cb-diff-sideside {
994 &.cb-diff-sideside {
989 td {
995 td {
990 &.cb-content {
996 &.cb-content {
991 width: 50%;
997 width: 50%;
992 }
998 }
993 }
999 }
994 }
1000 }
995
1001
996 tr {
1002 tr {
997 &.cb-annotate {
1003 &.cb-annotate {
998 border-top: 1px solid #eee;
1004 border-top: 1px solid #eee;
999
1005
1000 &+ .cb-line {
1006 &+ .cb-line {
1001 border-top: 1px solid #eee;
1007 border-top: 1px solid #eee;
1002 }
1008 }
1003
1009
1004 &:first-child {
1010 &:first-child {
1005 border-top: none;
1011 border-top: none;
1006 &+ .cb-line {
1012 &+ .cb-line {
1007 border-top: none;
1013 border-top: none;
1008 }
1014 }
1009 }
1015 }
1010 }
1016 }
1011
1017
1012 &.cb-hunk {
1018 &.cb-hunk {
1013 font-family: @font-family-monospace;
1019 font-family: @font-family-monospace;
1014 color: rgba(0, 0, 0, 0.3);
1020 color: rgba(0, 0, 0, 0.3);
1015
1021
1016 td {
1022 td {
1017 &:first-child {
1023 &:first-child {
1018 background: #edf2f9;
1024 background: #edf2f9;
1019 }
1025 }
1020 &:last-child {
1026 &:last-child {
1021 background: #f4f7fb;
1027 background: #f4f7fb;
1022 }
1028 }
1023 }
1029 }
1024 }
1030 }
1025 }
1031 }
1026
1032
1033
1027 td {
1034 td {
1028 vertical-align: top;
1035 vertical-align: top;
1029 padding: 0;
1036 padding: 0;
1030
1037
1031 &.cb-content {
1038 &.cb-content {
1032 font-size: 12.35px;
1039 font-size: 12.35px;
1033
1040
1034 &.cb-line-selected .cb-code {
1041 &.cb-line-selected .cb-code {
1035 background: @comment-highlight-color !important;
1042 background: @comment-highlight-color !important;
1036 }
1043 }
1037
1044
1038 span.cb-code {
1045 span.cb-code {
1039 line-height: @cb-line-height;
1046 line-height: @cb-line-height;
1040 padding-left: @cb-line-code-padding;
1047 padding-left: @cb-line-code-padding;
1041 padding-right: @cb-line-code-padding;
1048 padding-right: @cb-line-code-padding;
1042 display: block;
1049 display: block;
1043 white-space: pre-wrap;
1050 white-space: pre-wrap;
1044 font-family: @font-family-monospace;
1051 font-family: @font-family-monospace;
1045 word-break: break-word;
1052 word-break: break-word;
1046 .nonl {
1053 .nonl {
1047 color: @color5;
1054 color: @color5;
1048 }
1055 }
1049 }
1056 }
1050
1057
1051 &> button.cb-comment-box-opener {
1058 &> button.cb-comment-box-opener {
1052 padding: 2px 6px 2px 6px;
1059 padding: 2px 6px 2px 6px;
1053 margin-left: -20px;
1060 margin-left: -20px;
1054 margin-top: -2px;
1061 margin-top: -2px;
1055 border-radius: @border-radius;
1062 border-radius: @border-radius;
1056 position: absolute;
1063 position: absolute;
1057 display: none;
1064 display: none;
1058 }
1065 }
1059 .cb-comment {
1066 .cb-comment {
1060 margin-top: 10px;
1067 margin-top: 10px;
1061 white-space: normal;
1068 white-space: normal;
1062 }
1069 }
1063 }
1070 }
1064 &:hover {
1071 &:hover {
1065 button.cb-comment-box-opener {
1072 button.cb-comment-box-opener {
1066 display: block;
1073 display: block;
1067 }
1074 }
1068 &+ td button.cb-comment-box-opener {
1075 &+ td button.cb-comment-box-opener {
1069 display: block
1076 display: block
1070 }
1077 }
1071 }
1078 }
1072
1079
1073 &.cb-data {
1080 &.cb-data {
1074 text-align: right;
1081 text-align: right;
1075 width: 30px;
1082 width: 30px;
1076 font-family: @font-family-monospace;
1083 font-family: @font-family-monospace;
1077
1084
1078 .icon-comment {
1085 .icon-comment {
1079 cursor: pointer;
1086 cursor: pointer;
1080 }
1087 }
1081 &.cb-line-selected > div {
1088 &.cb-line-selected > div {
1082 display: block;
1089 display: block;
1083 background: @comment-highlight-color !important;
1090 background: @comment-highlight-color !important;
1084 line-height: @cb-line-height;
1091 line-height: @cb-line-height;
1085 color: rgba(0, 0, 0, 0.3);
1092 color: rgba(0, 0, 0, 0.3);
1086 }
1093 }
1087 }
1094 }
1088
1095
1089 &.cb-lineno {
1096 &.cb-lineno {
1090 padding: 0;
1097 padding: 0;
1091 width: 50px;
1098 width: 50px;
1092 color: rgba(0, 0, 0, 0.3);
1099 color: rgba(0, 0, 0, 0.3);
1093 text-align: right;
1100 text-align: right;
1094 border-right: 1px solid #eee;
1101 border-right: 1px solid #eee;
1095 font-family: @font-family-monospace;
1102 font-family: @font-family-monospace;
1096
1103
1097 a::before {
1104 a::before {
1098 content: attr(data-line-no);
1105 content: attr(data-line-no);
1099 }
1106 }
1100 &.cb-line-selected a {
1107 &.cb-line-selected a {
1101 background: @comment-highlight-color !important;
1108 background: @comment-highlight-color !important;
1102 }
1109 }
1103
1110
1104 a {
1111 a {
1105 display: block;
1112 display: block;
1106 padding-right: @cb-line-code-padding;
1113 padding-right: @cb-line-code-padding;
1107 padding-left: @cb-line-code-padding;
1114 padding-left: @cb-line-code-padding;
1108 line-height: @cb-line-height;
1115 line-height: @cb-line-height;
1109 color: rgba(0, 0, 0, 0.3);
1116 color: rgba(0, 0, 0, 0.3);
1110 }
1117 }
1111 }
1118 }
1112
1119
1113 &.cb-empty {
1120 &.cb-empty {
1114 background: @grey7;
1121 background: @grey7;
1115 }
1122 }
1116
1123
1117 ins {
1124 ins {
1118 color: black;
1125 color: black;
1119 background: #a6f3a6;
1126 background: #a6f3a6;
1120 text-decoration: none;
1127 text-decoration: none;
1121 }
1128 }
1122 del {
1129 del {
1123 color: black;
1130 color: black;
1124 background: #f8cbcb;
1131 background: #f8cbcb;
1125 text-decoration: none;
1132 text-decoration: none;
1126 }
1133 }
1127 &.cb-addition {
1134 &.cb-addition {
1128 background: #ecffec;
1135 background: #ecffec;
1129
1136
1130 &.blob-lineno {
1137 &.blob-lineno {
1131 background: #ddffdd;
1138 background: #ddffdd;
1132 }
1139 }
1133 }
1140 }
1134 &.cb-deletion {
1141 &.cb-deletion {
1135 background: #ffecec;
1142 background: #ffecec;
1136
1143
1137 &.blob-lineno {
1144 &.blob-lineno {
1138 background: #ffdddd;
1145 background: #ffdddd;
1139 }
1146 }
1140 }
1147 }
1141
1148
1142 &.cb-annotate-info {
1149 &.cb-annotate-info {
1143 width: 320px;
1150 width: 320px;
1144 min-width: 320px;
1151 min-width: 320px;
1145 max-width: 320px;
1152 max-width: 320px;
1146 padding: 5px 2px;
1153 padding: 5px 2px;
1147 font-size: 13px;
1154 font-size: 13px;
1148
1155
1149 strong.cb-annotate-message {
1156 strong.cb-annotate-message {
1150 padding: 5px 0;
1157 padding: 5px 0;
1151 white-space: pre-line;
1158 white-space: pre-line;
1152 display: inline-block;
1159 display: inline-block;
1153 }
1160 }
1154 .rc-user {
1161 .rc-user {
1155 float: none;
1162 float: none;
1156 padding: 0 6px 0 17px;
1163 padding: 0 6px 0 17px;
1157 min-width: auto;
1164 min-width: auto;
1158 min-height: auto;
1165 min-height: auto;
1159 }
1166 }
1160 }
1167 }
1161
1168
1162 &.cb-annotate-revision {
1169 &.cb-annotate-revision {
1163 cursor: pointer;
1170 cursor: pointer;
1164 text-align: right;
1171 text-align: right;
1165 }
1172 }
1166 }
1173 }
1167 }
1174 }
@@ -1,567 +1,569 b''
1 <%def name="diff_line_anchor(filename, line, type)"><%
1 <%def name="diff_line_anchor(filename, line, type)"><%
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 %></%def>
3 %></%def>
4
4
5 <%def name="action_class(action)"><%
5 <%def name="action_class(action)"><%
6 return {
6 return {
7 '-': 'cb-deletion',
7 '-': 'cb-deletion',
8 '+': 'cb-addition',
8 '+': 'cb-addition',
9 ' ': 'cb-context',
9 ' ': 'cb-context',
10 }.get(action, 'cb-empty')
10 }.get(action, 'cb-empty')
11 %></%def>
11 %></%def>
12
12
13 <%def name="op_class(op_id)"><%
13 <%def name="op_class(op_id)"><%
14 return {
14 return {
15 DEL_FILENODE: 'deletion', # file deleted
15 DEL_FILENODE: 'deletion', # file deleted
16 BIN_FILENODE: 'warning' # binary diff hidden
16 BIN_FILENODE: 'warning' # binary diff hidden
17 }.get(op_id, 'addition')
17 }.get(op_id, 'addition')
18 %></%def>
18 %></%def>
19
19
20 <%def name="link_for(**kw)"><%
20 <%def name="link_for(**kw)"><%
21 new_args = request.GET.mixed()
21 new_args = request.GET.mixed()
22 new_args.update(kw)
22 new_args.update(kw)
23 return h.url('', **new_args)
23 return h.url('', **new_args)
24 %></%def>
24 %></%def>
25
25
26 <%def name="render_diffset(diffset, commit=None,
26 <%def name="render_diffset(diffset, commit=None,
27
27
28 # collapse all file diff entries when there are more than this amount of files in the diff
28 # collapse all file diff entries when there are more than this amount of files in the diff
29 collapse_when_files_over=20,
29 collapse_when_files_over=20,
30
30
31 # collapse lines in the diff when more than this amount of lines changed in the file diff
31 # collapse lines in the diff when more than this amount of lines changed in the file diff
32 lines_changed_limit=500,
32 lines_changed_limit=500,
33
33
34 # add a ruler at to the output
34 # add a ruler at to the output
35 ruler_at_chars=0,
35 ruler_at_chars=0,
36
36
37 # turn on inline comments
37 # show inline comments
38 use_comments=False,
38 use_comments=False,
39
39
40 # disable new comments
41 disable_new_comments=False,
42
40 )">
43 )">
41
44
42 %if use_comments:
45 %if use_comments:
43 <div id="cb-comments-inline-container-template" class="js-template">
46 <div id="cb-comments-inline-container-template" class="js-template">
44 ${inline_comments_container([])}
47 ${inline_comments_container([])}
45 </div>
48 </div>
46 <div class="js-template" id="cb-comment-inline-form-template">
49 <div class="js-template" id="cb-comment-inline-form-template">
47 <div class="comment-inline-form ac">
50 <div class="comment-inline-form ac">
48 %if c.rhodecode_user.username != h.DEFAULT_USER:
51 %if c.rhodecode_user.username != h.DEFAULT_USER:
49 ${h.form('#', method='get')}
52 ${h.form('#', method='get')}
50 <div id="edit-container_{1}" class="clearfix">
53 <div id="edit-container_{1}" class="clearfix">
51 <div class="comment-title pull-left">
54 <div class="comment-title pull-left">
52 ${_('Create a comment on line {1}.')}
55 ${_('Create a comment on line {1}.')}
53 </div>
56 </div>
54 <div class="comment-help pull-right">
57 <div class="comment-help pull-right">
55 ${(_('Comments parsed using %s syntax with %s support.') % (
58 ${(_('Comments parsed using %s syntax with %s support.') % (
56 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
59 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
57 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
60 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
58 )
61 )
59 )|n
62 )|n
60 }
63 }
61 </div>
64 </div>
62 <div style="clear: both"></div>
65 <div style="clear: both"></div>
63 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
66 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
64 </div>
67 </div>
65 <div id="preview-container_{1}" class="clearfix" style="display: none;">
68 <div id="preview-container_{1}" class="clearfix" style="display: none;">
66 <div class="comment-help">
69 <div class="comment-help">
67 ${_('Comment preview')}
70 ${_('Comment preview')}
68 </div>
71 </div>
69 <div id="preview-box_{1}" class="preview-box"></div>
72 <div id="preview-box_{1}" class="preview-box"></div>
70 </div>
73 </div>
71 <div class="comment-footer">
74 <div class="comment-footer">
72 <div class="action-buttons">
75 <div class="action-buttons">
73 <input type="hidden" name="f_path" value="{0}">
76 <input type="hidden" name="f_path" value="{0}">
74 <input type="hidden" name="line" value="{1}">
77 <input type="hidden" name="line" value="{1}">
75 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
78 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
76 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
79 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
77 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
80 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
78 </div>
81 </div>
79 <div class="comment-button">
82 <div class="comment-button">
80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
83 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 ${_('Cancel')}
84 ${_('Cancel')}
82 </button>
85 </button>
83 </div>
86 </div>
84 ${h.end_form()}
87 ${h.end_form()}
85 </div>
88 </div>
86 %else:
89 %else:
87 ${h.form('', class_='inline-form comment-form-login', method='get')}
90 ${h.form('', class_='inline-form comment-form-login', method='get')}
88 <div class="pull-left">
91 <div class="pull-left">
89 <div class="comment-help pull-right">
92 <div class="comment-help pull-right">
90 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
93 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
91 </div>
94 </div>
92 </div>
95 </div>
93 <div class="comment-button pull-right">
96 <div class="comment-button pull-right">
94 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
97 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
95 ${_('Cancel')}
98 ${_('Cancel')}
96 </button>
99 </button>
97 </div>
100 </div>
98 <div class="clearfix"></div>
101 <div class="clearfix"></div>
99 ${h.end_form()}
102 ${h.end_form()}
100 %endif
103 %endif
101 </div>
104 </div>
102 </div>
105 </div>
103
106
104 %endif
107 %endif
105 <%
108 <%
106 collapse_all = len(diffset.files) > collapse_when_files_over
109 collapse_all = len(diffset.files) > collapse_when_files_over
107 %>
110 %>
108
111
109 %if c.diffmode == 'sideside':
112 %if c.diffmode == 'sideside':
110 <style>
113 <style>
111 .wrapper {
114 .wrapper {
112 max-width: 1600px !important;
115 max-width: 1600px !important;
113 }
116 }
114 </style>
117 </style>
115 %endif
118 %endif
116 %if ruler_at_chars:
119 %if ruler_at_chars:
117 <style>
120 <style>
118 .diff table.cb .cb-content:after {
121 .diff table.cb .cb-content:after {
119 content: "";
122 content: "";
120 border-left: 1px solid blue;
123 border-left: 1px solid blue;
121 position: absolute;
124 position: absolute;
122 top: 0;
125 top: 0;
123 height: 18px;
126 height: 18px;
124 opacity: .2;
127 opacity: .2;
125 z-index: 10;
128 z-index: 10;
126 ## +5 to account for diff action (+/-)
129 ## +5 to account for diff action (+/-)
127 left: ${ruler_at_chars + 5}ch;
130 left: ${ruler_at_chars + 5}ch;
128 </style>
131 </style>
129 %endif
132 %endif
130
133 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
131 <div class="diffset">
132 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
134 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
133 %if commit:
135 %if commit:
134 <div class="pull-right">
136 <div class="pull-right">
135 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
137 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
136 ${_('Browse Files')}
138 ${_('Browse Files')}
137 </a>
139 </a>
138 </div>
140 </div>
139 %endif
141 %endif
140 <h2 class="clearinner">
142 <h2 class="clearinner">
141 %if commit:
143 %if commit:
142 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
144 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
143 ${h.age_component(commit.date)} -
145 ${h.age_component(commit.date)} -
144 %endif
146 %endif
145 %if diffset.limited_diff:
147 %if diffset.limited_diff:
146 ${_('The requested commit is too big and content was truncated.')}
148 ${_('The requested commit is too big and content was truncated.')}
147
149
148 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
150 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
149 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
151 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
150 %else:
152 %else:
151 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
153 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
152 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
154 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
153 %endif
155 %endif
154 </h2>
156 </h2>
155 </div>
157 </div>
156
158
157 %if not diffset.files:
159 %if not diffset.files:
158 <p class="empty_data">${_('No files')}</p>
160 <p class="empty_data">${_('No files')}</p>
159 %endif
161 %endif
160
162
161 <div class="filediffs">
163 <div class="filediffs">
162 %for i, filediff in enumerate(diffset.files):
164 %for i, filediff in enumerate(diffset.files):
163 <%
165 <%
164 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
166 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
165 over_lines_changed_limit = lines_changed > lines_changed_limit
167 over_lines_changed_limit = lines_changed > lines_changed_limit
166 %>
168 %>
167 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
169 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
168 <div
170 <div
169 class="filediff"
171 class="filediff"
170 data-f-path="${filediff['patch']['filename']}"
172 data-f-path="${filediff['patch']['filename']}"
171 id="a_${h.FID('', filediff['patch']['filename'])}">
173 id="a_${h.FID('', filediff['patch']['filename'])}">
172 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
174 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
173 <div class="filediff-collapse-indicator"></div>
175 <div class="filediff-collapse-indicator"></div>
174 ${diff_ops(filediff)}
176 ${diff_ops(filediff)}
175 </label>
177 </label>
176 ${diff_menu(filediff, use_comments=use_comments)}
178 ${diff_menu(filediff, use_comments=use_comments)}
177 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
179 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
178 %if not filediff.hunks:
180 %if not filediff.hunks:
179 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
181 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
180 <tr>
182 <tr>
181 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
183 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
182 %if op_id == DEL_FILENODE:
184 %if op_id == DEL_FILENODE:
183 ${_('File was deleted')}
185 ${_('File was deleted')}
184 %elif op_id == BIN_FILENODE:
186 %elif op_id == BIN_FILENODE:
185 ${_('Binary file hidden')}
187 ${_('Binary file hidden')}
186 %else:
188 %else:
187 ${op_text}
189 ${op_text}
188 %endif
190 %endif
189 </td>
191 </td>
190 </tr>
192 </tr>
191 %endfor
193 %endfor
192 %endif
194 %endif
193 %if over_lines_changed_limit:
195 %if over_lines_changed_limit:
194 <tr class="cb-warning cb-collapser">
196 <tr class="cb-warning cb-collapser">
195 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
197 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
196 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
198 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
197 <a href="#" class="cb-expand"
199 <a href="#" class="cb-expand"
198 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
200 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
199 </a>
201 </a>
200 <a href="#" class="cb-collapse"
202 <a href="#" class="cb-collapse"
201 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
203 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
202 </a>
204 </a>
203 </td>
205 </td>
204 </tr>
206 </tr>
205 %endif
207 %endif
206 %if filediff.patch['is_limited_diff']:
208 %if filediff.patch['is_limited_diff']:
207 <tr class="cb-warning cb-collapser">
209 <tr class="cb-warning cb-collapser">
208 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
210 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
209 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
211 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
210 </td>
212 </td>
211 </tr>
213 </tr>
212 %endif
214 %endif
213 %for hunk in filediff.hunks:
215 %for hunk in filediff.hunks:
214 <tr class="cb-hunk">
216 <tr class="cb-hunk">
215 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
217 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
216 ## TODO: dan: add ajax loading of more context here
218 ## TODO: dan: add ajax loading of more context here
217 ## <a href="#">
219 ## <a href="#">
218 <i class="icon-more"></i>
220 <i class="icon-more"></i>
219 ## </a>
221 ## </a>
220 </td>
222 </td>
221 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
223 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
222 @@
224 @@
223 -${hunk.source_start},${hunk.source_length}
225 -${hunk.source_start},${hunk.source_length}
224 +${hunk.target_start},${hunk.target_length}
226 +${hunk.target_start},${hunk.target_length}
225 ${hunk.section_header}
227 ${hunk.section_header}
226 </td>
228 </td>
227 </tr>
229 </tr>
228 %if c.diffmode == 'unified':
230 %if c.diffmode == 'unified':
229 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
231 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
230 %elif c.diffmode == 'sideside':
232 %elif c.diffmode == 'sideside':
231 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
233 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
232 %else:
234 %else:
233 <tr class="cb-line">
235 <tr class="cb-line">
234 <td>unknown diff mode</td>
236 <td>unknown diff mode</td>
235 </tr>
237 </tr>
236 %endif
238 %endif
237 %endfor
239 %endfor
238 </table>
240 </table>
239 </div>
241 </div>
240 %endfor
242 %endfor
241 </div>
243 </div>
242 </div>
244 </div>
243 </%def>
245 </%def>
244
246
245 <%def name="diff_ops(filediff)">
247 <%def name="diff_ops(filediff)">
246 <%
248 <%
247 stats = filediff['patch']['stats']
249 stats = filediff['patch']['stats']
248 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
250 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
249 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
251 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
250 %>
252 %>
251 <span class="pill">
253 <span class="pill">
252 %if filediff.source_file_path and filediff.target_file_path:
254 %if filediff.source_file_path and filediff.target_file_path:
253 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
255 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
254 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
256 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
255 %else:
257 %else:
256 ## file was modified
258 ## file was modified
257 <strong>${filediff.source_file_path}</strong>
259 <strong>${filediff.source_file_path}</strong>
258 %endif
260 %endif
259 %else:
261 %else:
260 %if filediff.source_file_path:
262 %if filediff.source_file_path:
261 ## file was deleted
263 ## file was deleted
262 <strong>${filediff.source_file_path}</strong>
264 <strong>${filediff.source_file_path}</strong>
263 %else:
265 %else:
264 ## file was added
266 ## file was added
265 <strong>${filediff.target_file_path}</strong>
267 <strong>${filediff.target_file_path}</strong>
266 %endif
268 %endif
267 %endif
269 %endif
268 </span>
270 </span>
269 <span class="pill-group" style="float: left">
271 <span class="pill-group" style="float: left">
270 %if filediff.patch['is_limited_diff']:
272 %if filediff.patch['is_limited_diff']:
271 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
273 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
272 %endif
274 %endif
273 %if RENAMED_FILENODE in stats['ops']:
275 %if RENAMED_FILENODE in stats['ops']:
274 <span class="pill" op="renamed">renamed</span>
276 <span class="pill" op="renamed">renamed</span>
275 %endif
277 %endif
276
278
277 %if NEW_FILENODE in stats['ops']:
279 %if NEW_FILENODE in stats['ops']:
278 <span class="pill" op="created">created</span>
280 <span class="pill" op="created">created</span>
279 %if filediff['target_mode'].startswith('120'):
281 %if filediff['target_mode'].startswith('120'):
280 <span class="pill" op="symlink">symlink</span>
282 <span class="pill" op="symlink">symlink</span>
281 %else:
283 %else:
282 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
284 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
283 %endif
285 %endif
284 %endif
286 %endif
285
287
286 %if DEL_FILENODE in stats['ops']:
288 %if DEL_FILENODE in stats['ops']:
287 <span class="pill" op="removed">removed</span>
289 <span class="pill" op="removed">removed</span>
288 %endif
290 %endif
289
291
290 %if CHMOD_FILENODE in stats['ops']:
292 %if CHMOD_FILENODE in stats['ops']:
291 <span class="pill" op="mode">
293 <span class="pill" op="mode">
292 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
294 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
293 </span>
295 </span>
294 %endif
296 %endif
295 </span>
297 </span>
296
298
297 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
299 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
298
300
299 <span class="pill-group" style="float: right">
301 <span class="pill-group" style="float: right">
300 %if BIN_FILENODE in stats['ops']:
302 %if BIN_FILENODE in stats['ops']:
301 <span class="pill" op="binary">binary</span>
303 <span class="pill" op="binary">binary</span>
302 %if MOD_FILENODE in stats['ops']:
304 %if MOD_FILENODE in stats['ops']:
303 <span class="pill" op="modified">modified</span>
305 <span class="pill" op="modified">modified</span>
304 %endif
306 %endif
305 %endif
307 %endif
306 %if stats['added']:
308 %if stats['added']:
307 <span class="pill" op="added">+${stats['added']}</span>
309 <span class="pill" op="added">+${stats['added']}</span>
308 %endif
310 %endif
309 %if stats['deleted']:
311 %if stats['deleted']:
310 <span class="pill" op="deleted">-${stats['deleted']}</span>
312 <span class="pill" op="deleted">-${stats['deleted']}</span>
311 %endif
313 %endif
312 </span>
314 </span>
313
315
314 </%def>
316 </%def>
315
317
316 <%def name="nice_mode(filemode)">
318 <%def name="nice_mode(filemode)">
317 ${filemode.startswith('100') and filemode[3:] or filemode}
319 ${filemode.startswith('100') and filemode[3:] or filemode}
318 </%def>
320 </%def>
319
321
320 <%def name="diff_menu(filediff, use_comments=False)">
322 <%def name="diff_menu(filediff, use_comments=False)">
321 <div class="filediff-menu">
323 <div class="filediff-menu">
322 %if filediff.diffset.source_ref:
324 %if filediff.diffset.source_ref:
323 %if filediff.patch['operation'] in ['D', 'M']:
325 %if filediff.patch['operation'] in ['D', 'M']:
324 <a
326 <a
325 class="tooltip"
327 class="tooltip"
326 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
328 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
327 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
329 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
328 >
330 >
329 ${_('Show file before')}
331 ${_('Show file before')}
330 </a>
332 </a>
331 %else:
333 %else:
332 <span
334 <span
333 class="tooltip"
335 class="tooltip"
334 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
336 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
335 >
337 >
336 ${_('Show file before')}
338 ${_('Show file before')}
337 </span>
339 </span>
338 %endif
340 %endif
339 %if filediff.patch['operation'] in ['A', 'M']:
341 %if filediff.patch['operation'] in ['A', 'M']:
340 <a
342 <a
341 class="tooltip"
343 class="tooltip"
342 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
344 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
343 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
345 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
344 >
346 >
345 ${_('Show file after')}
347 ${_('Show file after')}
346 </a>
348 </a>
347 %else:
349 %else:
348 <span
350 <span
349 class="tooltip"
351 class="tooltip"
350 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
352 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
351 >
353 >
352 ${_('Show file after')}
354 ${_('Show file after')}
353 </span>
355 </span>
354 %endif
356 %endif
355 <a
357 <a
356 class="tooltip"
358 class="tooltip"
357 title="${h.tooltip(_('Raw diff'))}"
359 title="${h.tooltip(_('Raw diff'))}"
358 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
360 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
359 >
361 >
360 ${_('Raw diff')}
362 ${_('Raw diff')}
361 </a>
363 </a>
362 <a
364 <a
363 class="tooltip"
365 class="tooltip"
364 title="${h.tooltip(_('Download diff'))}"
366 title="${h.tooltip(_('Download diff'))}"
365 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
367 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
366 >
368 >
367 ${_('Download diff')}
369 ${_('Download diff')}
368 </a>
370 </a>
369
371
370 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
372 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
371 %if hasattr(c, 'ignorews_url'):
373 %if hasattr(c, 'ignorews_url'):
372 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
374 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
373 %endif
375 %endif
374 %if hasattr(c, 'context_url'):
376 %if hasattr(c, 'context_url'):
375 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
377 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
376 %endif
378 %endif
377
379
378
380
379 %if use_comments:
381 %if use_comments:
380 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
382 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
381 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
383 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
382 </a>
384 </a>
383 %endif
385 %endif
384 %endif
386 %endif
385 </div>
387 </div>
386 </%def>
388 </%def>
387
389
388
390
389 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
391 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
390 <%def name="inline_comments_container(comments)">
392 <%def name="inline_comments_container(comments)">
391 <div class="inline-comments">
393 <div class="inline-comments">
392 %for comment in comments:
394 %for comment in comments:
393 ${commentblock.comment_block(comment, inline=True)}
395 ${commentblock.comment_block(comment, inline=True)}
394 %endfor
396 %endfor
395 <span onclick="return Rhodecode.comments.createComment(this)"
397 <span onclick="return Rhodecode.comments.createComment(this)"
396 class="btn btn-secondary cb-comment-add-button">
398 class="btn btn-secondary cb-comment-add-button">
397 ${_('Add another comment')}
399 ${_('Add another comment')}
398 </span>
400 </span>
399 </div>
401 </div>
400 </%def>
402 </%def>
401
403
402
404
403 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
405 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
404 %for i, line in enumerate(hunk.sideside):
406 %for i, line in enumerate(hunk.sideside):
405 <%
407 <%
406 old_line_anchor, new_line_anchor = None, None
408 old_line_anchor, new_line_anchor = None, None
407 if line.original.lineno:
409 if line.original.lineno:
408 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
410 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
409 if line.modified.lineno:
411 if line.modified.lineno:
410 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
412 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
411 %>
413 %>
412 <tr class="cb-line">
414 <tr class="cb-line">
413 <td class="cb-data ${action_class(line.original.action)}"
415 <td class="cb-data ${action_class(line.original.action)}"
414 data-line-number="${line.original.lineno}"
416 data-line-number="${line.original.lineno}"
415 >
417 >
416 <div>
418 <div>
417 %if line.original.comments:
419 %if line.original.comments:
418 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
420 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
419 %endif
421 %endif
420 </div>
422 </div>
421 </td>
423 </td>
422 <td class="cb-lineno ${action_class(line.original.action)}"
424 <td class="cb-lineno ${action_class(line.original.action)}"
423 data-line-number="${line.original.lineno}"
425 data-line-number="${line.original.lineno}"
424 %if old_line_anchor:
426 %if old_line_anchor:
425 id="${old_line_anchor}"
427 id="${old_line_anchor}"
426 %endif
428 %endif
427 >
429 >
428 %if line.original.lineno:
430 %if line.original.lineno:
429 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
431 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
430 %endif
432 %endif
431 </td>
433 </td>
432 <td class="cb-content ${action_class(line.original.action)}"
434 <td class="cb-content ${action_class(line.original.action)}"
433 data-line-number="o${line.original.lineno}"
435 data-line-number="o${line.original.lineno}"
434 >
436 >
435 %if use_comments and line.original.lineno:
437 %if use_comments and line.original.lineno:
436 ${render_add_comment_button()}
438 ${render_add_comment_button()}
437 %endif
439 %endif
438 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
440 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
439 %if use_comments and line.original.lineno and line.original.comments:
441 %if use_comments and line.original.lineno and line.original.comments:
440 ${inline_comments_container(line.original.comments)}
442 ${inline_comments_container(line.original.comments)}
441 %endif
443 %endif
442 </td>
444 </td>
443 <td class="cb-data ${action_class(line.modified.action)}"
445 <td class="cb-data ${action_class(line.modified.action)}"
444 data-line-number="${line.modified.lineno}"
446 data-line-number="${line.modified.lineno}"
445 >
447 >
446 <div>
448 <div>
447 %if line.modified.comments:
449 %if line.modified.comments:
448 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
450 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
449 %endif
451 %endif
450 </div>
452 </div>
451 </td>
453 </td>
452 <td class="cb-lineno ${action_class(line.modified.action)}"
454 <td class="cb-lineno ${action_class(line.modified.action)}"
453 data-line-number="${line.modified.lineno}"
455 data-line-number="${line.modified.lineno}"
454 %if new_line_anchor:
456 %if new_line_anchor:
455 id="${new_line_anchor}"
457 id="${new_line_anchor}"
456 %endif
458 %endif
457 >
459 >
458 %if line.modified.lineno:
460 %if line.modified.lineno:
459 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
461 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
460 %endif
462 %endif
461 </td>
463 </td>
462 <td class="cb-content ${action_class(line.modified.action)}"
464 <td class="cb-content ${action_class(line.modified.action)}"
463 data-line-number="n${line.modified.lineno}"
465 data-line-number="n${line.modified.lineno}"
464 >
466 >
465 %if use_comments and line.modified.lineno:
467 %if use_comments and line.modified.lineno:
466 ${render_add_comment_button()}
468 ${render_add_comment_button()}
467 %endif
469 %endif
468 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
470 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
469 %if use_comments and line.modified.lineno and line.modified.comments:
471 %if use_comments and line.modified.lineno and line.modified.comments:
470 ${inline_comments_container(line.modified.comments)}
472 ${inline_comments_container(line.modified.comments)}
471 %endif
473 %endif
472 </td>
474 </td>
473 </tr>
475 </tr>
474 %endfor
476 %endfor
475 </%def>
477 </%def>
476
478
477
479
478 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
480 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
479 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
481 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
480 <%
482 <%
481 old_line_anchor, new_line_anchor = None, None
483 old_line_anchor, new_line_anchor = None, None
482 if old_line_no:
484 if old_line_no:
483 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
485 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
484 if new_line_no:
486 if new_line_no:
485 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
487 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
486 %>
488 %>
487 <tr class="cb-line">
489 <tr class="cb-line">
488 <td class="cb-data ${action_class(action)}">
490 <td class="cb-data ${action_class(action)}">
489 <div>
491 <div>
490 %if comments:
492 %if comments:
491 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
493 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
492 %endif
494 %endif
493 </div>
495 </div>
494 </td>
496 </td>
495 <td class="cb-lineno ${action_class(action)}"
497 <td class="cb-lineno ${action_class(action)}"
496 data-line-number="${old_line_no}"
498 data-line-number="${old_line_no}"
497 %if old_line_anchor:
499 %if old_line_anchor:
498 id="${old_line_anchor}"
500 id="${old_line_anchor}"
499 %endif
501 %endif
500 >
502 >
501 %if old_line_anchor:
503 %if old_line_anchor:
502 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
504 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
503 %endif
505 %endif
504 </td>
506 </td>
505 <td class="cb-lineno ${action_class(action)}"
507 <td class="cb-lineno ${action_class(action)}"
506 data-line-number="${new_line_no}"
508 data-line-number="${new_line_no}"
507 %if new_line_anchor:
509 %if new_line_anchor:
508 id="${new_line_anchor}"
510 id="${new_line_anchor}"
509 %endif
511 %endif
510 >
512 >
511 %if new_line_anchor:
513 %if new_line_anchor:
512 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
514 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
513 %endif
515 %endif
514 </td>
516 </td>
515 <td class="cb-content ${action_class(action)}"
517 <td class="cb-content ${action_class(action)}"
516 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
518 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
517 >
519 >
518 %if use_comments:
520 %if use_comments:
519 ${render_add_comment_button()}
521 ${render_add_comment_button()}
520 %endif
522 %endif
521 <span class="cb-code">${action} ${content or '' | n}</span>
523 <span class="cb-code">${action} ${content or '' | n}</span>
522 %if use_comments and comments:
524 %if use_comments and comments:
523 ${inline_comments_container(comments)}
525 ${inline_comments_container(comments)}
524 %endif
526 %endif
525 </td>
527 </td>
526 </tr>
528 </tr>
527 %endfor
529 %endfor
528 </%def>
530 </%def>
529
531
530 <%def name="render_add_comment_button()">
532 <%def name="render_add_comment_button()">
531 <button
533 <button
532 class="btn btn-small btn-primary cb-comment-box-opener"
534 class="btn btn-small btn-primary cb-comment-box-opener"
533 onclick="return Rhodecode.comments.createComment(this)"
535 onclick="return Rhodecode.comments.createComment(this)"
534 ><span>+</span></button>
536 ><span>+</span></button>
535 </%def>
537 </%def>
536
538
537 <%def name="render_diffset_menu()">
539 <%def name="render_diffset_menu()">
538 <div class="diffset-menu clearinner">
540 <div class="diffset-menu clearinner">
539 <div class="pull-right">
541 <div class="pull-right">
540 <div class="btn-group">
542 <div class="btn-group">
541 <a
543 <a
542 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
544 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
543 title="${_('View side by side')}"
545 title="${_('View side by side')}"
544 href="${h.url_replace(diffmode='sideside')}">
546 href="${h.url_replace(diffmode='sideside')}">
545 <span>${_('Side by Side')}</span>
547 <span>${_('Side by Side')}</span>
546 </a>
548 </a>
547 <a
549 <a
548 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
550 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
549 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
551 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
550 <span>${_('Unified')}</span>
552 <span>${_('Unified')}</span>
551 </a>
553 </a>
552 </div>
554 </div>
553 </div>
555 </div>
554 <div class="pull-left">
556 <div class="pull-left">
555 <div class="btn-group">
557 <div class="btn-group">
556 <a
558 <a
557 class="btn"
559 class="btn"
558 href="#"
560 href="#"
559 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
561 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
560 <a
562 <a
561 class="btn"
563 class="btn"
562 href="#"
564 href="#"
563 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
565 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
564 </div>
566 </div>
565 </div>
567 </div>
566 </div>
568 </div>
567 </%def>
569 </%def>
@@ -1,636 +1,511 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31 <script type="text/javascript">
31 <script type="text/javascript">
32 // TODO: marcink switch this to pyroutes
32 // TODO: marcink switch this to pyroutes
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 </script>
35 </script>
36 <div class="box">
36 <div class="box">
37 <div class="title">
37 <div class="title">
38 ${self.repo_page_title(c.rhodecode_db_repo)}
38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 </div>
39 </div>
40
40
41 ${self.breadcrumbs()}
41 ${self.breadcrumbs()}
42
42
43
43
44 <div class="box pr-summary">
44 <div class="box pr-summary">
45 <div class="summary-details block-left">
45 <div class="summary-details block-left">
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 <div class="pr-details-title">
47 <div class="pr-details-title">
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
48 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 %if c.allowed_to_update:
49 %if c.allowed_to_update:
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 % if c.allowed_to_delete:
51 % if c.allowed_to_delete:
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 ${h.end_form()}
55 ${h.end_form()}
56 % else:
56 % else:
57 ${_('Delete')}
57 ${_('Delete')}
58 % endif
58 % endif
59 </div>
59 </div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
62 %endif
62 %endif
63 </div>
63 </div>
64
64
65 <div id="summary" class="fields pr-details-content">
65 <div id="summary" class="fields pr-details-content">
66 <div class="field">
66 <div class="field">
67 <div class="label-summary">
67 <div class="label-summary">
68 <label>${_('Origin')}:</label>
68 <label>${_('Origin')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 <div class="pr-origininfo">
71 <div class="pr-origininfo">
72 ## branch link is only valid if it is a branch
72 ## branch link is only valid if it is a branch
73 <span class="tag">
73 <span class="tag">
74 %if c.pull_request.source_ref_parts.type == 'branch':
74 %if c.pull_request.source_ref_parts.type == 'branch':
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 %else:
76 %else:
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 %endif
78 %endif
79 </span>
79 </span>
80 <span class="clone-url">
80 <span class="clone-url">
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 </span>
82 </span>
83 </div>
83 </div>
84 <div class="pr-pullinfo">
84 <div class="pr-pullinfo">
85 %if h.is_hg(c.pull_request.source_repo):
85 %if h.is_hg(c.pull_request.source_repo):
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 %elif h.is_git(c.pull_request.source_repo):
87 %elif h.is_git(c.pull_request.source_repo):
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 %endif
89 %endif
90 </div>
90 </div>
91 </div>
91 </div>
92 </div>
92 </div>
93 <div class="field">
93 <div class="field">
94 <div class="label-summary">
94 <div class="label-summary">
95 <label>${_('Target')}:</label>
95 <label>${_('Target')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 <div class="pr-targetinfo">
98 <div class="pr-targetinfo">
99 ## branch link is only valid if it is a branch
99 ## branch link is only valid if it is a branch
100 <span class="tag">
100 <span class="tag">
101 %if c.pull_request.target_ref_parts.type == 'branch':
101 %if c.pull_request.target_ref_parts.type == 'branch':
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 %else:
103 %else:
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 %endif
105 %endif
106 </span>
106 </span>
107 <span class="clone-url">
107 <span class="clone-url">
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 </span>
109 </span>
110 </div>
110 </div>
111 </div>
111 </div>
112 </div>
112 </div>
113
113
114 ## Link to the shadow repository.
114 ## Link to the shadow repository.
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
115 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
116 <div class="field">
116 <div class="field">
117 <div class="label-summary">
117 <div class="label-summary">
118 <label>Merge:</label>
118 <label>Merge:</label>
119 </div>
119 </div>
120 <div class="input">
120 <div class="input">
121 <div class="pr-mergeinfo">
121 <div class="pr-mergeinfo">
122 %if h.is_hg(c.pull_request.target_repo):
122 %if h.is_hg(c.pull_request.target_repo):
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 %elif h.is_git(c.pull_request.target_repo):
124 %elif h.is_git(c.pull_request.target_repo):
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %endif
126 %endif
127 </div>
127 </div>
128 </div>
128 </div>
129 </div>
129 </div>
130 %endif
130 %endif
131
131
132 <div class="field">
132 <div class="field">
133 <div class="label-summary">
133 <div class="label-summary">
134 <label>${_('Review')}:</label>
134 <label>${_('Review')}:</label>
135 </div>
135 </div>
136 <div class="input">
136 <div class="input">
137 %if c.pull_request_review_status:
137 %if c.pull_request_review_status:
138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
139 <span class="changeset-status-lbl tooltip">
139 <span class="changeset-status-lbl tooltip">
140 %if c.pull_request.is_closed():
140 %if c.pull_request.is_closed():
141 ${_('Closed')},
141 ${_('Closed')},
142 %endif
142 %endif
143 ${h.commit_status_lbl(c.pull_request_review_status)}
143 ${h.commit_status_lbl(c.pull_request_review_status)}
144 </span>
144 </span>
145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
146 %endif
146 %endif
147 </div>
147 </div>
148 </div>
148 </div>
149 <div class="field">
149 <div class="field">
150 <div class="pr-description-label label-summary">
150 <div class="pr-description-label label-summary">
151 <label>${_('Description')}:</label>
151 <label>${_('Description')}:</label>
152 </div>
152 </div>
153 <div id="pr-desc" class="input">
153 <div id="pr-desc" class="input">
154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
155 </div>
155 </div>
156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
158 </div>
158 </div>
159 </div>
159 </div>
160 <div class="field">
160 <div class="field">
161 <div class="label-summary">
161 <div class="label-summary">
162 <label>${_('Comments')}:</label>
162 <label>${_('Comments')}:</label>
163 </div>
163 </div>
164 <div class="input">
164 <div class="input">
165 <div>
165 <div>
166 <div class="comments-number">
166 <div class="comments-number">
167 %if c.comments:
167 %if c.comments:
168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
169 %else:
169 %else:
170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
171 %endif
171 %endif
172 %if c.inline_cnt:
172 %if c.inline_cnt:
173 ## this is replaced with a proper link to first comment via JS linkifyComments() func
173 ## this is replaced with a proper link to first comment via JS linkifyComments() func
174 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
175 %else:
175 %else:
176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
177 %endif
177 %endif
178
178
179 % if c.outdated_cnt:
179 % if c.outdated_cnt:
180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
181 % endif
181 % endif
182 </div>
182 </div>
183 </div>
183 </div>
184 </div>
184 </div>
185 </div>
185 </div>
186 <div id="pr-save" class="field" style="display: none;">
186 <div id="pr-save" class="field" style="display: none;">
187 <div class="label-summary"></div>
187 <div class="label-summary"></div>
188 <div class="input">
188 <div class="input">
189 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
189 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
190 </div>
190 </div>
191 </div>
191 </div>
192 </div>
192 </div>
193 </div>
193 </div>
194 <div>
194 <div>
195 ## AUTHOR
195 ## AUTHOR
196 <div class="reviewers-title block-right">
196 <div class="reviewers-title block-right">
197 <div class="pr-details-title">
197 <div class="pr-details-title">
198 ${_('Author')}
198 ${_('Author')}
199 </div>
199 </div>
200 </div>
200 </div>
201 <div class="block-right pr-details-content reviewers">
201 <div class="block-right pr-details-content reviewers">
202 <ul class="group_members">
202 <ul class="group_members">
203 <li>
203 <li>
204 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
204 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
205 </li>
205 </li>
206 </ul>
206 </ul>
207 </div>
207 </div>
208 ## REVIEWERS
208 ## REVIEWERS
209 <div class="reviewers-title block-right">
209 <div class="reviewers-title block-right">
210 <div class="pr-details-title">
210 <div class="pr-details-title">
211 ${_('Pull request reviewers')}
211 ${_('Pull request reviewers')}
212 %if c.allowed_to_update:
212 %if c.allowed_to_update:
213 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
213 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
214 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
214 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
215 %endif
215 %endif
216 </div>
216 </div>
217 </div>
217 </div>
218 <div id="reviewers" class="block-right pr-details-content reviewers">
218 <div id="reviewers" class="block-right pr-details-content reviewers">
219 ## members goes here !
219 ## members goes here !
220 <input type="hidden" name="__start__" value="review_members:sequence">
220 <input type="hidden" name="__start__" value="review_members:sequence">
221 <ul id="review_members" class="group_members">
221 <ul id="review_members" class="group_members">
222 %for member,reasons,status in c.pull_request_reviewers:
222 %for member,reasons,status in c.pull_request_reviewers:
223 <li id="reviewer_${member.user_id}">
223 <li id="reviewer_${member.user_id}">
224 <div class="reviewers_member">
224 <div class="reviewers_member">
225 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
225 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
226 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
226 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
227 </div>
227 </div>
228 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
228 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
229 ${self.gravatar_with_user(member.email, 16)}
229 ${self.gravatar_with_user(member.email, 16)}
230 </div>
230 </div>
231 <input type="hidden" name="__start__" value="reviewer:mapping">
231 <input type="hidden" name="__start__" value="reviewer:mapping">
232 <input type="hidden" name="__start__" value="reasons:sequence">
232 <input type="hidden" name="__start__" value="reasons:sequence">
233 %for reason in reasons:
233 %for reason in reasons:
234 <div class="reviewer_reason">- ${reason}</div>
234 <div class="reviewer_reason">- ${reason}</div>
235 <input type="hidden" name="reason" value="${reason}">
235 <input type="hidden" name="reason" value="${reason}">
236
236
237 %endfor
237 %endfor
238 <input type="hidden" name="__end__" value="reasons:sequence">
238 <input type="hidden" name="__end__" value="reasons:sequence">
239 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
239 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
240 <input type="hidden" name="__end__" value="reviewer:mapping">
240 <input type="hidden" name="__end__" value="reviewer:mapping">
241 %if c.allowed_to_update:
241 %if c.allowed_to_update:
242 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
242 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
243 <i class="icon-remove-sign" ></i>
243 <i class="icon-remove-sign" ></i>
244 </div>
244 </div>
245 %endif
245 %endif
246 </div>
246 </div>
247 </li>
247 </li>
248 %endfor
248 %endfor
249 </ul>
249 </ul>
250 <input type="hidden" name="__end__" value="review_members:sequence">
250 <input type="hidden" name="__end__" value="review_members:sequence">
251 %if not c.pull_request.is_closed():
251 %if not c.pull_request.is_closed():
252 <div id="add_reviewer_input" class='ac' style="display: none;">
252 <div id="add_reviewer_input" class='ac' style="display: none;">
253 %if c.allowed_to_update:
253 %if c.allowed_to_update:
254 <div class="reviewer_ac">
254 <div class="reviewer_ac">
255 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
255 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
256 <div id="reviewers_container"></div>
256 <div id="reviewers_container"></div>
257 </div>
257 </div>
258 <div>
258 <div>
259 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
259 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
260 </div>
260 </div>
261 %endif
261 %endif
262 </div>
262 </div>
263 %endif
263 %endif
264 </div>
264 </div>
265 </div>
265 </div>
266 </div>
266 </div>
267 <div class="box">
267 <div class="box">
268 ##DIFF
268 ##DIFF
269 <div class="table" >
269 <div class="table" >
270 <div id="changeset_compare_view_content">
270 <div id="changeset_compare_view_content">
271 ##CS
271 ##CS
272 % if c.missing_requirements:
272 % if c.missing_requirements:
273 <div class="box">
273 <div class="box">
274 <div class="alert alert-warning">
274 <div class="alert alert-warning">
275 <div>
275 <div>
276 <strong>${_('Missing requirements:')}</strong>
276 <strong>${_('Missing requirements:')}</strong>
277 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
277 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
278 </div>
278 </div>
279 </div>
279 </div>
280 </div>
280 </div>
281 % elif c.missing_commits:
281 % elif c.missing_commits:
282 <div class="box">
282 <div class="box">
283 <div class="alert alert-warning">
283 <div class="alert alert-warning">
284 <div>
284 <div>
285 <strong>${_('Missing commits')}:</strong>
285 <strong>${_('Missing commits')}:</strong>
286 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
286 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
287 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
287 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
288 </div>
288 </div>
289 </div>
289 </div>
290 </div>
290 </div>
291 % endif
291 % endif
292 <div class="compare_view_commits_title">
292 <div class="compare_view_commits_title">
293 % if c.allowed_to_update and not c.pull_request.is_closed():
293 % if c.allowed_to_update and not c.pull_request.is_closed():
294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
295 % endif
295 % endif
296 % if len(c.commit_ranges):
296 % if len(c.commit_ranges):
297 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
297 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
298 % endif
298 % endif
299 </div>
299 </div>
300 % if not c.missing_commits:
300 % if not c.missing_commits:
301 <%include file="/compare/compare_commits.html" />
301 <%include file="/compare/compare_commits.html" />
302 ## FILES
302 <div class="cs_files">
303 <div class="cs_files_title">
303 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
304 <span class="cs_files_expand">
304 ${cbdiffs.render_diffset_menu()}
305 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
305 ${cbdiffs.render_diffset(
306 </span>
306 c.diffset, use_comments=True,
307 <h2>
307 collapse_when_files_over=30,
308 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
308 disable_new_comments=c.pull_request.is_closed())}
309 </h2>
309
310 </div>
310 </div>
311 % endif
311 % endif
312 <div class="cs_files">
313 %if not c.files and not c.missing_commits:
314 <span class="empty_data">${_('No files')}</span>
315 %endif
316 <table class="compare_view_files">
317 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
318 %for FID, change, path, stats in c.files:
319 <tr class="cs_${change} collapse_file" fid="${FID}">
320 <td class="cs_icon_td">
321 <span class="collapse_file_icon" fid="${FID}"></span>
322 </td>
323 <td class="cs_icon_td">
324 <div class="flag_status not_reviewed hidden"></div>
325 </td>
326 <td class="cs_${change}" id="a_${FID}">
327 <div class="node">
328 <a href="#a_${FID}">
329 <i class="icon-file-${change.lower()}"></i>
330 ${h.safe_unicode(path)}
331 </a>
332 </div>
312 </div>
333 </td>
334 <td>
335 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
336 <div class="comment-bubble pull-right" data-path="${path}">
337 <i class="icon-comment"></i>
338 </div>
339 </td>
340 </tr>
341 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
342 <td></td>
343 <td></td>
344 <td class="cs_${change}">
345 %if c.target_repo.repo_name == c.repo_name:
346 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
347 %else:
348 ## this is slightly different case later, since the other repo can have this
349 ## file in other state than the origin repo
350 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
351 %endif
352 </td>
353 <td class="td-actions rc-form">
354 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
355 <span class="comments-show">${_('Show comments')}</span>
356 <span class="comments-hide">${_('Hide comments')}</span>
357 </div>
358 </td>
359 </tr>
360 <tr id="tr_${FID}">
361 <td></td>
362 <td></td>
363 <td class="injected_diff" colspan="2">
364 ${diff_block.diff_block_simple([c.changes[FID]])}
365 </td>
366 </tr>
367
368 ## Loop through inline comments
369 % if c.outdated_comments.get(path,False):
370 <tr class="outdated">
371 <td></td>
372 <td></td>
373 <td colspan="2">
374 <p>${_('Outdated Inline Comments')}:</p>
375 </td>
376 </tr>
377 <tr class="outdated">
378 <td></td>
379 <td></td>
380 <td colspan="2" class="outdated_comment_block">
381 % for line, comments in c.outdated_comments[path].iteritems():
382 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
383 % for co in comments:
384 ${comment.comment_block_outdated(co)}
385 % endfor
386 </div>
387 % endfor
388 </td>
389 </tr>
390 % endif
391 %endfor
392 ## Loop through inline comments for deleted files
393 %for path in c.deleted_files:
394 <tr class="outdated deleted">
395 <td></td>
396 <td></td>
397 <td>${path}</td>
398 </tr>
399 <tr class="outdated deleted">
400 <td></td>
401 <td></td>
402 <td>(${_('Removed')})</td>
403 </tr>
404 % if path in c.outdated_comments:
405 <tr class="outdated deleted">
406 <td></td>
407 <td></td>
408 <td colspan="2">
409 <p>${_('Outdated Inline Comments')}:</p>
410 </td>
411 </tr>
412 <tr class="outdated">
413 <td></td>
414 <td></td>
415 <td colspan="2" class="outdated_comment_block">
416 % for line, comments in c.outdated_comments[path].iteritems():
417 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
418 % for co in comments:
419 ${comment.comment_block_outdated(co)}
420 % endfor
421 </div>
422 % endfor
423 </td>
424 </tr>
425 % endif
426 %endfor
427 </table>
428 </div>
429 % if c.limited_diff:
430 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
431 % endif
432 </div>
433 </div>
434
435 % if c.limited_diff:
436 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
437 % endif
438
313
439 ## template for inline comment form
314 ## template for inline comment form
440 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
315 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
441 ${comment.comment_inline_form()}
316 ${comment.comment_inline_form()}
442
317
443 ## render comments and inlines
318 ## render comments and inlines
444 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
319 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
445
320
446 % if not c.pull_request.is_closed():
321 % if not c.pull_request.is_closed():
447 ## main comment form and it status
322 ## main comment form and it status
448 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
323 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
449 pull_request_id=c.pull_request.pull_request_id),
324 pull_request_id=c.pull_request.pull_request_id),
450 c.pull_request_review_status,
325 c.pull_request_review_status,
451 is_pull_request=True, change_status=c.allowed_to_change_status)}
326 is_pull_request=True, change_status=c.allowed_to_change_status)}
452 %endif
327 %endif
453
328
454 <script type="text/javascript">
329 <script type="text/javascript">
455 if (location.hash) {
330 if (location.hash) {
456 var result = splitDelimitedHash(location.hash);
331 var result = splitDelimitedHash(location.hash);
457 var line = $('html').find(result.loc);
332 var line = $('html').find(result.loc);
458 if (line.length > 0){
333 if (line.length > 0){
459 offsetScroll(line, 70);
334 offsetScroll(line, 70);
460 }
335 }
461 }
336 }
462 $(function(){
337 $(function(){
463 ReviewerAutoComplete('user');
338 ReviewerAutoComplete('user');
464 // custom code mirror
339 // custom code mirror
465 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
340 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
466
341
467 var PRDetails = {
342 var PRDetails = {
468 editButton: $('#open_edit_pullrequest'),
343 editButton: $('#open_edit_pullrequest'),
469 closeButton: $('#close_edit_pullrequest'),
344 closeButton: $('#close_edit_pullrequest'),
470 deleteButton: $('#delete_pullrequest'),
345 deleteButton: $('#delete_pullrequest'),
471 viewFields: $('#pr-desc, #pr-title'),
346 viewFields: $('#pr-desc, #pr-title'),
472 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
347 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
473
348
474 init: function() {
349 init: function() {
475 var that = this;
350 var that = this;
476 this.editButton.on('click', function(e) { that.edit(); });
351 this.editButton.on('click', function(e) { that.edit(); });
477 this.closeButton.on('click', function(e) { that.view(); });
352 this.closeButton.on('click', function(e) { that.view(); });
478 },
353 },
479
354
480 edit: function(event) {
355 edit: function(event) {
481 this.viewFields.hide();
356 this.viewFields.hide();
482 this.editButton.hide();
357 this.editButton.hide();
483 this.deleteButton.hide();
358 this.deleteButton.hide();
484 this.closeButton.show();
359 this.closeButton.show();
485 this.editFields.show();
360 this.editFields.show();
486 codeMirrorInstance.refresh();
361 codeMirrorInstance.refresh();
487 },
362 },
488
363
489 view: function(event) {
364 view: function(event) {
490 this.editButton.show();
365 this.editButton.show();
491 this.deleteButton.show();
366 this.deleteButton.show();
492 this.editFields.hide();
367 this.editFields.hide();
493 this.closeButton.hide();
368 this.closeButton.hide();
494 this.viewFields.show();
369 this.viewFields.show();
495 }
370 }
496 };
371 };
497
372
498 var ReviewersPanel = {
373 var ReviewersPanel = {
499 editButton: $('#open_edit_reviewers'),
374 editButton: $('#open_edit_reviewers'),
500 closeButton: $('#close_edit_reviewers'),
375 closeButton: $('#close_edit_reviewers'),
501 addButton: $('#add_reviewer_input'),
376 addButton: $('#add_reviewer_input'),
502 removeButtons: $('.reviewer_member_remove'),
377 removeButtons: $('.reviewer_member_remove'),
503
378
504 init: function() {
379 init: function() {
505 var that = this;
380 var that = this;
506 this.editButton.on('click', function(e) { that.edit(); });
381 this.editButton.on('click', function(e) { that.edit(); });
507 this.closeButton.on('click', function(e) { that.close(); });
382 this.closeButton.on('click', function(e) { that.close(); });
508 },
383 },
509
384
510 edit: function(event) {
385 edit: function(event) {
511 this.editButton.hide();
386 this.editButton.hide();
512 this.closeButton.show();
387 this.closeButton.show();
513 this.addButton.show();
388 this.addButton.show();
514 this.removeButtons.css('visibility', 'visible');
389 this.removeButtons.css('visibility', 'visible');
515 },
390 },
516
391
517 close: function(event) {
392 close: function(event) {
518 this.editButton.show();
393 this.editButton.show();
519 this.closeButton.hide();
394 this.closeButton.hide();
520 this.addButton.hide();
395 this.addButton.hide();
521 this.removeButtons.css('visibility', 'hidden');
396 this.removeButtons.css('visibility', 'hidden');
522 },
397 },
523 };
398 };
524
399
525 PRDetails.init();
400 PRDetails.init();
526 ReviewersPanel.init();
401 ReviewersPanel.init();
527
402
528 $('#show-outdated-comments').on('click', function(e){
403 $('#show-outdated-comments').on('click', function(e){
529 var button = $(this);
404 var button = $(this);
530 var outdated = $('.outdated');
405 var outdated = $('.outdated');
531 if (button.html() === "(Show)") {
406 if (button.html() === "(Show)") {
532 button.html("(Hide)");
407 button.html("(Hide)");
533 outdated.show();
408 outdated.show();
534 } else {
409 } else {
535 button.html("(Show)");
410 button.html("(Show)");
536 outdated.hide();
411 outdated.hide();
537 }
412 }
538 });
413 });
539
414
540 $('.show-inline-comments').on('change', function(e){
415 $('.show-inline-comments').on('change', function(e){
541 var show = 'none';
416 var show = 'none';
542 var target = e.currentTarget;
417 var target = e.currentTarget;
543 if(target.checked){
418 if(target.checked){
544 show = ''
419 show = ''
545 }
420 }
546 var boxid = $(target).attr('id_for');
421 var boxid = $(target).attr('id_for');
547 var comments = $('#{0} .inline-comments'.format(boxid));
422 var comments = $('#{0} .inline-comments'.format(boxid));
548 var fn_display = function(idx){
423 var fn_display = function(idx){
549 $(this).css('display', show);
424 $(this).css('display', show);
550 };
425 };
551 $(comments).each(fn_display);
426 $(comments).each(fn_display);
552 var btns = $('#{0} .inline-comments-button'.format(boxid));
427 var btns = $('#{0} .inline-comments-button'.format(boxid));
553 $(btns).each(fn_display);
428 $(btns).each(fn_display);
554 });
429 });
555
430
556 // inject comments into their proper positions
431 // inject comments into their proper positions
557 var file_comments = $('.inline-comment-placeholder');
432 var file_comments = $('.inline-comment-placeholder');
558 %if c.pull_request.is_closed():
433 %if c.pull_request.is_closed():
559 renderInlineComments(file_comments, false);
434 renderInlineComments(file_comments, false);
560 %else:
435 %else:
561 renderInlineComments(file_comments, true);
436 renderInlineComments(file_comments, true);
562 %endif
437 %endif
563 var commentTotals = {};
438 var commentTotals = {};
564 $.each(file_comments, function(i, comment) {
439 $.each(file_comments, function(i, comment) {
565 var path = $(comment).attr('path');
440 var path = $(comment).attr('path');
566 var comms = $(comment).children().length;
441 var comms = $(comment).children().length;
567 if (path in commentTotals) {
442 if (path in commentTotals) {
568 commentTotals[path] += comms;
443 commentTotals[path] += comms;
569 } else {
444 } else {
570 commentTotals[path] = comms;
445 commentTotals[path] = comms;
571 }
446 }
572 });
447 });
573 $.each(commentTotals, function(path, total) {
448 $.each(commentTotals, function(path, total) {
574 var elem = $('.comment-bubble[data-path="'+ path +'"]');
449 var elem = $('.comment-bubble[data-path="'+ path +'"]');
575 elem.css('visibility', 'visible');
450 elem.css('visibility', 'visible');
576 elem.html(elem.html() + ' ' + total );
451 elem.html(elem.html() + ' ' + total );
577 });
452 });
578
453
579 $('#merge_pull_request_form').submit(function() {
454 $('#merge_pull_request_form').submit(function() {
580 if (!$('#merge_pull_request').attr('disabled')) {
455 if (!$('#merge_pull_request').attr('disabled')) {
581 $('#merge_pull_request').attr('disabled', 'disabled');
456 $('#merge_pull_request').attr('disabled', 'disabled');
582 }
457 }
583 return true;
458 return true;
584 });
459 });
585
460
586 $('#edit_pull_request').on('click', function(e){
461 $('#edit_pull_request').on('click', function(e){
587 var title = $('#pr-title-input').val();
462 var title = $('#pr-title-input').val();
588 var description = codeMirrorInstance.getValue();
463 var description = codeMirrorInstance.getValue();
589 editPullRequest(
464 editPullRequest(
590 "${c.repo_name}", "${c.pull_request.pull_request_id}",
465 "${c.repo_name}", "${c.pull_request.pull_request_id}",
591 title, description);
466 title, description);
592 });
467 });
593
468
594 $('#update_pull_request').on('click', function(e){
469 $('#update_pull_request').on('click', function(e){
595 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
470 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
596 });
471 });
597
472
598 $('#update_commits').on('click', function(e){
473 $('#update_commits').on('click', function(e){
599 var isDisabled = !$(e.currentTarget).attr('disabled');
474 var isDisabled = !$(e.currentTarget).attr('disabled');
600 $(e.currentTarget).text(_gettext('Updating...'));
475 $(e.currentTarget).text(_gettext('Updating...'));
601 $(e.currentTarget).attr('disabled', 'disabled');
476 $(e.currentTarget).attr('disabled', 'disabled');
602 if(isDisabled){
477 if(isDisabled){
603 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
478 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
604 }
479 }
605
480
606 });
481 });
607 // fixing issue with caches on firefox
482 // fixing issue with caches on firefox
608 $('#update_commits').removeAttr("disabled");
483 $('#update_commits').removeAttr("disabled");
609
484
610 $('#close_pull_request').on('click', function(e){
485 $('#close_pull_request').on('click', function(e){
611 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
486 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
612 });
487 });
613
488
614 $('.show-inline-comments').on('click', function(e){
489 $('.show-inline-comments').on('click', function(e){
615 var boxid = $(this).attr('data-comment-id');
490 var boxid = $(this).attr('data-comment-id');
616 var button = $(this);
491 var button = $(this);
617
492
618 if(button.hasClass("comments-visible")) {
493 if(button.hasClass("comments-visible")) {
619 $('#{0} .inline-comments'.format(boxid)).each(function(index){
494 $('#{0} .inline-comments'.format(boxid)).each(function(index){
620 $(this).hide();
495 $(this).hide();
621 });
496 });
622 button.removeClass("comments-visible");
497 button.removeClass("comments-visible");
623 } else {
498 } else {
624 $('#{0} .inline-comments'.format(boxid)).each(function(index){
499 $('#{0} .inline-comments'.format(boxid)).each(function(index){
625 $(this).show();
500 $(this).show();
626 });
501 });
627 button.addClass("comments-visible");
502 button.addClass("comments-visible");
628 }
503 }
629 });
504 });
630 })
505 })
631 </script>
506 </script>
632
507
633 </div>
508 </div>
634 </div>
509 </div>
635
510
636 </%def>
511 </%def>
General Comments 0
You need to be logged in to leave comments. Login now