##// END OF EJS Templates
diffs: add comments to changeset diffs
dan -
r1143:7bd159d9 default
parent child Browse files
Show More
@@ -1,470 +1,465 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)
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
160 # fetch global flags of ignore ws or context lines
161 context_lcl = get_line_ctx('', request.GET)
162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163
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
166 diff_limit = self.cut_off_limit_diff
167 file_limit = self.cut_off_limit_file
168
159 # get ranges of commit ids if preset
169 # get ranges of commit ids if preset
160 commit_range = commit_id_range.split('...')[:2]
170 commit_range = commit_id_range.split('...')[:2]
161 enable_comments = True
171
162 try:
172 try:
163 pre_load = ['affected_files', 'author', 'branch', 'date',
173 pre_load = ['affected_files', 'author', 'branch', 'date',
164 'message', 'parents']
174 'message', 'parents']
165
175
166 if len(commit_range) == 2:
176 if len(commit_range) == 2:
167 enable_comments = False
168 commits = c.rhodecode_repo.get_commits(
177 commits = c.rhodecode_repo.get_commits(
169 start_id=commit_range[0], end_id=commit_range[1],
178 start_id=commit_range[0], end_id=commit_range[1],
170 pre_load=pre_load)
179 pre_load=pre_load)
171 commits = list(commits)
180 commits = list(commits)
172 else:
181 else:
173 commits = [c.rhodecode_repo.get_commit(
182 commits = [c.rhodecode_repo.get_commit(
174 commit_id=commit_id_range, pre_load=pre_load)]
183 commit_id=commit_id_range, pre_load=pre_load)]
175
184
176 c.commit_ranges = commits
185 c.commit_ranges = commits
177 if not c.commit_ranges:
186 if not c.commit_ranges:
178 raise RepositoryError(
187 raise RepositoryError(
179 'The commit range returned an empty result')
188 'The commit range returned an empty result')
180 except CommitDoesNotExistError:
189 except CommitDoesNotExistError:
181 msg = _('No such commit exists for this repository')
190 msg = _('No such commit exists for this repository')
182 h.flash(msg, category='error')
191 h.flash(msg, category='error')
183 raise HTTPNotFound()
192 raise HTTPNotFound()
184 except Exception:
193 except Exception:
185 log.exception("General failure")
194 log.exception("General failure")
186 raise HTTPNotFound()
195 raise HTTPNotFound()
187
196
188 c.changes = OrderedDict()
197 c.changes = OrderedDict()
189 c.lines_added = 0
198 c.lines_added = 0
190 c.lines_deleted = 0
199 c.lines_deleted = 0
191
200
192 c.commit_statuses = ChangesetStatus.STATUSES
201 c.commit_statuses = ChangesetStatus.STATUSES
193 c.comments = []
194 c.statuses = []
195 c.inline_comments = []
202 c.inline_comments = []
196 c.inline_cnt = 0
203 c.inline_cnt = 0
197 c.files = []
204 c.files = []
198
205
206 c.statuses = []
207 c.comments = []
208 if len(c.commit_ranges) == 1:
209 commit = c.commit_ranges[0]
210 c.comments = ChangesetCommentsModel().get_comments(
211 c.rhodecode_db_repo.repo_id,
212 revision=commit.raw_id)
213 c.statuses.append(ChangesetStatusModel().get_status(
214 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 # comments from PR
216 statuses = ChangesetStatusModel().get_statuses(
217 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 with_revisions=True)
219 prs = set(st.pull_request for st in statuses
220 if st is st.pull_request is not None)
221
222 # from associated statuses, check the pull requests, and
223 # show comments from them
224 for pr in prs:
225 c.comments.extend(pr.comments)
226
199 # Iterate over ranges (default commit view is always one commit)
227 # Iterate over ranges (default commit view is always one commit)
200 for commit in c.commit_ranges:
228 for commit in c.commit_ranges:
201 if method == 'show':
202 c.statuses.extend([ChangesetStatusModel().get_status(
203 c.rhodecode_db_repo.repo_id, commit.raw_id)])
204
205 c.comments.extend(ChangesetCommentsModel().get_comments(
206 c.rhodecode_db_repo.repo_id,
207 revision=commit.raw_id))
208
209 # comments from PR
210 st = ChangesetStatusModel().get_statuses(
211 c.rhodecode_db_repo.repo_id, commit.raw_id,
212 with_revisions=True)
213
214 # from associated statuses, check the pull requests, and
215 # show comments from them
216
217 prs = set(x.pull_request for x in
218 filter(lambda x: x.pull_request is not None, st))
219 for pr in prs:
220 c.comments.extend(pr.comments)
221
222 inlines = ChangesetCommentsModel().get_inline_comments(
223 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
224 c.inline_comments.extend(inlines.iteritems())
225
226 c.changes[commit.raw_id] = []
229 c.changes[commit.raw_id] = []
227
230
228 commit2 = commit
231 commit2 = commit
229 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
232 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
230
233
231 # fetch global flags of ignore ws or context lines
232 context_lcl = get_line_ctx('', request.GET)
233 ign_whitespace_lcl = get_ignore_ws('', request.GET)
234
235 _diff = c.rhodecode_repo.get_diff(
234 _diff = c.rhodecode_repo.get_diff(
236 commit1, commit2,
235 commit1, commit2,
237 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
236 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
238
239 # diff_limit will cut off the whole diff if the limit is applied
240 # otherwise it will just hide the big files from the front-end
241 diff_limit = self.cut_off_limit_diff
242 file_limit = self.cut_off_limit_file
243
244 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
245 _diff, format='newdiff', diff_limit=diff_limit,
238 _diff, format='newdiff', diff_limit=diff_limit,
246 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
240
247 commit_changes = OrderedDict()
241 commit_changes = OrderedDict()
248 if method == 'show':
242 if method == 'show':
249 _parsed = diff_processor.prepare()
243 _parsed = diff_processor.prepare()
250 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
244 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
251
245
252 _parsed = diff_processor.prepare()
246 _parsed = diff_processor.prepare()
253
247
254 def _node_getter(commit):
248 def _node_getter(commit):
255 def get_node(fname):
249 def get_node(fname):
256 try:
250 try:
257 return commit.get_node(fname)
251 return commit.get_node(fname)
258 except NodeDoesNotExistError:
252 except NodeDoesNotExistError:
259 return None
253 return None
260 return get_node
254 return get_node
261
255
256 inline_comments = ChangesetCommentsModel().get_inline_comments(
257 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
258 c.inline_cnt += len(inline_comments)
259
262 diffset = codeblocks.DiffSet(
260 diffset = codeblocks.DiffSet(
263 repo_name=c.repo_name,
261 repo_name=c.repo_name,
264 source_node_getter=_node_getter(commit1),
262 source_node_getter=_node_getter(commit1),
265 target_node_getter=_node_getter(commit2),
263 target_node_getter=_node_getter(commit2),
264 comments=inline_comments
266 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
265 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
267 c.changes[commit.raw_id] = diffset
266 c.changes[commit.raw_id] = diffset
268 else:
267 else:
269 # downloads/raw we only need RAW diff nothing else
268 # downloads/raw we only need RAW diff nothing else
270 diff = diff_processor.as_raw()
269 diff = diff_processor.as_raw()
271 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
270 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
272
271
273 # sort comments by how they were generated
272 # sort comments by how they were generated
274 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
273 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
275
274
276 # count inline comments
277 for __, lines in c.inline_comments:
278 for comments in lines.values():
279 c.inline_cnt += len(comments)
280
275
281 if len(c.commit_ranges) == 1:
276 if len(c.commit_ranges) == 1:
282 c.commit = c.commit_ranges[0]
277 c.commit = c.commit_ranges[0]
283 c.parent_tmpl = ''.join(
278 c.parent_tmpl = ''.join(
284 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
285 if method == 'download':
280 if method == 'download':
286 response.content_type = 'text/plain'
281 response.content_type = 'text/plain'
287 response.content_disposition = (
282 response.content_disposition = (
288 'attachment; filename=%s.diff' % commit_id_range[:12])
283 'attachment; filename=%s.diff' % commit_id_range[:12])
289 return diff
284 return diff
290 elif method == 'patch':
285 elif method == 'patch':
291 response.content_type = 'text/plain'
286 response.content_type = 'text/plain'
292 c.diff = safe_unicode(diff)
287 c.diff = safe_unicode(diff)
293 return render('changeset/patch_changeset.html')
288 return render('changeset/patch_changeset.html')
294 elif method == 'raw':
289 elif method == 'raw':
295 response.content_type = 'text/plain'
290 response.content_type = 'text/plain'
296 return diff
291 return diff
297 elif method == 'show':
292 elif method == 'show':
298 if len(c.commit_ranges) == 1:
293 if len(c.commit_ranges) == 1:
299 return render('changeset/changeset.html')
294 return render('changeset/changeset.html')
300 else:
295 else:
301 c.ancestor = None
296 c.ancestor = None
302 c.target_repo = c.rhodecode_db_repo
297 c.target_repo = c.rhodecode_db_repo
303 return render('changeset/changeset_range.html')
298 return render('changeset/changeset_range.html')
304
299
305 @LoginRequired()
300 @LoginRequired()
306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 'repository.admin')
302 'repository.admin')
308 def index(self, revision, method='show'):
303 def index(self, revision, method='show'):
309 return self._index(revision, method=method)
304 return self._index(revision, method=method)
310
305
311 @LoginRequired()
306 @LoginRequired()
312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 'repository.admin')
308 'repository.admin')
314 def changeset_raw(self, revision):
309 def changeset_raw(self, revision):
315 return self._index(revision, method='raw')
310 return self._index(revision, method='raw')
316
311
317 @LoginRequired()
312 @LoginRequired()
318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 'repository.admin')
314 'repository.admin')
320 def changeset_patch(self, revision):
315 def changeset_patch(self, revision):
321 return self._index(revision, method='patch')
316 return self._index(revision, method='patch')
322
317
323 @LoginRequired()
318 @LoginRequired()
324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 'repository.admin')
320 'repository.admin')
326 def changeset_download(self, revision):
321 def changeset_download(self, revision):
327 return self._index(revision, method='download')
322 return self._index(revision, method='download')
328
323
329 @LoginRequired()
324 @LoginRequired()
330 @NotAnonymous()
325 @NotAnonymous()
331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
332 'repository.admin')
327 'repository.admin')
333 @auth.CSRFRequired()
328 @auth.CSRFRequired()
334 @jsonify
329 @jsonify
335 def comment(self, repo_name, revision):
330 def comment(self, repo_name, revision):
336 commit_id = revision
331 commit_id = revision
337 status = request.POST.get('changeset_status', None)
332 status = request.POST.get('changeset_status', None)
338 text = request.POST.get('text')
333 text = request.POST.get('text')
339 if status:
334 if status:
340 text = text or (_('Status change %(transition_icon)s %(status)s')
335 text = text or (_('Status change %(transition_icon)s %(status)s')
341 % {'transition_icon': '>',
336 % {'transition_icon': '>',
342 'status': ChangesetStatus.get_status_lbl(status)})
337 'status': ChangesetStatus.get_status_lbl(status)})
343
338
344 multi_commit_ids = filter(
339 multi_commit_ids = filter(
345 lambda s: s not in ['', None],
340 lambda s: s not in ['', None],
346 request.POST.get('commit_ids', '').split(','),)
341 request.POST.get('commit_ids', '').split(','),)
347
342
348 commit_ids = multi_commit_ids or [commit_id]
343 commit_ids = multi_commit_ids or [commit_id]
349 comment = None
344 comment = None
350 for current_id in filter(None, commit_ids):
345 for current_id in filter(None, commit_ids):
351 c.co = comment = ChangesetCommentsModel().create(
346 c.co = comment = ChangesetCommentsModel().create(
352 text=text,
347 text=text,
353 repo=c.rhodecode_db_repo.repo_id,
348 repo=c.rhodecode_db_repo.repo_id,
354 user=c.rhodecode_user.user_id,
349 user=c.rhodecode_user.user_id,
355 revision=current_id,
350 revision=current_id,
356 f_path=request.POST.get('f_path'),
351 f_path=request.POST.get('f_path'),
357 line_no=request.POST.get('line'),
352 line_no=request.POST.get('line'),
358 status_change=(ChangesetStatus.get_status_lbl(status)
353 status_change=(ChangesetStatus.get_status_lbl(status)
359 if status else None),
354 if status else None),
360 status_change_type=status
355 status_change_type=status
361 )
356 )
362 # get status if set !
357 # get status if set !
363 if status:
358 if status:
364 # if latest status was from pull request and it's closed
359 # if latest status was from pull request and it's closed
365 # disallow changing status !
360 # disallow changing status !
366 # dont_allow_on_closed_pull_request = True !
361 # dont_allow_on_closed_pull_request = True !
367
362
368 try:
363 try:
369 ChangesetStatusModel().set_status(
364 ChangesetStatusModel().set_status(
370 c.rhodecode_db_repo.repo_id,
365 c.rhodecode_db_repo.repo_id,
371 status,
366 status,
372 c.rhodecode_user.user_id,
367 c.rhodecode_user.user_id,
373 comment,
368 comment,
374 revision=current_id,
369 revision=current_id,
375 dont_allow_on_closed_pull_request=True
370 dont_allow_on_closed_pull_request=True
376 )
371 )
377 except StatusChangeOnClosedPullRequestError:
372 except StatusChangeOnClosedPullRequestError:
378 msg = _('Changing the status of a commit associated with '
373 msg = _('Changing the status of a commit associated with '
379 'a closed pull request is not allowed')
374 'a closed pull request is not allowed')
380 log.exception(msg)
375 log.exception(msg)
381 h.flash(msg, category='warning')
376 h.flash(msg, category='warning')
382 return redirect(h.url(
377 return redirect(h.url(
383 'changeset_home', repo_name=repo_name,
378 'changeset_home', repo_name=repo_name,
384 revision=current_id))
379 revision=current_id))
385
380
386 # finalize, commit and redirect
381 # finalize, commit and redirect
387 Session().commit()
382 Session().commit()
388
383
389 data = {
384 data = {
390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 }
386 }
392 if comment:
387 if comment:
393 data.update(comment.get_dict())
388 data.update(comment.get_dict())
394 data.update({'rendered_text':
389 data.update({'rendered_text':
395 render('changeset/changeset_comment_block.html')})
390 render('changeset/changeset_comment_block.html')})
396
391
397 return data
392 return data
398
393
399 @LoginRequired()
394 @LoginRequired()
400 @NotAnonymous()
395 @NotAnonymous()
401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 'repository.admin')
397 'repository.admin')
403 @auth.CSRFRequired()
398 @auth.CSRFRequired()
404 def preview_comment(self):
399 def preview_comment(self):
405 # Technically a CSRF token is not needed as no state changes with this
400 # Technically a CSRF token is not needed as no state changes with this
406 # call. However, as this is a POST is better to have it, so automated
401 # call. However, as this is a POST is better to have it, so automated
407 # tools don't flag it as potential CSRF.
402 # tools don't flag it as potential CSRF.
408 # Post is required because the payload could be bigger than the maximum
403 # Post is required because the payload could be bigger than the maximum
409 # allowed by GET.
404 # allowed by GET.
410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
411 raise HTTPBadRequest()
406 raise HTTPBadRequest()
412 text = request.POST.get('text')
407 text = request.POST.get('text')
413 renderer = request.POST.get('renderer') or 'rst'
408 renderer = request.POST.get('renderer') or 'rst'
414 if text:
409 if text:
415 return h.render(text, renderer=renderer, mentions=True)
410 return h.render(text, renderer=renderer, mentions=True)
416 return ''
411 return ''
417
412
418 @LoginRequired()
413 @LoginRequired()
419 @NotAnonymous()
414 @NotAnonymous()
420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
421 'repository.admin')
416 'repository.admin')
422 @auth.CSRFRequired()
417 @auth.CSRFRequired()
423 @jsonify
418 @jsonify
424 def delete_comment(self, repo_name, comment_id):
419 def delete_comment(self, repo_name, comment_id):
425 comment = ChangesetComment.get(comment_id)
420 comment = ChangesetComment.get(comment_id)
426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 owner = (comment.author.user_id == c.rhodecode_user.user_id)
427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
429 ChangesetCommentsModel().delete(comment=comment)
424 ChangesetCommentsModel().delete(comment=comment)
430 Session().commit()
425 Session().commit()
431 return True
426 return True
432 else:
427 else:
433 raise HTTPForbidden()
428 raise HTTPForbidden()
434
429
435 @LoginRequired()
430 @LoginRequired()
436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 'repository.admin')
432 'repository.admin')
438 @jsonify
433 @jsonify
439 def changeset_info(self, repo_name, revision):
434 def changeset_info(self, repo_name, revision):
440 if request.is_xhr:
435 if request.is_xhr:
441 try:
436 try:
442 return c.rhodecode_repo.get_commit(commit_id=revision)
437 return c.rhodecode_repo.get_commit(commit_id=revision)
443 except CommitDoesNotExistError as e:
438 except CommitDoesNotExistError as e:
444 return EmptyCommit(message=str(e))
439 return EmptyCommit(message=str(e))
445 else:
440 else:
446 raise HTTPBadRequest()
441 raise HTTPBadRequest()
447
442
448 @LoginRequired()
443 @LoginRequired()
449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 'repository.admin')
445 'repository.admin')
451 @jsonify
446 @jsonify
452 def changeset_children(self, repo_name, revision):
447 def changeset_children(self, repo_name, revision):
453 if request.is_xhr:
448 if request.is_xhr:
454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 commit = c.rhodecode_repo.get_commit(commit_id=revision)
455 result = {"results": commit.children}
450 result = {"results": commit.children}
456 return result
451 return result
457 else:
452 else:
458 raise HTTPBadRequest()
453 raise HTTPBadRequest()
459
454
460 @LoginRequired()
455 @LoginRequired()
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
462 'repository.admin')
457 'repository.admin')
463 @jsonify
458 @jsonify
464 def changeset_parents(self, repo_name, revision):
459 def changeset_parents(self, repo_name, revision):
465 if request.is_xhr:
460 if request.is_xhr:
466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 commit = c.rhodecode_repo.get_commit(commit_id=revision)
467 result = {"results": commit.parents}
462 result = {"results": commit.parents}
468 return result
463 return result
469 else:
464 else:
470 raise HTTPBadRequest()
465 raise HTTPBadRequest()
@@ -1,642 +1,665 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-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 import logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from rhodecode.lib.helpers import (
27 from rhodecode.lib.helpers import (
28 get_lexer_for_filenode, get_lexer_safe, html_escape)
28 get_lexer_for_filenode, get_lexer_safe, html_escape)
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.lib.vcs.nodes import FileNode
30 from rhodecode.lib.vcs.nodes import FileNode
31 from rhodecode.lib.diff_match_patch import diff_match_patch
31 from rhodecode.lib.diff_match_patch import diff_match_patch
32 from rhodecode.lib.diffs import LimitedDiffContainer
32 from rhodecode.lib.diffs import LimitedDiffContainer
33 from pygments.lexers import get_lexer_by_name
33 from pygments.lexers import get_lexer_by_name
34
34
35 plain_text_lexer = get_lexer_by_name(
35 plain_text_lexer = get_lexer_by_name(
36 'text', stripall=False, stripnl=False, ensurenl=False)
36 'text', stripall=False, stripnl=False, ensurenl=False)
37
37
38
38
39 log = logging.getLogger()
39 log = logging.getLogger()
40
40
41
41
42 def filenode_as_lines_tokens(filenode, lexer=None):
42 def filenode_as_lines_tokens(filenode, lexer=None):
43 lexer = lexer or get_lexer_for_filenode(filenode)
43 lexer = lexer or get_lexer_for_filenode(filenode)
44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
44 log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode)
45 tokens = tokenize_string(filenode.content, lexer)
45 tokens = tokenize_string(filenode.content, lexer)
46 lines = split_token_stream(tokens, split_string='\n')
46 lines = split_token_stream(tokens, split_string='\n')
47 rv = list(lines)
47 rv = list(lines)
48 return rv
48 return rv
49
49
50
50
51 def tokenize_string(content, lexer):
51 def tokenize_string(content, lexer):
52 """
52 """
53 Use pygments to tokenize some content based on a lexer
53 Use pygments to tokenize some content based on a lexer
54 ensuring all original new lines and whitespace is preserved
54 ensuring all original new lines and whitespace is preserved
55 """
55 """
56
56
57 lexer.stripall = False
57 lexer.stripall = False
58 lexer.stripnl = False
58 lexer.stripnl = False
59 lexer.ensurenl = False
59 lexer.ensurenl = False
60 for token_type, token_text in lex(content, lexer):
60 for token_type, token_text in lex(content, lexer):
61 yield pygment_token_class(token_type), token_text
61 yield pygment_token_class(token_type), token_text
62
62
63
63
64 def split_token_stream(tokens, split_string=u'\n'):
64 def split_token_stream(tokens, split_string=u'\n'):
65 """
65 """
66 Take a list of (TokenType, text) tuples and split them by a string
66 Take a list of (TokenType, text) tuples and split them by a string
67
67
68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
68 >>> split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
69 [(TEXT, 'some'), (TEXT, 'text'),
69 [(TEXT, 'some'), (TEXT, 'text'),
70 (TEXT, 'more'), (TEXT, 'text')]
70 (TEXT, 'more'), (TEXT, 'text')]
71 """
71 """
72
72
73 buffer = []
73 buffer = []
74 for token_class, token_text in tokens:
74 for token_class, token_text in tokens:
75 parts = token_text.split(split_string)
75 parts = token_text.split(split_string)
76 for part in parts[:-1]:
76 for part in parts[:-1]:
77 buffer.append((token_class, part))
77 buffer.append((token_class, part))
78 yield buffer
78 yield buffer
79 buffer = []
79 buffer = []
80
80
81 buffer.append((token_class, parts[-1]))
81 buffer.append((token_class, parts[-1]))
82
82
83 if buffer:
83 if buffer:
84 yield buffer
84 yield buffer
85
85
86
86
87 def filenode_as_annotated_lines_tokens(filenode):
87 def filenode_as_annotated_lines_tokens(filenode):
88 """
88 """
89 Take a file node and return a list of annotations => lines, if no annotation
89 Take a file node and return a list of annotations => lines, if no annotation
90 is found, it will be None.
90 is found, it will be None.
91
91
92 eg:
92 eg:
93
93
94 [
94 [
95 (annotation1, [
95 (annotation1, [
96 (1, line1_tokens_list),
96 (1, line1_tokens_list),
97 (2, line2_tokens_list),
97 (2, line2_tokens_list),
98 ]),
98 ]),
99 (annotation2, [
99 (annotation2, [
100 (3, line1_tokens_list),
100 (3, line1_tokens_list),
101 ]),
101 ]),
102 (None, [
102 (None, [
103 (4, line1_tokens_list),
103 (4, line1_tokens_list),
104 ]),
104 ]),
105 (annotation1, [
105 (annotation1, [
106 (5, line1_tokens_list),
106 (5, line1_tokens_list),
107 (6, line2_tokens_list),
107 (6, line2_tokens_list),
108 ])
108 ])
109 ]
109 ]
110 """
110 """
111
111
112 commit_cache = {} # cache commit_getter lookups
112 commit_cache = {} # cache commit_getter lookups
113
113
114 def _get_annotation(commit_id, commit_getter):
114 def _get_annotation(commit_id, commit_getter):
115 if commit_id not in commit_cache:
115 if commit_id not in commit_cache:
116 commit_cache[commit_id] = commit_getter()
116 commit_cache[commit_id] = commit_getter()
117 return commit_cache[commit_id]
117 return commit_cache[commit_id]
118
118
119 annotation_lookup = {
119 annotation_lookup = {
120 line_no: _get_annotation(commit_id, commit_getter)
120 line_no: _get_annotation(commit_id, commit_getter)
121 for line_no, commit_id, commit_getter, line_content
121 for line_no, commit_id, commit_getter, line_content
122 in filenode.annotate
122 in filenode.annotate
123 }
123 }
124
124
125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
125 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
126 for line_no, tokens
126 for line_no, tokens
127 in enumerate(filenode_as_lines_tokens(filenode), 1))
127 in enumerate(filenode_as_lines_tokens(filenode), 1))
128
128
129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
129 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
130
130
131 for annotation, group in grouped_annotations_lines:
131 for annotation, group in grouped_annotations_lines:
132 yield (
132 yield (
133 annotation, [(line_no, tokens)
133 annotation, [(line_no, tokens)
134 for (_, line_no, tokens) in group]
134 for (_, line_no, tokens) in group]
135 )
135 )
136
136
137
137
138 def render_tokenstream(tokenstream):
138 def render_tokenstream(tokenstream):
139 result = []
139 result = []
140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
140 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
141
141
142 if token_class:
142 if token_class:
143 result.append(u'<span class="%s">' % token_class)
143 result.append(u'<span class="%s">' % token_class)
144 else:
144 else:
145 result.append(u'<span>')
145 result.append(u'<span>')
146
146
147 for op_tag, token_text in token_ops_texts:
147 for op_tag, token_text in token_ops_texts:
148
148
149 if op_tag:
149 if op_tag:
150 result.append(u'<%s>' % op_tag)
150 result.append(u'<%s>' % op_tag)
151
151
152 escaped_text = html_escape(token_text)
152 escaped_text = html_escape(token_text)
153
153
154 # TODO: dan: investigate showing hidden characters like space/nl/tab
154 # TODO: dan: investigate showing hidden characters like space/nl/tab
155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
155 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
156 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
157 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
158
158
159 result.append(escaped_text)
159 result.append(escaped_text)
160
160
161 if op_tag:
161 if op_tag:
162 result.append(u'</%s>' % op_tag)
162 result.append(u'</%s>' % op_tag)
163
163
164 result.append(u'</span>')
164 result.append(u'</span>')
165
165
166 html = ''.join(result)
166 html = ''.join(result)
167 return html
167 return html
168
168
169
169
170 def rollup_tokenstream(tokenstream):
170 def rollup_tokenstream(tokenstream):
171 """
171 """
172 Group a token stream of the format:
172 Group a token stream of the format:
173
173
174 ('class', 'op', 'text')
174 ('class', 'op', 'text')
175 or
175 or
176 ('class', 'text')
176 ('class', 'text')
177
177
178 into
178 into
179
179
180 [('class1',
180 [('class1',
181 [('op1', 'text'),
181 [('op1', 'text'),
182 ('op2', 'text')]),
182 ('op2', 'text')]),
183 ('class2',
183 ('class2',
184 [('op3', 'text')])]
184 [('op3', 'text')])]
185
185
186 This is used to get the minimal tags necessary when
186 This is used to get the minimal tags necessary when
187 rendering to html eg for a token stream ie.
187 rendering to html eg for a token stream ie.
188
188
189 <span class="A"><ins>he</ins>llo</span>
189 <span class="A"><ins>he</ins>llo</span>
190 vs
190 vs
191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
191 <span class="A"><ins>he</ins></span><span class="A">llo</span>
192
192
193 If a 2 tuple is passed in, the output op will be an empty string.
193 If a 2 tuple is passed in, the output op will be an empty string.
194
194
195 eg:
195 eg:
196
196
197 >>> rollup_tokenstream([('classA', '', 'h'),
197 >>> rollup_tokenstream([('classA', '', 'h'),
198 ('classA', 'del', 'ell'),
198 ('classA', 'del', 'ell'),
199 ('classA', '', 'o'),
199 ('classA', '', 'o'),
200 ('classB', '', ' '),
200 ('classB', '', ' '),
201 ('classA', '', 'the'),
201 ('classA', '', 'the'),
202 ('classA', '', 're'),
202 ('classA', '', 're'),
203 ])
203 ])
204
204
205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
205 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
206 ('classB', [('', ' ')],
206 ('classB', [('', ' ')],
207 ('classA', [('', 'there')]]
207 ('classA', [('', 'there')]]
208
208
209 """
209 """
210 if tokenstream and len(tokenstream[0]) == 2:
210 if tokenstream and len(tokenstream[0]) == 2:
211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
211 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
212
212
213 result = []
213 result = []
214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
214 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
215 ops = []
215 ops = []
216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
216 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
217 text_buffer = []
217 text_buffer = []
218 for t_class, t_op, t_text in token_text_list:
218 for t_class, t_op, t_text in token_text_list:
219 text_buffer.append(t_text)
219 text_buffer.append(t_text)
220 ops.append((token_op, ''.join(text_buffer)))
220 ops.append((token_op, ''.join(text_buffer)))
221 result.append((token_class, ops))
221 result.append((token_class, ops))
222 return result
222 return result
223
223
224
224
225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
225 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
226 """
226 """
227 Converts a list of (token_class, token_text) tuples to a list of
227 Converts a list of (token_class, token_text) tuples to a list of
228 (token_class, token_op, token_text) tuples where token_op is one of
228 (token_class, token_op, token_text) tuples where token_op is one of
229 ('ins', 'del', '')
229 ('ins', 'del', '')
230
230
231 :param old_tokens: list of (token_class, token_text) tuples of old line
231 :param old_tokens: list of (token_class, token_text) tuples of old line
232 :param new_tokens: list of (token_class, token_text) tuples of new line
232 :param new_tokens: list of (token_class, token_text) tuples of new line
233 :param use_diff_match_patch: boolean, will use google's diff match patch
233 :param use_diff_match_patch: boolean, will use google's diff match patch
234 library which has options to 'smooth' out the character by character
234 library which has options to 'smooth' out the character by character
235 differences making nicer ins/del blocks
235 differences making nicer ins/del blocks
236 """
236 """
237
237
238 old_tokens_result = []
238 old_tokens_result = []
239 new_tokens_result = []
239 new_tokens_result = []
240
240
241 similarity = difflib.SequenceMatcher(None,
241 similarity = difflib.SequenceMatcher(None,
242 ''.join(token_text for token_class, token_text in old_tokens),
242 ''.join(token_text for token_class, token_text in old_tokens),
243 ''.join(token_text for token_class, token_text in new_tokens)
243 ''.join(token_text for token_class, token_text in new_tokens)
244 ).ratio()
244 ).ratio()
245
245
246 if similarity < 0.6: # return, the blocks are too different
246 if similarity < 0.6: # return, the blocks are too different
247 for token_class, token_text in old_tokens:
247 for token_class, token_text in old_tokens:
248 old_tokens_result.append((token_class, '', token_text))
248 old_tokens_result.append((token_class, '', token_text))
249 for token_class, token_text in new_tokens:
249 for token_class, token_text in new_tokens:
250 new_tokens_result.append((token_class, '', token_text))
250 new_tokens_result.append((token_class, '', token_text))
251 return old_tokens_result, new_tokens_result, similarity
251 return old_tokens_result, new_tokens_result, similarity
252
252
253 token_sequence_matcher = difflib.SequenceMatcher(None,
253 token_sequence_matcher = difflib.SequenceMatcher(None,
254 [x[1] for x in old_tokens],
254 [x[1] for x in old_tokens],
255 [x[1] for x in new_tokens])
255 [x[1] for x in new_tokens])
256
256
257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
257 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
258 # check the differences by token block types first to give a more
258 # check the differences by token block types first to give a more
259 # nicer "block" level replacement vs character diffs
259 # nicer "block" level replacement vs character diffs
260
260
261 if tag == 'equal':
261 if tag == 'equal':
262 for token_class, token_text in old_tokens[o1:o2]:
262 for token_class, token_text in old_tokens[o1:o2]:
263 old_tokens_result.append((token_class, '', token_text))
263 old_tokens_result.append((token_class, '', token_text))
264 for token_class, token_text in new_tokens[n1:n2]:
264 for token_class, token_text in new_tokens[n1:n2]:
265 new_tokens_result.append((token_class, '', token_text))
265 new_tokens_result.append((token_class, '', token_text))
266 elif tag == 'delete':
266 elif tag == 'delete':
267 for token_class, token_text in old_tokens[o1:o2]:
267 for token_class, token_text in old_tokens[o1:o2]:
268 old_tokens_result.append((token_class, 'del', token_text))
268 old_tokens_result.append((token_class, 'del', token_text))
269 elif tag == 'insert':
269 elif tag == 'insert':
270 for token_class, token_text in new_tokens[n1:n2]:
270 for token_class, token_text in new_tokens[n1:n2]:
271 new_tokens_result.append((token_class, 'ins', token_text))
271 new_tokens_result.append((token_class, 'ins', token_text))
272 elif tag == 'replace':
272 elif tag == 'replace':
273 # if same type token blocks must be replaced, do a diff on the
273 # if same type token blocks must be replaced, do a diff on the
274 # characters in the token blocks to show individual changes
274 # characters in the token blocks to show individual changes
275
275
276 old_char_tokens = []
276 old_char_tokens = []
277 new_char_tokens = []
277 new_char_tokens = []
278 for token_class, token_text in old_tokens[o1:o2]:
278 for token_class, token_text in old_tokens[o1:o2]:
279 for char in token_text:
279 for char in token_text:
280 old_char_tokens.append((token_class, char))
280 old_char_tokens.append((token_class, char))
281
281
282 for token_class, token_text in new_tokens[n1:n2]:
282 for token_class, token_text in new_tokens[n1:n2]:
283 for char in token_text:
283 for char in token_text:
284 new_char_tokens.append((token_class, char))
284 new_char_tokens.append((token_class, char))
285
285
286 old_string = ''.join([token_text for
286 old_string = ''.join([token_text for
287 token_class, token_text in old_char_tokens])
287 token_class, token_text in old_char_tokens])
288 new_string = ''.join([token_text for
288 new_string = ''.join([token_text for
289 token_class, token_text in new_char_tokens])
289 token_class, token_text in new_char_tokens])
290
290
291 char_sequence = difflib.SequenceMatcher(
291 char_sequence = difflib.SequenceMatcher(
292 None, old_string, new_string)
292 None, old_string, new_string)
293 copcodes = char_sequence.get_opcodes()
293 copcodes = char_sequence.get_opcodes()
294 obuffer, nbuffer = [], []
294 obuffer, nbuffer = [], []
295
295
296 if use_diff_match_patch:
296 if use_diff_match_patch:
297 dmp = diff_match_patch()
297 dmp = diff_match_patch()
298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
298 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
299 reps = dmp.diff_main(old_string, new_string)
299 reps = dmp.diff_main(old_string, new_string)
300 dmp.diff_cleanupEfficiency(reps)
300 dmp.diff_cleanupEfficiency(reps)
301
301
302 a, b = 0, 0
302 a, b = 0, 0
303 for op, rep in reps:
303 for op, rep in reps:
304 l = len(rep)
304 l = len(rep)
305 if op == 0:
305 if op == 0:
306 for i, c in enumerate(rep):
306 for i, c in enumerate(rep):
307 obuffer.append((old_char_tokens[a+i][0], '', c))
307 obuffer.append((old_char_tokens[a+i][0], '', c))
308 nbuffer.append((new_char_tokens[b+i][0], '', c))
308 nbuffer.append((new_char_tokens[b+i][0], '', c))
309 a += l
309 a += l
310 b += l
310 b += l
311 elif op == -1:
311 elif op == -1:
312 for i, c in enumerate(rep):
312 for i, c in enumerate(rep):
313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
313 obuffer.append((old_char_tokens[a+i][0], 'del', c))
314 a += l
314 a += l
315 elif op == 1:
315 elif op == 1:
316 for i, c in enumerate(rep):
316 for i, c in enumerate(rep):
317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
317 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
318 b += l
318 b += l
319 else:
319 else:
320 for ctag, co1, co2, cn1, cn2 in copcodes:
320 for ctag, co1, co2, cn1, cn2 in copcodes:
321 if ctag == 'equal':
321 if ctag == 'equal':
322 for token_class, token_text in old_char_tokens[co1:co2]:
322 for token_class, token_text in old_char_tokens[co1:co2]:
323 obuffer.append((token_class, '', token_text))
323 obuffer.append((token_class, '', token_text))
324 for token_class, token_text in new_char_tokens[cn1:cn2]:
324 for token_class, token_text in new_char_tokens[cn1:cn2]:
325 nbuffer.append((token_class, '', token_text))
325 nbuffer.append((token_class, '', token_text))
326 elif ctag == 'delete':
326 elif ctag == 'delete':
327 for token_class, token_text in old_char_tokens[co1:co2]:
327 for token_class, token_text in old_char_tokens[co1:co2]:
328 obuffer.append((token_class, 'del', token_text))
328 obuffer.append((token_class, 'del', token_text))
329 elif ctag == 'insert':
329 elif ctag == 'insert':
330 for token_class, token_text in new_char_tokens[cn1:cn2]:
330 for token_class, token_text in new_char_tokens[cn1:cn2]:
331 nbuffer.append((token_class, 'ins', token_text))
331 nbuffer.append((token_class, 'ins', token_text))
332 elif ctag == 'replace':
332 elif ctag == 'replace':
333 for token_class, token_text in old_char_tokens[co1:co2]:
333 for token_class, token_text in old_char_tokens[co1:co2]:
334 obuffer.append((token_class, 'del', token_text))
334 obuffer.append((token_class, 'del', token_text))
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
335 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 nbuffer.append((token_class, 'ins', token_text))
336 nbuffer.append((token_class, 'ins', token_text))
337
337
338 old_tokens_result.extend(obuffer)
338 old_tokens_result.extend(obuffer)
339 new_tokens_result.extend(nbuffer)
339 new_tokens_result.extend(nbuffer)
340
340
341 return old_tokens_result, new_tokens_result, similarity
341 return old_tokens_result, new_tokens_result, similarity
342
342
343
343
344 class DiffSet(object):
344 class DiffSet(object):
345 """
345 """
346 An object for parsing the diff result from diffs.DiffProcessor and
346 An object for parsing the diff result from diffs.DiffProcessor and
347 adding highlighting, side by side/unified renderings and line diffs
347 adding highlighting, side by side/unified renderings and line diffs
348 """
348 """
349
349
350 HL_REAL = 'REAL' # highlights using original file, slow
350 HL_REAL = 'REAL' # highlights using original file, slow
351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
351 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
352 # in the case of multiline code
352 # in the case of multiline code
353 HL_NONE = 'NONE' # no highlighting, fastest
353 HL_NONE = 'NONE' # no highlighting, fastest
354
354
355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
355 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
356 source_node_getter=lambda filename: None,
356 source_node_getter=lambda filename: None,
357 target_node_getter=lambda filename: None,
357 target_node_getter=lambda filename: None,
358 source_nodes=None, target_nodes=None,
358 source_nodes=None, target_nodes=None,
359 max_file_size_limit=150 * 1024, # files over this size will
359 max_file_size_limit=150 * 1024, # files over this size will
360 # use fast highlighting
360 # use fast highlighting
361 comments=None,
361 ):
362 ):
362
363
363 self.highlight_mode = highlight_mode
364 self.highlight_mode = highlight_mode
364 self.highlighted_filenodes = {}
365 self.highlighted_filenodes = {}
365 self.source_node_getter = source_node_getter
366 self.source_node_getter = source_node_getter
366 self.target_node_getter = target_node_getter
367 self.target_node_getter = target_node_getter
367 self.source_nodes = source_nodes or {}
368 self.source_nodes = source_nodes or {}
368 self.target_nodes = target_nodes or {}
369 self.target_nodes = target_nodes or {}
369 self.repo_name = repo_name
370 self.repo_name = repo_name
370
371 self.comments = comments or {}
371 self.max_file_size_limit = max_file_size_limit
372 self.max_file_size_limit = max_file_size_limit
372
373
373 def render_patchset(self, patchset, source_ref=None, target_ref=None):
374 def render_patchset(self, patchset, source_ref=None, target_ref=None):
374 diffset = AttributeDict(dict(
375 diffset = AttributeDict(dict(
375 lines_added=0,
376 lines_added=0,
376 lines_deleted=0,
377 lines_deleted=0,
377 changed_files=0,
378 changed_files=0,
378 files=[],
379 files=[],
379 limited_diff=isinstance(patchset, LimitedDiffContainer),
380 limited_diff=isinstance(patchset, LimitedDiffContainer),
380 repo_name=self.repo_name,
381 repo_name=self.repo_name,
381 source_ref=source_ref,
382 source_ref=source_ref,
382 target_ref=target_ref,
383 target_ref=target_ref,
383 ))
384 ))
384 for patch in patchset:
385 for patch in patchset:
385 filediff = self.render_patch(patch)
386 filediff = self.render_patch(patch)
386 filediff.diffset = diffset
387 filediff.diffset = diffset
387 diffset.files.append(filediff)
388 diffset.files.append(filediff)
388 diffset.changed_files += 1
389 diffset.changed_files += 1
389 if not patch['stats']['binary']:
390 if not patch['stats']['binary']:
390 diffset.lines_added += patch['stats']['added']
391 diffset.lines_added += patch['stats']['added']
391 diffset.lines_deleted += patch['stats']['deleted']
392 diffset.lines_deleted += patch['stats']['deleted']
392
393
393 return diffset
394 return diffset
394
395
395 _lexer_cache = {}
396 _lexer_cache = {}
396 def _get_lexer_for_filename(self, filename):
397 def _get_lexer_for_filename(self, filename):
397 # cached because we might need to call it twice for source/target
398 # cached because we might need to call it twice for source/target
398 if filename not in self._lexer_cache:
399 if filename not in self._lexer_cache:
399 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
400 self._lexer_cache[filename] = get_lexer_safe(filepath=filename)
400 return self._lexer_cache[filename]
401 return self._lexer_cache[filename]
401
402
402 def render_patch(self, patch):
403 def render_patch(self, patch):
403 log.debug('rendering diff for %r' % patch['filename'])
404 log.debug('rendering diff for %r' % patch['filename'])
404
405
405 source_filename = patch['original_filename']
406 source_filename = patch['original_filename']
406 target_filename = patch['filename']
407 target_filename = patch['filename']
407
408
408 source_lexer = plain_text_lexer
409 source_lexer = plain_text_lexer
409 target_lexer = plain_text_lexer
410 target_lexer = plain_text_lexer
410
411
411 if not patch['stats']['binary']:
412 if not patch['stats']['binary']:
412 if self.highlight_mode == self.HL_REAL:
413 if self.highlight_mode == self.HL_REAL:
413 if (source_filename and patch['operation'] in ('D', 'M')
414 if (source_filename and patch['operation'] in ('D', 'M')
414 and source_filename not in self.source_nodes):
415 and source_filename not in self.source_nodes):
415 self.source_nodes[source_filename] = (
416 self.source_nodes[source_filename] = (
416 self.source_node_getter(source_filename))
417 self.source_node_getter(source_filename))
417
418
418 if (target_filename and patch['operation'] in ('A', 'M')
419 if (target_filename and patch['operation'] in ('A', 'M')
419 and target_filename not in self.target_nodes):
420 and target_filename not in self.target_nodes):
420 self.target_nodes[target_filename] = (
421 self.target_nodes[target_filename] = (
421 self.target_node_getter(target_filename))
422 self.target_node_getter(target_filename))
422
423
423 elif self.highlight_mode == self.HL_FAST:
424 elif self.highlight_mode == self.HL_FAST:
424 source_lexer = self._get_lexer_for_filename(source_filename)
425 source_lexer = self._get_lexer_for_filename(source_filename)
425 target_lexer = self._get_lexer_for_filename(target_filename)
426 target_lexer = self._get_lexer_for_filename(target_filename)
426
427
427 source_file = self.source_nodes.get(source_filename, source_filename)
428 source_file = self.source_nodes.get(source_filename, source_filename)
428 target_file = self.target_nodes.get(target_filename, target_filename)
429 target_file = self.target_nodes.get(target_filename, target_filename)
429
430
430 source_filenode, target_filenode = None, None
431 source_filenode, target_filenode = None, None
431
432
432 # TODO: dan: FileNode.lexer works on the content of the file - which
433 # TODO: dan: FileNode.lexer works on the content of the file - which
433 # can be slow - issue #4289 explains a lexer clean up - which once
434 # can be slow - issue #4289 explains a lexer clean up - which once
434 # done can allow caching a lexer for a filenode to avoid the file lookup
435 # done can allow caching a lexer for a filenode to avoid the file lookup
435 if isinstance(source_file, FileNode):
436 if isinstance(source_file, FileNode):
436 source_filenode = source_file
437 source_filenode = source_file
437 source_lexer = source_file.lexer
438 source_lexer = source_file.lexer
438 if isinstance(target_file, FileNode):
439 if isinstance(target_file, FileNode):
439 target_filenode = target_file
440 target_filenode = target_file
440 target_lexer = target_file.lexer
441 target_lexer = target_file.lexer
441
442
442 source_file_path, target_file_path = None, None
443 source_file_path, target_file_path = None, None
443
444
444 if source_filename != '/dev/null':
445 if source_filename != '/dev/null':
445 source_file_path = source_filename
446 source_file_path = source_filename
446 if target_filename != '/dev/null':
447 if target_filename != '/dev/null':
447 target_file_path = target_filename
448 target_file_path = target_filename
448
449
449 source_file_type = source_lexer.name
450 source_file_type = source_lexer.name
450 target_file_type = target_lexer.name
451 target_file_type = target_lexer.name
451
452
452 op_hunks = patch['chunks'][0]
453 op_hunks = patch['chunks'][0]
453 hunks = patch['chunks'][1:]
454 hunks = patch['chunks'][1:]
454
455
455 filediff = AttributeDict({
456 filediff = AttributeDict({
456 'source_file_path': source_file_path,
457 'source_file_path': source_file_path,
457 'target_file_path': target_file_path,
458 'target_file_path': target_file_path,
458 'source_filenode': source_filenode,
459 'source_filenode': source_filenode,
459 'target_filenode': target_filenode,
460 'target_filenode': target_filenode,
460 'hunks': [],
461 'hunks': [],
461 'source_file_type': target_file_type,
462 'source_file_type': target_file_type,
462 'target_file_type': source_file_type,
463 'target_file_type': source_file_type,
463 'patch': patch,
464 'patch': patch,
464 'source_mode': patch['stats']['old_mode'],
465 'source_mode': patch['stats']['old_mode'],
465 'target_mode': patch['stats']['new_mode'],
466 'target_mode': patch['stats']['new_mode'],
466 'limited_diff': isinstance(patch, LimitedDiffContainer),
467 'limited_diff': isinstance(patch, LimitedDiffContainer),
467 'diffset': self,
468 'diffset': self,
468 })
469 })
469
470
470 for hunk in hunks:
471 for hunk in hunks:
471 hunkbit = self.parse_hunk(hunk, source_file, target_file)
472 hunkbit = self.parse_hunk(hunk, source_file, target_file)
472 hunkbit.filediff = filediff
473 hunkbit.filediff = filediff
473 filediff.hunks.append(hunkbit)
474 filediff.hunks.append(hunkbit)
474 return filediff
475 return filediff
475
476
476 def parse_hunk(self, hunk, source_file, target_file):
477 def parse_hunk(self, hunk, source_file, target_file):
477 result = AttributeDict(dict(
478 result = AttributeDict(dict(
478 source_start=hunk['source_start'],
479 source_start=hunk['source_start'],
479 source_length=hunk['source_length'],
480 source_length=hunk['source_length'],
480 target_start=hunk['target_start'],
481 target_start=hunk['target_start'],
481 target_length=hunk['target_length'],
482 target_length=hunk['target_length'],
482 section_header=hunk['section_header'],
483 section_header=hunk['section_header'],
483 lines=[],
484 lines=[],
484 ))
485 ))
485 before, after = [], []
486 before, after = [], []
486
487
487 for line in hunk['lines']:
488 for line in hunk['lines']:
488 if line['action'] == 'unmod':
489 if line['action'] == 'unmod':
489 result.lines.extend(
490 result.lines.extend(
490 self.parse_lines(before, after, source_file, target_file))
491 self.parse_lines(before, after, source_file, target_file))
491 after.append(line)
492 after.append(line)
492 before.append(line)
493 before.append(line)
493 elif line['action'] == 'add':
494 elif line['action'] == 'add':
494 after.append(line)
495 after.append(line)
495 elif line['action'] == 'del':
496 elif line['action'] == 'del':
496 before.append(line)
497 before.append(line)
497 elif line['action'] == 'old-no-nl':
498 elif line['action'] == 'old-no-nl':
498 before.append(line)
499 before.append(line)
499 elif line['action'] == 'new-no-nl':
500 elif line['action'] == 'new-no-nl':
500 after.append(line)
501 after.append(line)
501
502
502 result.lines.extend(
503 result.lines.extend(
503 self.parse_lines(before, after, source_file, target_file))
504 self.parse_lines(before, after, source_file, target_file))
504 result.unified = self.as_unified(result.lines)
505 result.unified = self.as_unified(result.lines)
505 result.sideside = result.lines
506 result.sideside = result.lines
506 return result
507 return result
507
508
508 def parse_lines(self, before_lines, after_lines, source_file, target_file):
509 def parse_lines(self, before_lines, after_lines, source_file, target_file):
509 # TODO: dan: investigate doing the diff comparison and fast highlighting
510 # TODO: dan: investigate doing the diff comparison and fast highlighting
510 # on the entire before and after buffered block lines rather than by
511 # on the entire before and after buffered block lines rather than by
511 # line, this means we can get better 'fast' highlighting if the context
512 # line, this means we can get better 'fast' highlighting if the context
512 # allows it - eg.
513 # allows it - eg.
513 # line 4: """
514 # line 4: """
514 # line 5: this gets highlighted as a string
515 # line 5: this gets highlighted as a string
515 # line 6: """
516 # line 6: """
516
517
517 lines = []
518 lines = []
518 while before_lines or after_lines:
519 while before_lines or after_lines:
519 before, after = None, None
520 before, after = None, None
520 before_tokens, after_tokens = None, None
521 before_tokens, after_tokens = None, None
521
522
522 if before_lines:
523 if before_lines:
523 before = before_lines.pop(0)
524 before = before_lines.pop(0)
524 if after_lines:
525 if after_lines:
525 after = after_lines.pop(0)
526 after = after_lines.pop(0)
526
527
527 original = AttributeDict()
528 original = AttributeDict()
528 modified = AttributeDict()
529 modified = AttributeDict()
529
530
530 if before:
531 if before:
531 if before['action'] == 'old-no-nl':
532 if before['action'] == 'old-no-nl':
532 before_tokens = [('nonl', before['line'])]
533 before_tokens = [('nonl', before['line'])]
533 else:
534 else:
534 before_tokens = self.get_line_tokens(
535 before_tokens = self.get_line_tokens(
535 line_text=before['line'], line_number=before['old_lineno'],
536 line_text=before['line'], line_number=before['old_lineno'],
536 file=source_file)
537 file=source_file)
537 original.lineno = before['old_lineno']
538 original.lineno = before['old_lineno']
538 original.content = before['line']
539 original.content = before['line']
539 original.action = self.action_to_op(before['action'])
540 original.action = self.action_to_op(before['action'])
541 original.comments = self.get_comments_for('old',
542 source_file, before['old_lineno'])
540
543
541 if after:
544 if after:
542 if after['action'] == 'new-no-nl':
545 if after['action'] == 'new-no-nl':
543 after_tokens = [('nonl', after['line'])]
546 after_tokens = [('nonl', after['line'])]
544 else:
547 else:
545 after_tokens = self.get_line_tokens(
548 after_tokens = self.get_line_tokens(
546 line_text=after['line'], line_number=after['new_lineno'],
549 line_text=after['line'], line_number=after['new_lineno'],
547 file=target_file)
550 file=target_file)
548 modified.lineno = after['new_lineno']
551 modified.lineno = after['new_lineno']
549 modified.content = after['line']
552 modified.content = after['line']
550 modified.action = self.action_to_op(after['action'])
553 modified.action = self.action_to_op(after['action'])
554 modified.comments = self.get_comments_for('new',
555 target_file, after['new_lineno'])
551
556
552 # diff the lines
557 # diff the lines
553 if before_tokens and after_tokens:
558 if before_tokens and after_tokens:
554 o_tokens, m_tokens, similarity = tokens_diff(
559 o_tokens, m_tokens, similarity = tokens_diff(
555 before_tokens, after_tokens)
560 before_tokens, after_tokens)
556 original.content = render_tokenstream(o_tokens)
561 original.content = render_tokenstream(o_tokens)
557 modified.content = render_tokenstream(m_tokens)
562 modified.content = render_tokenstream(m_tokens)
558 elif before_tokens:
563 elif before_tokens:
559 original.content = render_tokenstream(
564 original.content = render_tokenstream(
560 [(x[0], '', x[1]) for x in before_tokens])
565 [(x[0], '', x[1]) for x in before_tokens])
561 elif after_tokens:
566 elif after_tokens:
562 modified.content = render_tokenstream(
567 modified.content = render_tokenstream(
563 [(x[0], '', x[1]) for x in after_tokens])
568 [(x[0], '', x[1]) for x in after_tokens])
564
569
565 lines.append(AttributeDict({
570 lines.append(AttributeDict({
566 'original': original,
571 'original': original,
567 'modified': modified,
572 'modified': modified,
568 }))
573 }))
569
574
570 return lines
575 return lines
571
576
577 def get_comments_for(self, version, file, line_number):
578 if hasattr(file, 'unicode_path'):
579 file = file.unicode_path
580
581 if not isinstance(file, basestring):
582 return None
583
584 line_key = {
585 'old': 'o',
586 'new': 'n',
587 }[version] + str(line_number)
588
589 return self.comments.get(file, {}).get(line_key)
590
572 def get_line_tokens(self, line_text, line_number, file=None):
591 def get_line_tokens(self, line_text, line_number, file=None):
573 filenode = None
592 filenode = None
574 filename = None
593 filename = None
575
594
576 if isinstance(file, basestring):
595 if isinstance(file, basestring):
577 filename = file
596 filename = file
578 elif isinstance(file, FileNode):
597 elif isinstance(file, FileNode):
579 filenode = file
598 filenode = file
580 filename = file.unicode_path
599 filename = file.unicode_path
581
600
582 if self.highlight_mode == self.HL_REAL and filenode:
601 if self.highlight_mode == self.HL_REAL and filenode:
583 if line_number and file.size < self.max_file_size_limit:
602 if line_number and file.size < self.max_file_size_limit:
584 return self.get_tokenized_filenode_line(file, line_number)
603 return self.get_tokenized_filenode_line(file, line_number)
585
604
586 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
605 if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename:
587 lexer = self._get_lexer_for_filename(filename)
606 lexer = self._get_lexer_for_filename(filename)
588 return list(tokenize_string(line_text, lexer))
607 return list(tokenize_string(line_text, lexer))
589
608
590 return list(tokenize_string(line_text, plain_text_lexer))
609 return list(tokenize_string(line_text, plain_text_lexer))
591
610
592 def get_tokenized_filenode_line(self, filenode, line_number):
611 def get_tokenized_filenode_line(self, filenode, line_number):
593
612
594 if filenode not in self.highlighted_filenodes:
613 if filenode not in self.highlighted_filenodes:
595 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
614 tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer)
596 self.highlighted_filenodes[filenode] = tokenized_lines
615 self.highlighted_filenodes[filenode] = tokenized_lines
597 return self.highlighted_filenodes[filenode][line_number - 1]
616 return self.highlighted_filenodes[filenode][line_number - 1]
598
617
599 def action_to_op(self, action):
618 def action_to_op(self, action):
600 return {
619 return {
601 'add': '+',
620 'add': '+',
602 'del': '-',
621 'del': '-',
603 'unmod': ' ',
622 'unmod': ' ',
604 'old-no-nl': ' ',
623 'old-no-nl': ' ',
605 'new-no-nl': ' ',
624 'new-no-nl': ' ',
606 }.get(action, action)
625 }.get(action, action)
607
626
608 def as_unified(self, lines):
627 def as_unified(self, lines):
609 """ Return a generator that yields the lines of a diff in unified order """
628 """ Return a generator that yields the lines of a diff in unified order """
610 def generator():
629 def generator():
611 buf = []
630 buf = []
612 for line in lines:
631 for line in lines:
613
632
614 if buf and not line.original or line.original.action == ' ':
633 if buf and not line.original or line.original.action == ' ':
615 for b in buf:
634 for b in buf:
616 yield b
635 yield b
617 buf = []
636 buf = []
618
637
619 if line.original:
638 if line.original:
620 if line.original.action == ' ':
639 if line.original.action == ' ':
621 yield (line.original.lineno, line.modified.lineno,
640 yield (line.original.lineno, line.modified.lineno,
622 line.original.action, line.original.content)
641 line.original.action, line.original.content,
642 line.original.comments)
623 continue
643 continue
624
644
625 if line.original.action == '-':
645 if line.original.action == '-':
626 yield (line.original.lineno, None,
646 yield (line.original.lineno, None,
627 line.original.action, line.original.content)
647 line.original.action, line.original.content,
648 line.original.comments)
628
649
629 if line.modified.action == '+':
650 if line.modified.action == '+':
630 buf.append((
651 buf.append((
631 None, line.modified.lineno,
652 None, line.modified.lineno,
632 line.modified.action, line.modified.content))
653 line.modified.action, line.modified.content,
654 line.modified.comments))
633 continue
655 continue
634
656
635 if line.modified:
657 if line.modified:
636 yield (None, line.modified.lineno,
658 yield (None, line.modified.lineno,
637 line.modified.action, line.modified.content)
659 line.modified.action, line.modified.content,
660 line.modified.comments)
638
661
639 for b in buf:
662 for b in buf:
640 yield b
663 yield b
641
664
642 return generator()
665 return generator()
@@ -1,1039 +1,1142 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 }
713 }
714 .pill {
714 .pill {
715 display: block;
715 display: block;
716 float: left;
716 float: left;
717 padding: @pill-padding;
717 padding: @pill-padding;
718 }
718 }
719 .pill-group {
719 .pill-group {
720 .pill {
720 .pill {
721 opacity: .8;
721 opacity: .8;
722 &:first-child {
722 &:first-child {
723 border-radius: @border-radius 0 0 @border-radius;
723 border-radius: @border-radius 0 0 @border-radius;
724 }
724 }
725 &:last-child {
725 &:last-child {
726 border-radius: 0 @border-radius @border-radius 0;
726 border-radius: 0 @border-radius @border-radius 0;
727 }
727 }
728 &:only-child {
728 &:only-child {
729 border-radius: @border-radius;
729 border-radius: @border-radius;
730 }
730 }
731 }
731 }
732 }
732 }
733
733 .filediff {
734 .filediff {
734 border: 1px solid @grey5;
735 border: 1px solid @grey5;
735
736
736 /* START OVERRIDES */
737 /* START OVERRIDES */
737 .code-highlight {
738 .code-highlight {
738 border: none; // TODO: remove this border from the global
739 border: none; // TODO: remove this border from the global
739 // .code-highlight, it doesn't belong there
740 // .code-highlight, it doesn't belong there
740 }
741 }
741 label {
742 label {
742 margin: 0; // TODO: remove this margin definition from global label
743 margin: 0; // TODO: remove this margin definition from global label
743 // it doesn't belong there - if margin on labels
744 // it doesn't belong there - if margin on labels
744 // are needed for a form they should be defined
745 // are needed for a form they should be defined
745 // in the form's class
746 // in the form's class
746 }
747 }
747 /* END OVERRIDES */
748 /* END OVERRIDES */
748
749
749 * {
750 * {
750 box-sizing: border-box;
751 box-sizing: border-box;
751 }
752 }
752 .filediff-anchor {
753 .filediff-anchor {
753 visibility: hidden;
754 visibility: hidden;
754 }
755 }
755 &:hover {
756 &:hover {
756 .filediff-anchor {
757 .filediff-anchor {
757 visibility: visible;
758 visibility: visible;
758 }
759 }
759 }
760 }
760
761
761 .filediff-collapse-indicator {
762 .filediff-collapse-indicator {
762 width: 0;
763 width: 0;
763 height: 0;
764 height: 0;
764 border-style: solid;
765 border-style: solid;
765 float: left;
766 float: left;
766 margin: 2px 2px 0 0;
767 margin: 2px 2px 0 0;
767 cursor: pointer;
768 cursor: pointer;
768 }
769 }
769
770
770 .filediff-heading {
771 .filediff-heading {
771 background: @grey7;
772 background: @grey7;
772 cursor: pointer;
773 cursor: pointer;
773 display: block;
774 display: block;
774 padding: 5px 10px;
775 padding: 5px 10px;
775 }
776 }
776 .filediff-heading:after {
777 .filediff-heading:after {
777 content: "";
778 content: "";
778 display: table;
779 display: table;
779 clear: both;
780 clear: both;
780 }
781 }
781 .filediff-heading:hover {
782 .filediff-heading:hover {
782 background: #e1e9f4 !important;
783 background: #e1e9f4 !important;
783 }
784 }
784
785
785 .filediff-menu {
786 .filediff-menu {
786 float: right;
787 float: right;
787
788
788 a, span {
789 &> a, &> span {
789 padding: 5px;
790 padding: 5px;
790 display: block;
791 display: block;
791 float: left
792 float: left
792 }
793 }
793 }
794 }
795
794 .pill {
796 .pill {
795 &[op="name"] {
797 &[op="name"] {
796 background: none;
798 background: none;
797 color: @grey2;
799 color: @grey2;
798 opacity: 1;
800 opacity: 1;
799 color: white;
801 color: white;
800 }
802 }
801 &[op="limited"] {
803 &[op="limited"] {
802 background: @grey2;
804 background: @grey2;
803 color: white;
805 color: white;
804 }
806 }
805 &[op="binary"] {
807 &[op="binary"] {
806 background: @color7;
808 background: @color7;
807 color: white;
809 color: white;
808 }
810 }
809 &[op="modified"] {
811 &[op="modified"] {
810 background: @alert1;
812 background: @alert1;
811 color: white;
813 color: white;
812 }
814 }
813 &[op="renamed"] {
815 &[op="renamed"] {
814 background: @color4;
816 background: @color4;
815 color: white;
817 color: white;
816 }
818 }
817 &[op="mode"] {
819 &[op="mode"] {
818 background: @grey3;
820 background: @grey3;
819 color: white;
821 color: white;
820 }
822 }
821 &[op="symlink"] {
823 &[op="symlink"] {
822 background: @color8;
824 background: @color8;
823 color: white;
825 color: white;
824 }
826 }
825
827
826 &[op="added"] { /* added lines */
828 &[op="added"] { /* added lines */
827 background: @alert1;
829 background: @alert1;
828 color: white;
830 color: white;
829 }
831 }
830 &[op="deleted"] { /* deleted lines */
832 &[op="deleted"] { /* deleted lines */
831 background: @alert2;
833 background: @alert2;
832 color: white;
834 color: white;
833 }
835 }
834
836
835 &[op="created"] { /* created file */
837 &[op="created"] { /* created file */
836 background: @alert1;
838 background: @alert1;
837 color: white;
839 color: white;
838 }
840 }
839 &[op="removed"] { /* deleted file */
841 &[op="removed"] { /* deleted file */
840 background: @color5;
842 background: @color5;
841 color: white;
843 color: white;
842 }
844 }
843 }
845 }
844
846
845 .filediff-collapse-button, .filediff-expand-button {
847 .filediff-collapse-button, .filediff-expand-button {
846 cursor: pointer;
848 cursor: pointer;
847 }
849 }
848 .filediff-collapse-button {
850 .filediff-collapse-button {
849 display: inline;
851 display: inline;
850 }
852 }
851 .filediff-expand-button {
853 .filediff-expand-button {
852 display: none;
854 display: none;
853 }
855 }
854 .filediff-collapsed .filediff-collapse-button {
856 .filediff-collapsed .filediff-collapse-button {
855 display: none;
857 display: none;
856 }
858 }
857 .filediff-collapsed .filediff-expand-button {
859 .filediff-collapsed .filediff-expand-button {
858 display: inline;
860 display: inline;
859 }
861 }
862
863 @comment-padding: 5px;
864
865 /**** COMMENTS ****/
866
867 .filediff-menu {
868 .show-comment-button {
869 display: none;
870 }
871 }
872 &.hide-comments {
873 .inline-comments {
874 display: none;
875 }
876 .filediff-menu {
877 .show-comment-button {
878 display: inline;
879 }
880 .show-comment-button {
881 display: none;
882 }
883 }
884 }
885 .inline-comments {
886 border-radius: @border-radius;
887 background: @grey6;
888 .comment {
889 margin: 0;
890 border-radius: @border-radius;
891 }
892 .comment-outdated {
893 opacity: 0.5;
894 }
895 .comment-inline {
896 background: white;
897 padding: (@comment-padding + 3px) @comment-padding;
898 border: @comment-padding solid @grey6;
899
900 .text {
901 border: none;
902 }
903 .meta {
904 border-bottom: 1px solid @grey6;
905 padding-bottom: 10px;
906 }
907 }
908 .comment-selected {
909 border-left: 6px solid @comment-highlight-color;
910 }
911 .comment-inline-form {
912 padding: @comment-padding;
913 display: none;
914 }
915 .cb-comment-add-button {
916 margin: @comment-padding;
917 }
918 /* hide add comment button when form is open */
919 .comment-inline-form-open + .cb-comment-add-button {
920 display: none;
921 }
922 .comment-inline-form-open {
923 display: block;
924 }
925 /* hide add comment button when form but no comments */
926 .comment-inline-form:first-child + .cb-comment-add-button {
927 display: none;
928 }
929 /* hide add comment button when no comments or form */
930 .cb-comment-add-button:first-child {
931 display: none;
932 }
933 /* hide add comment button when only comment is being deleted */
934 .comment-deleting:first-child + .cb-comment-add-button {
935 display: none;
936 }
937 }
938 /**** END COMMENTS ****/
939
860 }
940 }
941
942
861 table.cb {
943 table.cb {
862 width: 100%;
944 width: 100%;
863 border-collapse: collapse;
945 border-collapse: collapse;
864
946
865 .cb-text {
947 .cb-text {
866 padding: @cb-text-padding;
948 padding: @cb-text-padding;
867 }
949 }
868 .cb-hunk {
950 .cb-hunk {
869 padding: @cb-text-padding;
951 padding: @cb-text-padding;
870 }
952 }
871 .cb-expand {
953 .cb-expand {
872 display: none;
954 display: none;
873 }
955 }
874 .cb-collapse {
956 .cb-collapse {
875 display: inline;
957 display: inline;
876 }
958 }
877 &.cb-collapsed {
959 &.cb-collapsed {
878 .cb-line {
960 .cb-line {
879 display: none;
961 display: none;
880 }
962 }
881 .cb-expand {
963 .cb-expand {
882 display: inline;
964 display: inline;
883 }
965 }
884 .cb-collapse {
966 .cb-collapse {
885 display: none;
967 display: none;
886 }
968 }
887 }
969 }
888
970
889 /* intentionally general selector since .cb-line-selected must override it
971 /* intentionally general selector since .cb-line-selected must override it
890 and they both use !important since the td itself may have a random color
972 and they both use !important since the td itself may have a random color
891 generated by annotation blocks. TLDR: if you change it, make sure
973 generated by annotation blocks. TLDR: if you change it, make sure
892 annotated block selection and line selection in file view still work */
974 annotated block selection and line selection in file view still work */
893 .cb-line-fresh .cb-content {
975 .cb-line-fresh .cb-content {
894 background: white !important;
976 background: white !important;
895 }
977 }
896 .cb-warning {
978 .cb-warning {
897 background: #fff4dd;
979 background: #fff4dd;
898 }
980 }
899
981
900 &.cb-diff-sideside {
982 &.cb-diff-sideside {
901 td {
983 td {
902 &.cb-content {
984 &.cb-content {
903 width: 50%;
985 width: 50%;
904 }
986 }
905 }
987 }
906 }
988 }
907
989
908 tr {
990 tr {
909 &.cb-annotate {
991 &.cb-annotate {
910 border-top: 1px solid #eee;
992 border-top: 1px solid #eee;
911
993
912 &+ .cb-line {
994 &+ .cb-line {
913 border-top: 1px solid #eee;
995 border-top: 1px solid #eee;
914 }
996 }
915
997
916 &:first-child {
998 &:first-child {
917 border-top: none;
999 border-top: none;
918 &+ .cb-line {
1000 &+ .cb-line {
919 border-top: none;
1001 border-top: none;
920 }
1002 }
921 }
1003 }
922 }
1004 }
923
1005
924 &.cb-hunk {
1006 &.cb-hunk {
925 font-family: @font-family-monospace;
1007 font-family: @font-family-monospace;
926 color: rgba(0, 0, 0, 0.3);
1008 color: rgba(0, 0, 0, 0.3);
927
1009
928 td {
1010 td {
929 &:first-child {
1011 &:first-child {
930 background: #edf2f9;
1012 background: #edf2f9;
931 }
1013 }
932 &:last-child {
1014 &:last-child {
933 background: #f4f7fb;
1015 background: #f4f7fb;
934 }
1016 }
935 }
1017 }
936 }
1018 }
937 }
1019 }
938
1020
939 td {
1021 td {
940 vertical-align: top;
1022 vertical-align: top;
941 padding: 0;
1023 padding: 0;
942
1024
943 &.cb-content {
1025 &.cb-content {
944 font-size: 12.35px;
1026 font-size: 12.35px;
945
1027
946 &.cb-line-selected .cb-code {
1028 &.cb-line-selected .cb-code {
947 background: @comment-highlight-color !important;
1029 background: @comment-highlight-color !important;
948 }
1030 }
949
1031
950 span.cb-code {
1032 span.cb-code {
951 line-height: @cb-line-height;
1033 line-height: @cb-line-height;
952 padding-left: @cb-line-code-padding;
1034 padding-left: @cb-line-code-padding;
953 padding-right: @cb-line-code-padding;
1035 padding-right: @cb-line-code-padding;
954 display: block;
1036 display: block;
955 white-space: pre-wrap;
1037 white-space: pre-wrap;
956 font-family: @font-family-monospace;
1038 font-family: @font-family-monospace;
957 word-break: break-word;
1039 word-break: break-word;
958 }
1040 }
1041
1042 &> button.cb-comment-box-opener {
1043 padding: 2px 6px 2px 6px;
1044 margin-left: -20px;
1045 margin-top: -2px;
1046 border-radius: @border-radius;
1047 position: absolute;
1048 display: none;
1049 }
1050 .cb-comment {
1051 margin-top: 10px;
1052 white-space: normal;
1053 }
1054 }
1055 &:hover {
1056 button.cb-comment-box-opener {
1057 display: block;
1058 }
1059 &+ td button.cb-comment-box-opener {
1060 display: block
1061 }
959 }
1062 }
960
1063
961 &.cb-lineno {
1064 &.cb-lineno {
962 padding: 0;
1065 padding: 0;
963 width: 50px;
1066 width: 50px;
964 color: rgba(0, 0, 0, 0.3);
1067 color: rgba(0, 0, 0, 0.3);
965 text-align: right;
1068 text-align: right;
966 border-right: 1px solid #eee;
1069 border-right: 1px solid #eee;
967 font-family: @font-family-monospace;
1070 font-family: @font-family-monospace;
968
1071
969 a::before {
1072 a::before {
970 content: attr(data-line-no);
1073 content: attr(data-line-no);
971 }
1074 }
972 &.cb-line-selected a {
1075 &.cb-line-selected a {
973 background: @comment-highlight-color !important;
1076 background: @comment-highlight-color !important;
974 }
1077 }
975
1078
976 a {
1079 a {
977 display: block;
1080 display: block;
978 padding-right: @cb-line-code-padding;
1081 padding-right: @cb-line-code-padding;
979 padding-left: @cb-line-code-padding;
1082 padding-left: @cb-line-code-padding;
980 line-height: @cb-line-height;
1083 line-height: @cb-line-height;
981 color: rgba(0, 0, 0, 0.3);
1084 color: rgba(0, 0, 0, 0.3);
982 }
1085 }
983 }
1086 }
984
1087
985 &.cb-empty {
1088 &.cb-empty {
986 background: @grey7;
1089 background: @grey7;
987 }
1090 }
988
1091
989 ins {
1092 ins {
990 color: black;
1093 color: black;
991 background: #a6f3a6;
1094 background: #a6f3a6;
992 text-decoration: none;
1095 text-decoration: none;
993 }
1096 }
994 del {
1097 del {
995 color: black;
1098 color: black;
996 background: #f8cbcb;
1099 background: #f8cbcb;
997 text-decoration: none;
1100 text-decoration: none;
998 }
1101 }
999 &.cb-addition {
1102 &.cb-addition {
1000 background: #ecffec;
1103 background: #ecffec;
1001
1104
1002 &.blob-lineno {
1105 &.blob-lineno {
1003 background: #ddffdd;
1106 background: #ddffdd;
1004 }
1107 }
1005 }
1108 }
1006 &.cb-deletion {
1109 &.cb-deletion {
1007 background: #ffecec;
1110 background: #ffecec;
1008
1111
1009 &.blob-lineno {
1112 &.blob-lineno {
1010 background: #ffdddd;
1113 background: #ffdddd;
1011 }
1114 }
1012 }
1115 }
1013
1116
1014 &.cb-annotate-info {
1117 &.cb-annotate-info {
1015 width: 320px;
1118 width: 320px;
1016 min-width: 320px;
1119 min-width: 320px;
1017 max-width: 320px;
1120 max-width: 320px;
1018 padding: 5px 2px;
1121 padding: 5px 2px;
1019 font-size: 13px;
1122 font-size: 13px;
1020
1123
1021 strong.cb-annotate-message {
1124 strong.cb-annotate-message {
1022 padding: 5px 0;
1125 padding: 5px 0;
1023 white-space: pre-line;
1126 white-space: pre-line;
1024 display: inline-block;
1127 display: inline-block;
1025 }
1128 }
1026 .rc-user {
1129 .rc-user {
1027 float: none;
1130 float: none;
1028 padding: 0 6px 0 17px;
1131 padding: 0 6px 0 17px;
1029 min-width: auto;
1132 min-width: auto;
1030 min-height: auto;
1133 min-height: auto;
1031 }
1134 }
1032 }
1135 }
1033
1136
1034 &.cb-annotate-revision {
1137 &.cb-annotate-revision {
1035 cursor: pointer;
1138 cursor: pointer;
1036 text-align: right;
1139 text-align: right;
1037 }
1140 }
1038 }
1141 }
1039 }
1142 }
@@ -1,85 +1,88 b''
1 //--- RESETS ---//
1 //--- RESETS ---//
2 :focus { outline: none; }
2 :focus { outline: none; }
3 a { cursor: pointer; }
3 a { cursor: pointer; }
4
4
5 //--- clearfix --//
5 //--- clearfix --//
6 .clearfix {
6 .clearfix {
7 &:before,
7 &:before,
8 &:after {
8 &:after {
9 content:"";
9 content:"";
10 width: 100%;
10 width: 100%;
11 clear: both;
11 clear: both;
12 float: left;
12 float: left;
13 }
13 }
14 }
14 }
15
15
16 .clearinner:after { /* clears all floating divs inside a block */
16 .clearinner:after { /* clears all floating divs inside a block */
17 content: "";
17 content: "";
18 display: table;
18 display: table;
19 clear: both;
19 clear: both;
20 }
20 }
21
21
22 .js-template { /* mark a template for javascript use */
23 display: none;
24 }
22
25
23 .linebreak {
26 .linebreak {
24 display: block;
27 display: block;
25 }
28 }
26
29
27 .pull-right {
30 .pull-right {
28 float: right !important;
31 float: right !important;
29 }
32 }
30
33
31 .pull-left {
34 .pull-left {
32 float: left !important;
35 float: left !important;
33 }
36 }
34
37
35 .block-left {
38 .block-left {
36 float: left;
39 float: left;
37 }
40 }
38
41
39 .block-right {
42 .block-right {
40 float: right;
43 float: right;
41 clear: right;
44 clear: right;
42
45
43 li:before { content:none; }
46 li:before { content:none; }
44 }
47 }
45
48
46 //--- DEVICE-SPECIFIC CLASSES ---------------//
49 //--- DEVICE-SPECIFIC CLASSES ---------------//
47 //regular tablet and up
50 //regular tablet and up
48 @media (min-width:768px) {
51 @media (min-width:768px) {
49 .no-mobile {
52 .no-mobile {
50 display: block;
53 display: block;
51 }
54 }
52 .mobile-only {
55 .mobile-only {
53 display: none;
56 display: none;
54 }
57 }
55 }
58 }
56 //small tablet and phone
59 //small tablet and phone
57 @media (max-width:767px) {
60 @media (max-width:767px) {
58 .mobile-only {
61 .mobile-only {
59 display: block;
62 display: block;
60 }
63 }
61 .no-mobile {
64 .no-mobile {
62 display: none;
65 display: none;
63 }
66 }
64 }
67 }
65
68
66 //--- STICKY FOOTER ---//
69 //--- STICKY FOOTER ---//
67 html, body {
70 html, body {
68 height: 100%;
71 height: 100%;
69 margin: 0;
72 margin: 0;
70 }
73 }
71 .outerwrapper {
74 .outerwrapper {
72 height: 100%;
75 height: 100%;
73 min-height: 100%;
76 min-height: 100%;
74 margin: 0;
77 margin: 0;
75 padding-bottom: 3em; /* must be equal to footer height */
78 padding-bottom: 3em; /* must be equal to footer height */
76 }
79 }
77 .outerwrapper:after{
80 .outerwrapper:after{
78 content:" ";
81 content:" ";
79 }
82 }
80 #footer {
83 #footer {
81 clear: both;
84 clear: both;
82 position: relative;
85 position: relative;
83 height: 3em; /* footer height */
86 height: 3em; /* footer height */
84 margin: -3em 0 0; /* must be equal to footer height */
87 margin: -3em 0 0; /* must be equal to footer height */
85 }
88 }
@@ -1,672 +1,672 b''
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 // returns a node from given html;
28 // returns a node from given html;
29 var fromHTML = function(html){
29 var fromHTML = function(html){
30 var _html = document.createElement('element');
30 var _html = document.createElement('element');
31 _html.innerHTML = html;
31 _html.innerHTML = html;
32 return _html;
32 return _html;
33 };
33 };
34
34
35 var tableTr = function(cls, body){
35 var tableTr = function(cls, body){
36 var _el = document.createElement('div');
36 var _el = document.createElement('div');
37 var _body = $(body).attr('id');
37 var _body = $(body).attr('id');
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 var id = 'comment-tr-{0}'.format(comment_id);
39 var id = 'comment-tr-{0}'.format(comment_id);
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 '<td></td>'+
42 '<td></td>'+
43 '<td></td>'+
43 '<td></td>'+
44 '<td></td>'+
44 '<td></td>'+
45 '<td>{2}</td>'+
45 '<td>{2}</td>'+
46 '</tr></tbody></table>').format(id, cls, body);
46 '</tr></tbody></table>').format(id, cls, body);
47 $(_el).html(_html);
47 $(_el).html(_html);
48 return _el.children[0].children[0].children[0];
48 return _el.children[0].children[0].children[0];
49 };
49 };
50
50
51 var removeInlineForm = function(form) {
51 var removeInlineForm = function(form) {
52 form.parentNode.removeChild(form);
52 form.parentNode.removeChild(form);
53 };
53 };
54
54
55 var createInlineForm = function(parent_tr, f_path, line) {
55 var createInlineForm = function(parent_tr, f_path, line) {
56 var tmpl = $('#comment-inline-form-template').html();
56 var tmpl = $('#comment-inline-form-template').html();
57 tmpl = tmpl.format(f_path, line);
57 tmpl = tmpl.format(f_path, line);
58 var form = tableTr('comment-form-inline', tmpl);
58 var form = tableTr('comment-form-inline', tmpl);
59 var form_hide_button = $(form).find('.hide-inline-form');
59 var form_hide_button = $(form).find('.hide-inline-form');
60
60
61 $(form_hide_button).click(function(e) {
61 $(form_hide_button).click(function(e) {
62 $('.inline-comments').removeClass('hide-comment-button');
62 $('.inline-comments').removeClass('hide-comment-button');
63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
65 $(newtr.nextElementSibling).show();
65 $(newtr.nextElementSibling).show();
66 }
66 }
67 $(newtr).parents('.comment-form-inline').remove();
67 $(newtr).parents('.comment-form-inline').remove();
68 $(parent_tr).removeClass('form-open');
68 $(parent_tr).removeClass('form-open');
69 $(parent_tr).removeClass('hl-comment');
69 $(parent_tr).removeClass('hl-comment');
70 });
70 });
71
71
72 return form;
72 return form;
73 };
73 };
74
74
75 var getLineNo = function(tr) {
75 var getLineNo = function(tr) {
76 var line;
76 var line;
77 // Try to get the id and return "" (empty string) if it doesn't exist
77 // Try to get the id and return "" (empty string) if it doesn't exist
78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
80 if (n.length >= 2) {
80 if (n.length >= 2) {
81 line = n[n.length-1];
81 line = n[n.length-1];
82 } else if (o.length >= 2) {
82 } else if (o.length >= 2) {
83 line = o[o.length-1];
83 line = o[o.length-1];
84 }
84 }
85 return line;
85 return line;
86 };
86 };
87
87
88 /**
88 /**
89 * make a single inline comment and place it inside
89 * make a single inline comment and place it inside
90 */
90 */
91 var renderInlineComment = function(json_data, show_add_button) {
91 var renderInlineComment = function(json_data, show_add_button) {
92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
93 try {
93 try {
94 var html = json_data.rendered_text;
94 var html = json_data.rendered_text;
95 var lineno = json_data.line_no;
95 var lineno = json_data.line_no;
96 var target_id = json_data.target_id;
96 var target_id = json_data.target_id;
97 placeInline(target_id, lineno, html, show_add_button);
97 placeInline(target_id, lineno, html, show_add_button);
98 } catch (e) {
98 } catch (e) {
99 console.error(e);
99 console.error(e);
100 }
100 }
101 };
101 };
102
102
103 function bindDeleteCommentButtons() {
103 function bindDeleteCommentButtons() {
104 $('.delete-comment').one('click', function() {
104 $('.delete-comment').one('click', function() {
105 var comment_id = $(this).data("comment-id");
105 var comment_id = $(this).data("comment-id");
106
106
107 if (comment_id){
107 if (comment_id){
108 deleteComment(comment_id);
108 deleteComment(comment_id);
109 }
109 }
110 });
110 });
111 }
111 }
112
112
113 /**
113 /**
114 * Inject inline comment for on given TR this tr should be always an .line
114 * Inject inline comment for on given TR this tr should be always an .line
115 * tr containing the line. Code will detect comment, and always put the comment
115 * tr containing the line. Code will detect comment, and always put the comment
116 * block at the very bottom
116 * block at the very bottom
117 */
117 */
118 var injectInlineForm = function(tr){
118 var injectInlineForm = function(tr){
119 if (!$(tr).hasClass('line')) {
119 if (!$(tr).hasClass('line')) {
120 return;
120 return;
121 }
121 }
122
122
123 var _td = $(tr).find('.code').get(0);
123 var _td = $(tr).find('.code').get(0);
124 if ($(tr).hasClass('form-open') ||
124 if ($(tr).hasClass('form-open') ||
125 $(tr).hasClass('context') ||
125 $(tr).hasClass('context') ||
126 $(_td).hasClass('no-comment')) {
126 $(_td).hasClass('no-comment')) {
127 return;
127 return;
128 }
128 }
129 $(tr).addClass('form-open');
129 $(tr).addClass('form-open');
130 $(tr).addClass('hl-comment');
130 $(tr).addClass('hl-comment');
131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
132 var f_path = $(node).attr('path');
132 var f_path = $(node).attr('path');
133 var lineno = getLineNo(tr);
133 var lineno = getLineNo(tr);
134 var form = createInlineForm(tr, f_path, lineno);
134 var form = createInlineForm(tr, f_path, lineno);
135
135
136 var parent = tr;
136 var parent = tr;
137 while (1) {
137 while (1) {
138 var n = parent.nextElementSibling;
138 var n = parent.nextElementSibling;
139 // next element are comments !
139 // next element are comments !
140 if ($(n).hasClass('inline-comments')) {
140 if ($(n).hasClass('inline-comments')) {
141 parent = n;
141 parent = n;
142 }
142 }
143 else {
143 else {
144 break;
144 break;
145 }
145 }
146 }
146 }
147 var _parent = $(parent).get(0);
147 var _parent = $(parent).get(0);
148 $(_parent).after(form);
148 $(_parent).after(form);
149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
150 var f = $(form).get(0);
150 var f = $(form).get(0);
151
151
152 var _form = $(f).find('.inline-form').get(0);
152 var _form = $(f).find('.inline-form').get(0);
153
153
154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
155 var commitId = templateContext.commit_data.commit_id;
155 var commitId = templateContext.commit_data.commit_id;
156
156
157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
158 var cm = commentForm.getCmInstance();
158 var cm = commentForm.getCmInstance();
159
159
160 // set a CUSTOM submit handler for inline comments.
160 // set a CUSTOM submit handler for inline comments.
161 commentForm.setHandleFormSubmit(function(o) {
161 commentForm.setHandleFormSubmit(function(o) {
162 var text = commentForm.cm.getValue();
162 var text = commentForm.cm.getValue();
163
163
164 if (text === "") {
164 if (text === "") {
165 return;
165 return;
166 }
166 }
167
167
168 if (lineno === undefined) {
168 if (lineno === undefined) {
169 alert('missing line !');
169 alert('missing line !');
170 return;
170 return;
171 }
171 }
172 if (f_path === undefined) {
172 if (f_path === undefined) {
173 alert('missing file path !');
173 alert('missing file path !');
174 return;
174 return;
175 }
175 }
176
176
177 var excludeCancelBtn = false;
177 var excludeCancelBtn = false;
178 var submitEvent = true;
178 var submitEvent = true;
179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
180 commentForm.cm.setOption("readOnly", true);
180 commentForm.cm.setOption("readOnly", true);
181 var postData = {
181 var postData = {
182 'text': text,
182 'text': text,
183 'f_path': f_path,
183 'f_path': f_path,
184 'line': lineno,
184 'line': lineno,
185 'csrf_token': CSRF_TOKEN
185 'csrf_token': CSRF_TOKEN
186 };
186 };
187 var submitSuccessCallback = function(o) {
187 var submitSuccessCallback = function(o) {
188 $(tr).removeClass('form-open');
188 $(tr).removeClass('form-open');
189 removeInlineForm(f);
189 removeInlineForm(f);
190 renderInlineComment(o);
190 renderInlineComment(o);
191 $('.inline-comments').removeClass('hide-comment-button');
191 $('.inline-comments').removeClass('hide-comment-button');
192
192
193 // re trigger the linkification of next/prev navigation
193 // re trigger the linkification of next/prev navigation
194 linkifyComments($('.inline-comment-injected'));
194 linkifyComments($('.inline-comment-injected'));
195 timeagoActivate();
195 timeagoActivate();
196 bindDeleteCommentButtons();
196 bindDeleteCommentButtons();
197 commentForm.setActionButtonsDisabled(false);
197 commentForm.setActionButtonsDisabled(false);
198
198
199 };
199 };
200 var submitFailCallback = function(){
200 var submitFailCallback = function(){
201 commentForm.resetCommentFormState(text)
201 commentForm.resetCommentFormState(text)
202 };
202 };
203 commentForm.submitAjaxPOST(
203 commentForm.submitAjaxPOST(
204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
205 });
205 });
206
206
207 setTimeout(function() {
207 setTimeout(function() {
208 // callbacks
208 // callbacks
209 if (cm !== undefined) {
209 if (cm !== undefined) {
210 cm.focus();
210 cm.focus();
211 }
211 }
212 }, 10);
212 }, 10);
213
213
214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
215 form:_form,
215 form:_form,
216 parent:_parent,
216 parent:_parent,
217 lineno: lineno,
217 lineno: lineno,
218 f_path: f_path}
218 f_path: f_path}
219 );
219 );
220 };
220 };
221
221
222 var deleteComment = function(comment_id) {
222 var deleteComment = function(comment_id) {
223 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
223 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
224 var postData = {
224 var postData = {
225 '_method': 'delete',
225 '_method': 'delete',
226 'csrf_token': CSRF_TOKEN
226 'csrf_token': CSRF_TOKEN
227 };
227 };
228
228
229 var success = function(o) {
229 var success = function(o) {
230 window.location.reload();
230 window.location.reload();
231 };
231 };
232 ajaxPOST(url, postData, success);
232 ajaxPOST(url, postData, success);
233 };
233 };
234
234
235 var createInlineAddButton = function(tr){
235 var createInlineAddButton = function(tr){
236 var label = _gettext('Add another comment');
236 var label = _gettext('Add another comment');
237 var html_el = document.createElement('div');
237 var html_el = document.createElement('div');
238 $(html_el).addClass('add-comment');
238 $(html_el).addClass('add-comment');
239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
240 var add = new $(html_el);
240 var add = new $(html_el);
241 add.on('click', function(e) {
241 add.on('click', function(e) {
242 injectInlineForm(tr);
242 injectInlineForm(tr);
243 });
243 });
244 return add;
244 return add;
245 };
245 };
246
246
247 var placeAddButton = function(target_tr){
247 var placeAddButton = function(target_tr){
248 if(!target_tr){
248 if(!target_tr){
249 return;
249 return;
250 }
250 }
251 var last_node = target_tr;
251 var last_node = target_tr;
252 // scan
252 // scan
253 while (1){
253 while (1){
254 var n = last_node.nextElementSibling;
254 var n = last_node.nextElementSibling;
255 // next element are comments !
255 // next element are comments !
256 if($(n).hasClass('inline-comments')){
256 if($(n).hasClass('inline-comments')){
257 last_node = n;
257 last_node = n;
258 // also remove the comment button from previous
258 // also remove the comment button from previous
259 var comment_add_buttons = $(last_node).find('.add-comment');
259 var comment_add_buttons = $(last_node).find('.add-comment');
260 for(var i=0; i<comment_add_buttons.length; i++){
260 for(var i=0; i<comment_add_buttons.length; i++){
261 var b = comment_add_buttons[i];
261 var b = comment_add_buttons[i];
262 b.parentNode.removeChild(b);
262 b.parentNode.removeChild(b);
263 }
263 }
264 }
264 }
265 else{
265 else{
266 break;
266 break;
267 }
267 }
268 }
268 }
269 var add = createInlineAddButton(target_tr);
269 var add = createInlineAddButton(target_tr);
270 // get the comment div
270 // get the comment div
271 var comment_block = $(last_node).find('.comment')[0];
271 var comment_block = $(last_node).find('.comment')[0];
272 // attach add button
272 // attach add button
273 $(add).insertAfter(comment_block);
273 $(add).insertAfter(comment_block);
274 };
274 };
275
275
276 /**
276 /**
277 * Places the inline comment into the changeset block in proper line position
277 * Places the inline comment into the changeset block in proper line position
278 */
278 */
279 var placeInline = function(target_container, lineno, html, show_add_button) {
279 var placeInline = function(target_container, lineno, html, show_add_button) {
280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
281
281
282 var lineid = "{0}_{1}".format(target_container, lineno);
282 var lineid = "{0}_{1}".format(target_container, lineno);
283 var target_line = $('#' + lineid).get(0);
283 var target_line = $('#' + lineid).get(0);
284 var comment = new $(tableTr('inline-comments', html));
284 var comment = new $(tableTr('inline-comments', html));
285 // check if there are comments already !
285 // check if there are comments already !
286 if (target_line) {
286 if (target_line) {
287 var parent_node = target_line.parentNode;
287 var parent_node = target_line.parentNode;
288 var root_parent = parent_node;
288 var root_parent = parent_node;
289
289
290 while (1) {
290 while (1) {
291 var n = parent_node.nextElementSibling;
291 var n = parent_node.nextElementSibling;
292 // next element are comments !
292 // next element are comments !
293 if ($(n).hasClass('inline-comments')) {
293 if ($(n).hasClass('inline-comments')) {
294 parent_node = n;
294 parent_node = n;
295 }
295 }
296 else {
296 else {
297 break;
297 break;
298 }
298 }
299 }
299 }
300 // put in the comment at the bottom
300 // put in the comment at the bottom
301 $(comment).insertAfter(parent_node);
301 $(comment).insertAfter(parent_node);
302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
303 // scan nodes, and attach add button to last one
303 // scan nodes, and attach add button to last one
304 if (show_add_button) {
304 if (show_add_button) {
305 placeAddButton(root_parent);
305 placeAddButton(root_parent);
306 }
306 }
307 addCommentToggle(target_line);
307 addCommentToggle(target_line);
308 }
308 }
309
309
310 return target_line;
310 return target_line;
311 };
311 };
312
312
313 var addCommentToggle = function(target_line) {
313 var addCommentToggle = function(target_line) {
314 // exposes comment toggle button
314 // exposes comment toggle button
315 $(target_line).siblings('.comment-toggle').addClass('active');
315 $(target_line).siblings('.comment-toggle').addClass('active');
316 return;
316 return;
317 };
317 };
318
318
319 var bindToggleButtons = function() {
319 var bindToggleButtons = function() {
320 $('.comment-toggle').on('click', function() {
320 $('.comment-toggle').on('click', function() {
321 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
321 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
322 });
322 });
323 };
323 };
324
324
325 var linkifyComments = function(comments) {
325 var linkifyComments = function(comments) {
326
326 /* TODO: dan: remove this - it should no longer needed */
327 for (var i = 0; i < comments.length; i++) {
327 for (var i = 0; i < comments.length; i++) {
328 var comment_id = $(comments[i]).data('comment-id');
328 var comment_id = $(comments[i]).data('comment-id');
329 var prev_comment_id = $(comments[i - 1]).data('comment-id');
329 var prev_comment_id = $(comments[i - 1]).data('comment-id');
330 var next_comment_id = $(comments[i + 1]).data('comment-id');
330 var next_comment_id = $(comments[i + 1]).data('comment-id');
331
331
332 // place next/prev links
332 // place next/prev links
333 if (prev_comment_id) {
333 if (prev_comment_id) {
334 $('#prev_c_' + comment_id).show();
334 $('#prev_c_' + comment_id).show();
335 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
335 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
336 'href', '#comment-' + prev_comment_id).removeClass('disabled');
336 'href', '#comment-' + prev_comment_id).removeClass('disabled');
337 }
337 }
338 if (next_comment_id) {
338 if (next_comment_id) {
339 $('#next_c_' + comment_id).show();
339 $('#next_c_' + comment_id).show();
340 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
340 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
341 'href', '#comment-' + next_comment_id).removeClass('disabled');
341 'href', '#comment-' + next_comment_id).removeClass('disabled');
342 }
342 }
343 // place a first link to the total counter
343 // place a first link to the total counter
344 if (i === 0) {
344 if (i === 0) {
345 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
345 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
346 }
346 }
347 }
347 }
348
348
349 };
349 };
350
350
351 /**
351 /**
352 * Iterates over all the inlines, and places them inside proper blocks of data
352 * Iterates over all the inlines, and places them inside proper blocks of data
353 */
353 */
354 var renderInlineComments = function(file_comments, show_add_button) {
354 var renderInlineComments = function(file_comments, show_add_button) {
355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
356
356
357 for (var i = 0; i < file_comments.length; i++) {
357 for (var i = 0; i < file_comments.length; i++) {
358 var box = file_comments[i];
358 var box = file_comments[i];
359
359
360 var target_id = $(box).attr('target_id');
360 var target_id = $(box).attr('target_id');
361
361
362 // actually comments with line numbers
362 // actually comments with line numbers
363 var comments = box.children;
363 var comments = box.children;
364
364
365 for (var j = 0; j < comments.length; j++) {
365 for (var j = 0; j < comments.length; j++) {
366 var data = {
366 var data = {
367 'rendered_text': comments[j].outerHTML,
367 'rendered_text': comments[j].outerHTML,
368 'line_no': $(comments[j]).attr('line'),
368 'line_no': $(comments[j]).attr('line'),
369 'target_id': target_id
369 'target_id': target_id
370 };
370 };
371 renderInlineComment(data, show_add_button);
371 renderInlineComment(data, show_add_button);
372 }
372 }
373 }
373 }
374
374
375 // since order of injection is random, we're now re-iterating
375 // since order of injection is random, we're now re-iterating
376 // from correct order and filling in links
376 // from correct order and filling in links
377 linkifyComments($('.inline-comment-injected'));
377 linkifyComments($('.inline-comment-injected'));
378 bindDeleteCommentButtons();
378 bindDeleteCommentButtons();
379 firefoxAnchorFix();
379 firefoxAnchorFix();
380 };
380 };
381
381
382
382
383 /* Comment form for main and inline comments */
383 /* Comment form for main and inline comments */
384 var CommentForm = (function() {
384 var CommentForm = (function() {
385 "use strict";
385 "use strict";
386
386
387 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
387 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
388
388
389 this.withLineNo = function(selector) {
389 this.withLineNo = function(selector) {
390 var lineNo = this.lineNo;
390 var lineNo = this.lineNo;
391 if (lineNo === undefined) {
391 if (lineNo === undefined) {
392 return selector
392 return selector
393 } else {
393 } else {
394 return selector + '_' + lineNo;
394 return selector + '_' + lineNo;
395 }
395 }
396 };
396 };
397
397
398 this.commitId = commitId;
398 this.commitId = commitId;
399 this.pullRequestId = pullRequestId;
399 this.pullRequestId = pullRequestId;
400 this.lineNo = lineNo;
400 this.lineNo = lineNo;
401 this.initAutocompleteActions = initAutocompleteActions;
401 this.initAutocompleteActions = initAutocompleteActions;
402
402
403 this.previewButton = this.withLineNo('#preview-btn');
403 this.previewButton = this.withLineNo('#preview-btn');
404 this.previewContainer = this.withLineNo('#preview-container');
404 this.previewContainer = this.withLineNo('#preview-container');
405
405
406 this.previewBoxSelector = this.withLineNo('#preview-box');
406 this.previewBoxSelector = this.withLineNo('#preview-box');
407
407
408 this.editButton = this.withLineNo('#edit-btn');
408 this.editButton = this.withLineNo('#edit-btn');
409 this.editContainer = this.withLineNo('#edit-container');
409 this.editContainer = this.withLineNo('#edit-container');
410
410
411 this.cancelButton = this.withLineNo('#cancel-btn');
411 this.cancelButton = this.withLineNo('#cancel-btn');
412
412
413 this.statusChange = '#change_status';
413 this.statusChange = '#change_status';
414 this.cmBox = this.withLineNo('#text');
414 this.cmBox = this.withLineNo('#text');
415 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
415 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
416
416
417 this.submitForm = formElement;
417 this.submitForm = formElement;
418 this.submitButton = $(this.submitForm).find('input[type="submit"]');
418 this.submitButton = $(this.submitForm).find('input[type="submit"]');
419 this.submitButtonText = this.submitButton.val();
419 this.submitButtonText = this.submitButton.val();
420
420
421 this.previewUrl = pyroutes.url('changeset_comment_preview',
421 this.previewUrl = pyroutes.url('changeset_comment_preview',
422 {'repo_name': templateContext.repo_name});
422 {'repo_name': templateContext.repo_name});
423
423
424 // based on commitId, or pullReuqestId decide where do we submit
424 // based on commitId, or pullReuqestId decide where do we submit
425 // out data
425 // out data
426 if (this.commitId){
426 if (this.commitId){
427 this.submitUrl = pyroutes.url('changeset_comment',
427 this.submitUrl = pyroutes.url('changeset_comment',
428 {'repo_name': templateContext.repo_name,
428 {'repo_name': templateContext.repo_name,
429 'revision': this.commitId});
429 'revision': this.commitId});
430
430
431 } else if (this.pullRequestId) {
431 } else if (this.pullRequestId) {
432 this.submitUrl = pyroutes.url('pullrequest_comment',
432 this.submitUrl = pyroutes.url('pullrequest_comment',
433 {'repo_name': templateContext.repo_name,
433 {'repo_name': templateContext.repo_name,
434 'pull_request_id': this.pullRequestId});
434 'pull_request_id': this.pullRequestId});
435
435
436 } else {
436 } else {
437 throw new Error(
437 throw new Error(
438 'CommentForm requires pullRequestId, or commitId to be specified.')
438 'CommentForm requires pullRequestId, or commitId to be specified.')
439 }
439 }
440
440
441 this.getCmInstance = function(){
441 this.getCmInstance = function(){
442 return this.cm
442 return this.cm
443 };
443 };
444
444
445 var self = this;
445 var self = this;
446
446
447 this.getCommentStatus = function() {
447 this.getCommentStatus = function() {
448 return $(this.submitForm).find(this.statusChange).val();
448 return $(this.submitForm).find(this.statusChange).val();
449 };
449 };
450
450
451 this.isAllowedToSubmit = function() {
451 this.isAllowedToSubmit = function() {
452 return !$(this.submitButton).prop('disabled');
452 return !$(this.submitButton).prop('disabled');
453 };
453 };
454
454
455 this.initStatusChangeSelector = function(){
455 this.initStatusChangeSelector = function(){
456 var formatChangeStatus = function(state, escapeMarkup) {
456 var formatChangeStatus = function(state, escapeMarkup) {
457 var originalOption = state.element;
457 var originalOption = state.element;
458 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
458 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
459 '<span>' + escapeMarkup(state.text) + '</span>';
459 '<span>' + escapeMarkup(state.text) + '</span>';
460 };
460 };
461 var formatResult = function(result, container, query, escapeMarkup) {
461 var formatResult = function(result, container, query, escapeMarkup) {
462 return formatChangeStatus(result, escapeMarkup);
462 return formatChangeStatus(result, escapeMarkup);
463 };
463 };
464
464
465 var formatSelection = function(data, container, escapeMarkup) {
465 var formatSelection = function(data, container, escapeMarkup) {
466 return formatChangeStatus(data, escapeMarkup);
466 return formatChangeStatus(data, escapeMarkup);
467 };
467 };
468
468
469 $(this.submitForm).find(this.statusChange).select2({
469 $(this.submitForm).find(this.statusChange).select2({
470 placeholder: _gettext('Status Review'),
470 placeholder: _gettext('Status Review'),
471 formatResult: formatResult,
471 formatResult: formatResult,
472 formatSelection: formatSelection,
472 formatSelection: formatSelection,
473 containerCssClass: "drop-menu status_box_menu",
473 containerCssClass: "drop-menu status_box_menu",
474 dropdownCssClass: "drop-menu-dropdown",
474 dropdownCssClass: "drop-menu-dropdown",
475 dropdownAutoWidth: true,
475 dropdownAutoWidth: true,
476 minimumResultsForSearch: -1
476 minimumResultsForSearch: -1
477 });
477 });
478 $(this.submitForm).find(this.statusChange).on('change', function() {
478 $(this.submitForm).find(this.statusChange).on('change', function() {
479 var status = self.getCommentStatus();
479 var status = self.getCommentStatus();
480 if (status && !self.lineNo) {
480 if (status && !self.lineNo) {
481 $(self.submitButton).prop('disabled', false);
481 $(self.submitButton).prop('disabled', false);
482 }
482 }
483 //todo, fix this name
483 //todo, fix this name
484 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
484 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
485 self.cm.setOption('placeholder', placeholderText);
485 self.cm.setOption('placeholder', placeholderText);
486 })
486 })
487 };
487 };
488
488
489 // reset the comment form into it's original state
489 // reset the comment form into it's original state
490 this.resetCommentFormState = function(content) {
490 this.resetCommentFormState = function(content) {
491 content = content || '';
491 content = content || '';
492
492
493 $(this.editContainer).show();
493 $(this.editContainer).show();
494 $(this.editButton).hide();
494 $(this.editButton).hide();
495
495
496 $(this.previewContainer).hide();
496 $(this.previewContainer).hide();
497 $(this.previewButton).show();
497 $(this.previewButton).show();
498
498
499 this.setActionButtonsDisabled(true);
499 this.setActionButtonsDisabled(true);
500 self.cm.setValue(content);
500 self.cm.setValue(content);
501 self.cm.setOption("readOnly", false);
501 self.cm.setOption("readOnly", false);
502 };
502 };
503
503
504 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
504 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
505 failHandler = failHandler || function() {};
505 failHandler = failHandler || function() {};
506 var postData = toQueryString(postData);
506 var postData = toQueryString(postData);
507 var request = $.ajax({
507 var request = $.ajax({
508 url: url,
508 url: url,
509 type: 'POST',
509 type: 'POST',
510 data: postData,
510 data: postData,
511 headers: {'X-PARTIAL-XHR': true}
511 headers: {'X-PARTIAL-XHR': true}
512 })
512 })
513 .done(function(data) {
513 .done(function(data) {
514 successHandler(data);
514 successHandler(data);
515 })
515 })
516 .fail(function(data, textStatus, errorThrown){
516 .fail(function(data, textStatus, errorThrown){
517 alert(
517 alert(
518 "Error while submitting comment.\n" +
518 "Error while submitting comment.\n" +
519 "Error code {0} ({1}).".format(data.status, data.statusText));
519 "Error code {0} ({1}).".format(data.status, data.statusText));
520 failHandler()
520 failHandler()
521 });
521 });
522 return request;
522 return request;
523 };
523 };
524
524
525 // overwrite a submitHandler, we need to do it for inline comments
525 // overwrite a submitHandler, we need to do it for inline comments
526 this.setHandleFormSubmit = function(callback) {
526 this.setHandleFormSubmit = function(callback) {
527 this.handleFormSubmit = callback;
527 this.handleFormSubmit = callback;
528 };
528 };
529
529
530 // default handler for for submit for main comments
530 // default handler for for submit for main comments
531 this.handleFormSubmit = function() {
531 this.handleFormSubmit = function() {
532 var text = self.cm.getValue();
532 var text = self.cm.getValue();
533 var status = self.getCommentStatus();
533 var status = self.getCommentStatus();
534
534
535 if (text === "" && !status) {
535 if (text === "" && !status) {
536 return;
536 return;
537 }
537 }
538
538
539 var excludeCancelBtn = false;
539 var excludeCancelBtn = false;
540 var submitEvent = true;
540 var submitEvent = true;
541 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
541 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
542 self.cm.setOption("readOnly", true);
542 self.cm.setOption("readOnly", true);
543 var postData = {
543 var postData = {
544 'text': text,
544 'text': text,
545 'changeset_status': status,
545 'changeset_status': status,
546 'csrf_token': CSRF_TOKEN
546 'csrf_token': CSRF_TOKEN
547 };
547 };
548
548
549 var submitSuccessCallback = function(o) {
549 var submitSuccessCallback = function(o) {
550 if (status) {
550 if (status) {
551 location.reload(true);
551 location.reload(true);
552 } else {
552 } else {
553 $('#injected_page_comments').append(o.rendered_text);
553 $('#injected_page_comments').append(o.rendered_text);
554 self.resetCommentFormState();
554 self.resetCommentFormState();
555 bindDeleteCommentButtons();
555 bindDeleteCommentButtons();
556 timeagoActivate();
556 timeagoActivate();
557 }
557 }
558 };
558 };
559 var submitFailCallback = function(){
559 var submitFailCallback = function(){
560 self.resetCommentFormState(text)
560 self.resetCommentFormState(text)
561 };
561 };
562 self.submitAjaxPOST(
562 self.submitAjaxPOST(
563 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
563 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
564 };
564 };
565
565
566 this.previewSuccessCallback = function(o) {
566 this.previewSuccessCallback = function(o) {
567 $(self.previewBoxSelector).html(o);
567 $(self.previewBoxSelector).html(o);
568 $(self.previewBoxSelector).removeClass('unloaded');
568 $(self.previewBoxSelector).removeClass('unloaded');
569
569
570 // swap buttons
570 // swap buttons
571 $(self.previewButton).hide();
571 $(self.previewButton).hide();
572 $(self.editButton).show();
572 $(self.editButton).show();
573
573
574 // unlock buttons
574 // unlock buttons
575 self.setActionButtonsDisabled(false);
575 self.setActionButtonsDisabled(false);
576 };
576 };
577
577
578 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
578 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
579 excludeCancelBtn = excludeCancelBtn || false;
579 excludeCancelBtn = excludeCancelBtn || false;
580 submitEvent = submitEvent || false;
580 submitEvent = submitEvent || false;
581
581
582 $(this.editButton).prop('disabled', state);
582 $(this.editButton).prop('disabled', state);
583 $(this.previewButton).prop('disabled', state);
583 $(this.previewButton).prop('disabled', state);
584
584
585 if (!excludeCancelBtn) {
585 if (!excludeCancelBtn) {
586 $(this.cancelButton).prop('disabled', state);
586 $(this.cancelButton).prop('disabled', state);
587 }
587 }
588
588
589 var submitState = state;
589 var submitState = state;
590 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
590 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
591 // if the value of commit review status is set, we allow
591 // if the value of commit review status is set, we allow
592 // submit button, but only on Main form, lineNo means inline
592 // submit button, but only on Main form, lineNo means inline
593 submitState = false
593 submitState = false
594 }
594 }
595 $(this.submitButton).prop('disabled', submitState);
595 $(this.submitButton).prop('disabled', submitState);
596 if (submitEvent) {
596 if (submitEvent) {
597 $(this.submitButton).val(_gettext('Submitting...'));
597 $(this.submitButton).val(_gettext('Submitting...'));
598 } else {
598 } else {
599 $(this.submitButton).val(this.submitButtonText);
599 $(this.submitButton).val(this.submitButtonText);
600 }
600 }
601
601
602 };
602 };
603
603
604 // lock preview/edit/submit buttons on load, but exclude cancel button
604 // lock preview/edit/submit buttons on load, but exclude cancel button
605 var excludeCancelBtn = true;
605 var excludeCancelBtn = true;
606 this.setActionButtonsDisabled(true, excludeCancelBtn);
606 this.setActionButtonsDisabled(true, excludeCancelBtn);
607
607
608 // anonymous users don't have access to initialized CM instance
608 // anonymous users don't have access to initialized CM instance
609 if (this.cm !== undefined){
609 if (this.cm !== undefined){
610 this.cm.on('change', function(cMirror) {
610 this.cm.on('change', function(cMirror) {
611 if (cMirror.getValue() === "") {
611 if (cMirror.getValue() === "") {
612 self.setActionButtonsDisabled(true, excludeCancelBtn)
612 self.setActionButtonsDisabled(true, excludeCancelBtn)
613 } else {
613 } else {
614 self.setActionButtonsDisabled(false, excludeCancelBtn)
614 self.setActionButtonsDisabled(false, excludeCancelBtn)
615 }
615 }
616 });
616 });
617 }
617 }
618
618
619 $(this.editButton).on('click', function(e) {
619 $(this.editButton).on('click', function(e) {
620 e.preventDefault();
620 e.preventDefault();
621
621
622 $(self.previewButton).show();
622 $(self.previewButton).show();
623 $(self.previewContainer).hide();
623 $(self.previewContainer).hide();
624 $(self.editButton).hide();
624 $(self.editButton).hide();
625 $(self.editContainer).show();
625 $(self.editContainer).show();
626
626
627 });
627 });
628
628
629 $(this.previewButton).on('click', function(e) {
629 $(this.previewButton).on('click', function(e) {
630 e.preventDefault();
630 e.preventDefault();
631 var text = self.cm.getValue();
631 var text = self.cm.getValue();
632
632
633 if (text === "") {
633 if (text === "") {
634 return;
634 return;
635 }
635 }
636
636
637 var postData = {
637 var postData = {
638 'text': text,
638 'text': text,
639 'renderer': DEFAULT_RENDERER,
639 'renderer': DEFAULT_RENDERER,
640 'csrf_token': CSRF_TOKEN
640 'csrf_token': CSRF_TOKEN
641 };
641 };
642
642
643 // lock ALL buttons on preview
643 // lock ALL buttons on preview
644 self.setActionButtonsDisabled(true);
644 self.setActionButtonsDisabled(true);
645
645
646 $(self.previewBoxSelector).addClass('unloaded');
646 $(self.previewBoxSelector).addClass('unloaded');
647 $(self.previewBoxSelector).html(_gettext('Loading ...'));
647 $(self.previewBoxSelector).html(_gettext('Loading ...'));
648 $(self.editContainer).hide();
648 $(self.editContainer).hide();
649 $(self.previewContainer).show();
649 $(self.previewContainer).show();
650
650
651 // by default we reset state of comment preserving the text
651 // by default we reset state of comment preserving the text
652 var previewFailCallback = function(){
652 var previewFailCallback = function(){
653 self.resetCommentFormState(text)
653 self.resetCommentFormState(text)
654 };
654 };
655 self.submitAjaxPOST(
655 self.submitAjaxPOST(
656 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
656 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
657
657
658 });
658 });
659
659
660 $(this.submitForm).submit(function(e) {
660 $(this.submitForm).submit(function(e) {
661 e.preventDefault();
661 e.preventDefault();
662 var allowedToSubmit = self.isAllowedToSubmit();
662 var allowedToSubmit = self.isAllowedToSubmit();
663 if (!allowedToSubmit){
663 if (!allowedToSubmit){
664 return false;
664 return false;
665 }
665 }
666 self.handleFormSubmit();
666 self.handleFormSubmit();
667 });
667 });
668
668
669 }
669 }
670
670
671 return CommentForm;
671 return CommentForm;
672 })();
672 })();
@@ -1,175 +1,387 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <!DOCTYPE html>
2 <!DOCTYPE html>
3
3
4 <%
4 <%
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6
6
7 if hasattr(c, 'rhodecode_db_repo'):
7 if hasattr(c, 'rhodecode_db_repo'):
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10
10
11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
11 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
12 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
13 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
14 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
15 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.name
15 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.name
16 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.lastname
16 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.lastname
17
17
18 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
18 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
19 %>
19 %>
20 <html xmlns="http://www.w3.org/1999/xhtml">
20 <html xmlns="http://www.w3.org/1999/xhtml">
21 <head>
21 <head>
22 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}"></script>
22 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.min.js', ver=c.rhodecode_version_hash)}"></script>
23 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
23 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
24 <title>${self.title()}</title>
24 <title>${self.title()}</title>
25 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
25 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
26 <%def name="robots()">
26 <%def name="robots()">
27 <meta name="robots" content="index, nofollow"/>
27 <meta name="robots" content="index, nofollow"/>
28 </%def>
28 </%def>
29 ${self.robots()}
29 ${self.robots()}
30 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
30 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
31
31
32 ## CSS definitions
32 ## CSS definitions
33 <%def name="css()">
33 <%def name="css()">
34 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
34 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
35 <!--[if lt IE 9]>
35 <!--[if lt IE 9]>
36 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
36 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
37 <![endif]-->
37 <![endif]-->
38 ## EXTRA FOR CSS
38 ## EXTRA FOR CSS
39 ${self.css_extra()}
39 ${self.css_extra()}
40 </%def>
40 </%def>
41 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
41 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
42 <%def name="css_extra()">
42 <%def name="css_extra()">
43 </%def>
43 </%def>
44
44
45 ${self.css()}
45 ${self.css()}
46
46
47 ## JAVASCRIPT
47 ## JAVASCRIPT
48 <%def name="js()">
48 <%def name="js()">
49 <script>
49 <script>
50 // setup Polymer options
50 // setup Polymer options
51 window.Polymer = {lazyRegister: true, dom: 'shadow'};
51 window.Polymer = {lazyRegister: true, dom: 'shadow'};
52
52
53 // Load webcomponentsjs polyfill if browser does not support native Web Components
53 // Load webcomponentsjs polyfill if browser does not support native Web Components
54 (function() {
54 (function() {
55 'use strict';
55 'use strict';
56 var onload = function() {
56 var onload = function() {
57 // For native Imports, manually fire WebComponentsReady so user code
57 // For native Imports, manually fire WebComponentsReady so user code
58 // can use the same code path for native and polyfill'd imports.
58 // can use the same code path for native and polyfill'd imports.
59 if (!window.HTMLImports) {
59 if (!window.HTMLImports) {
60 document.dispatchEvent(
60 document.dispatchEvent(
61 new CustomEvent('WebComponentsReady', {bubbles: true})
61 new CustomEvent('WebComponentsReady', {bubbles: true})
62 );
62 );
63 }
63 }
64 };
64 };
65 var webComponentsSupported = (
65 var webComponentsSupported = (
66 'registerElement' in document
66 'registerElement' in document
67 && 'import' in document.createElement('link')
67 && 'import' in document.createElement('link')
68 && 'content' in document.createElement('template')
68 && 'content' in document.createElement('template')
69 );
69 );
70 if (!webComponentsSupported) {
70 if (!webComponentsSupported) {
71 } else {
71 } else {
72 onload();
72 onload();
73 }
73 }
74 })();
74 })();
75 </script>
75 </script>
76
76
77 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
77 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
78 <script type="text/javascript">
78 <script type="text/javascript">
79 // register templateContext to pass template variables to JS
79 // register templateContext to pass template variables to JS
80 var templateContext = ${h.json.dumps(c.template_context)|n};
80 var templateContext = ${h.json.dumps(c.template_context)|n};
81
81
82 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
82 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
83 %if hasattr(c, 'rhodecode_db_repo'):
83 %if hasattr(c, 'rhodecode_db_repo'):
84 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
84 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
85 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
85 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
86 %else:
86 %else:
87 var REPO_LANDING_REV = '';
87 var REPO_LANDING_REV = '';
88 var REPO_TYPE = '';
88 var REPO_TYPE = '';
89 %endif
89 %endif
90 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
90 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
91 var ASSET_URL = "${h.asset('')}";
91 var ASSET_URL = "${h.asset('')}";
92 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
92 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
93 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
93 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
94 % if getattr(c, 'rhodecode_user', None):
94 % if getattr(c, 'rhodecode_user', None):
95 var USER = {name:'${c.rhodecode_user.username}'};
95 var USER = {name:'${c.rhodecode_user.username}'};
96 % else:
96 % else:
97 var USER = {name:null};
97 var USER = {name:null};
98 % endif
98 % endif
99
99
100 var APPENLIGHT = {
100 var APPENLIGHT = {
101 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
101 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
102 key: '${getattr(c, "appenlight_api_public_key", "")}',
102 key: '${getattr(c, "appenlight_api_public_key", "")}',
103 % if getattr(c, 'appenlight_server_url', None):
103 % if getattr(c, 'appenlight_server_url', None):
104 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
104 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
105 % endif
105 % endif
106 requestInfo: {
106 requestInfo: {
107 % if getattr(c, 'rhodecode_user', None):
107 % if getattr(c, 'rhodecode_user', None):
108 ip: '${c.rhodecode_user.ip_addr}',
108 ip: '${c.rhodecode_user.ip_addr}',
109 username: '${c.rhodecode_user.username}'
109 username: '${c.rhodecode_user.username}'
110 % endif
110 % endif
111 },
111 },
112 tags: {
112 tags: {
113 rhodecode_version: '${c.rhodecode_version}',
113 rhodecode_version: '${c.rhodecode_version}',
114 rhodecode_edition: '${c.rhodecode_edition}'
114 rhodecode_edition: '${c.rhodecode_edition}'
115 }
115 }
116 };
116 };
117
118
119 Rhodecode = (function() {
120 function _Rhodecode() {
121 this.comments = new (function() { /* comments controller */
122 var self = this;
123
124 this.cancelComment = function(node) {
125 var $node = $(node);
126 var $td = $node.closest('td');
127 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
128 return false;
129 }
130 this.getLineNumber = function(node) {
131 var $node = $(node);
132 return $node.closest('td').attr('data-line-number');
133 }
134 this.scrollToComment = function(node, offset) {
135 if (!node) {
136 node = $('.comment-selected');
137 if (!node.length) {
138 node = $('comment-current')
139 }
140 }
141 $comment = $(node).closest('.comment-current');
142 $comments = $('.comment-current');
143
144 $('.comment-selected').removeClass('comment-selected');
145
146 var nextIdx = $('.comment-current').index($comment) + offset;
147 if (nextIdx >= $comments.length) {
148 nextIdx = 0;
149 }
150 var $next = $('.comment-current').eq(nextIdx);
151 var $cb = $next.closest('.cb');
152 $cb.removeClass('cb-collapsed')
153
154 var $filediffCollapseState = $cb.closest('.filediff').prev();
155 $filediffCollapseState.prop('checked', false);
156 $next.addClass('comment-selected');
157 scrollToElement($next);
158 return false;
159 }
160 this.nextComment = function(node) {
161 return self.scrollToComment(node, 1);
162 }
163 this.prevComment = function(node) {
164 return self.scrollToComment(node, -1);
165 }
166 this.deleteComment = function(node) {
167 if (!confirm(_gettext('Delete this comment?'))) {
168 return false;
169 }
170 var $node = $(node);
171 var $td = $node.closest('td');
172 var $comment = $node.closest('.comment');
173 var comment_id = $comment.attr('data-comment-id');
174 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
175 var postData = {
176 '_method': 'delete',
177 'csrf_token': CSRF_TOKEN
178 };
179
180 $comment.addClass('comment-deleting');
181 $comment.hide('fast');
182
183 var success = function(response) {
184 $comment.remove();
185 return false;
186 };
187 var failure = function(data, textStatus, xhr) {
188 alert("error processing request: " + textStatus);
189 $comment.show('fast');
190 $comment.removeClass('comment-deleting');
191 return false;
192 };
193 ajaxPOST(url, postData, success, failure);
194 }
195 this.createComment = function(node) {
196 var $node = $(node);
197 var $td = $node.closest('td');
198 var $form = $td.find('.comment-inline-form');
199
200 if (!$form.length) {
201 var tmpl = $('#cb-comment-inline-form-template').html();
202 var f_path = $node.closest('.filediff').attr('data-f-path');
203 var lineno = self.getLineNumber(node);
204 tmpl = tmpl.format(f_path, lineno);
205 $form = $(tmpl);
206
207 var $comments = $td.find('.inline-comments');
208 if (!$comments.length) {
209 $comments = $(
210 $('#cb-comments-inline-container-template').html());
211 $td.append($comments);
212 }
213
214 $td.find('.cb-comment-add-button').before($form);
215
216 var pullRequestId = templateContext.pull_request_data.pull_request_id;
217 var commitId = templateContext.commit_data.commit_id;
218 var _form = $form[0];
219 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
220 var cm = commentForm.getCmInstance();
221
222 // set a CUSTOM submit handler for inline comments.
223 commentForm.setHandleFormSubmit(function(o) {
224 var text = commentForm.cm.getValue();
225
226 if (text === "") {
227 return;
228 }
229
230 if (lineno === undefined) {
231 alert('missing line !');
232 return;
233 }
234 if (f_path === undefined) {
235 alert('missing file path !');
236 return;
237 }
238
239 var excludeCancelBtn = false;
240 var submitEvent = true;
241 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
242 commentForm.cm.setOption("readOnly", true);
243 var postData = {
244 'text': text,
245 'f_path': f_path,
246 'line': lineno,
247 'csrf_token': CSRF_TOKEN
248 };
249 var submitSuccessCallback = function(json_data) {
250 $form.remove();
251 console.log(json_data)
252 try {
253 var html = json_data.rendered_text;
254 var lineno = json_data.line_no;
255 var target_id = json_data.target_id;
256
257 $comments.find('.cb-comment-add-button').before(html);
258 console.log(lineno, target_id, $comments);
259
260 } catch (e) {
261 console.error(e);
262 }
263
264
265 // re trigger the linkification of next/prev navigation
266 linkifyComments($('.inline-comment-injected'));
267 timeagoActivate();
268 bindDeleteCommentButtons();
269 commentForm.setActionButtonsDisabled(false);
270
271 };
272 var submitFailCallback = function(){
273 commentForm.resetCommentFormState(text)
274 };
275 commentForm.submitAjaxPOST(
276 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
277 });
278
279 setTimeout(function() {
280 // callbacks
281 if (cm !== undefined) {
282 cm.focus();
283 }
284 }, 10);
285
286 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
287 form: _form,
288 parent: $td[0],
289 lineno: lineno,
290 f_path: f_path}
291 );
292 }
293
294 $form.addClass('comment-inline-form-open');
295 }
296
297 this.renderInlineComments = function(file_comments) {
298 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
299
300 for (var i = 0; i < file_comments.length; i++) {
301 var box = file_comments[i];
302
303 var target_id = $(box).attr('target_id');
304
305 // actually comments with line numbers
306 var comments = box.children;
307
308 for (var j = 0; j < comments.length; j++) {
309 var data = {
310 'rendered_text': comments[j].outerHTML,
311 'line_no': $(comments[j]).attr('line'),
312 'target_id': target_id
313 };
314 }
315 }
316
317 // since order of injection is random, we're now re-iterating
318 // from correct order and filling in links
319 linkifyComments($('.inline-comment-injected'));
320 bindDeleteCommentButtons();
321 firefoxAnchorFix();
322 };
323
324 })();
325 }
326 return new _Rhodecode();
327 })();
328
117 </script>
329 </script>
118 <%include file="/base/plugins_base.html"/>
330 <%include file="/base/plugins_base.html"/>
119 <!--[if lt IE 9]>
331 <!--[if lt IE 9]>
120 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
332 <script language="javascript" type="text/javascript" src="${h.asset('js/excanvas.min.js')}"></script>
121 <![endif]-->
333 <![endif]-->
122 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
334 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
123 <script> var alertMessagePayloads = ${h.flash.json_alerts()|n}; </script>
335 <script> var alertMessagePayloads = ${h.flash.json_alerts()|n}; </script>
124 ## avoide escaping the %N
336 ## avoide escaping the %N
125 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}"></script>
337 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode-components.js', ver=c.rhodecode_version_hash)}"></script>
126 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
338 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
127
339
128
340
129 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
341 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
130 ${self.js_extra()}
342 ${self.js_extra()}
131
343
132 <script type="text/javascript">
344 <script type="text/javascript">
133 $(document).ready(function(){
345 $(document).ready(function(){
134 show_more_event();
346 show_more_event();
135 timeagoActivate();
347 timeagoActivate();
136 })
348 })
137 </script>
349 </script>
138
350
139 </%def>
351 </%def>
140
352
141 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
353 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
142 <%def name="js_extra()"></%def>
354 <%def name="js_extra()"></%def>
143 ${self.js()}
355 ${self.js()}
144
356
145 <%def name="head_extra()"></%def>
357 <%def name="head_extra()"></%def>
146 ${self.head_extra()}
358 ${self.head_extra()}
147 ## extra stuff
359 ## extra stuff
148 %if c.pre_code:
360 %if c.pre_code:
149 ${c.pre_code|n}
361 ${c.pre_code|n}
150 %endif
362 %endif
151 </head>
363 </head>
152 <body id="body">
364 <body id="body">
153 <noscript>
365 <noscript>
154 <div class="noscript-error">
366 <div class="noscript-error">
155 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
367 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
156 </div>
368 </div>
157 </noscript>
369 </noscript>
158 ## IE hacks
370 ## IE hacks
159 <!--[if IE 7]>
371 <!--[if IE 7]>
160 <script>$(document.body).addClass('ie7')</script>
372 <script>$(document.body).addClass('ie7')</script>
161 <![endif]-->
373 <![endif]-->
162 <!--[if IE 8]>
374 <!--[if IE 8]>
163 <script>$(document.body).addClass('ie8')</script>
375 <script>$(document.body).addClass('ie8')</script>
164 <![endif]-->
376 <![endif]-->
165 <!--[if IE 9]>
377 <!--[if IE 9]>
166 <script>$(document.body).addClass('ie9')</script>
378 <script>$(document.body).addClass('ie9')</script>
167 <![endif]-->
379 <![endif]-->
168
380
169 ${next.body()}
381 ${next.body()}
170 %if c.post_code:
382 %if c.post_code:
171 ${c.post_code|n}
383 ${c.post_code|n}
172 %endif
384 %endif
173 <rhodecode-app></rhodecode-app>
385 <rhodecode-app></rhodecode-app>
174 </body>
386 </body>
175 </html>
387 </html>
@@ -1,317 +1,315 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
5
5
6 <%def name="title()">
6 <%def name="title()">
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
7 ${_('%s Commit') % c.repo_name} - ${h.show_id(c.commit)}
8 %if c.rhodecode_name:
8 %if c.rhodecode_name:
9 &middot; ${h.branding(c.rhodecode_name)}
9 &middot; ${h.branding(c.rhodecode_name)}
10 %endif
10 %endif
11 </%def>
11 </%def>
12
12
13 <%def name="menu_bar_nav()">
13 <%def name="menu_bar_nav()">
14 ${self.menu_items(active='repositories')}
14 ${self.menu_items(active='repositories')}
15 </%def>
15 </%def>
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='changelog')}
18 ${self.repo_menu(active='changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <script>
22 <script>
23 // TODO: marcink switch this to pyroutes
23 // TODO: marcink switch this to pyroutes
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
24 AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
25 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
26 </script>
26 </script>
27 <div class="box">
27 <div class="box">
28 <div class="title">
28 <div class="title">
29 ${self.repo_page_title(c.rhodecode_db_repo)}
29 ${self.repo_page_title(c.rhodecode_db_repo)}
30 </div>
30 </div>
31
31
32 <div id="changeset_compare_view_content" class="summary changeset">
32 <div id="changeset_compare_view_content" class="summary changeset">
33 <div class="summary-detail">
33 <div class="summary-detail">
34 <div class="summary-detail-header">
34 <div class="summary-detail-header">
35 <span class="breadcrumbs files_location">
35 <span class="breadcrumbs files_location">
36 <h4>${_('Commit')}
36 <h4>${_('Commit')}
37 <code>
37 <code>
38 ${h.show_id(c.commit)}
38 ${h.show_id(c.commit)}
39 </code>
39 </code>
40 </h4>
40 </h4>
41 </span>
41 </span>
42 <span id="parent_link">
42 <span id="parent_link">
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
43 <a href="#" title="${_('Parent Commit')}">${_('Parent')}</a>
44 </span>
44 </span>
45 |
45 |
46 <span id="child_link">
46 <span id="child_link">
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
47 <a href="#" title="${_('Child Commit')}">${_('Child')}</a>
48 </span>
48 </span>
49 </div>
49 </div>
50
50
51 <div class="fieldset">
51 <div class="fieldset">
52 <div class="left-label">
52 <div class="left-label">
53 ${_('Description')}:
53 ${_('Description')}:
54 </div>
54 </div>
55 <div class="right-content">
55 <div class="right-content">
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
56 <div id="trimmed_message_box" class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
57 <div id="message_expand" style="display:none;">
57 <div id="message_expand" style="display:none;">
58 ${_('Expand')}
58 ${_('Expand')}
59 </div>
59 </div>
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 %if c.statuses:
63 %if c.statuses:
64 <div class="fieldset">
64 <div class="fieldset">
65 <div class="left-label">
65 <div class="left-label">
66 ${_('Commit status')}:
66 ${_('Commit status')}:
67 </div>
67 </div>
68 <div class="right-content">
68 <div class="right-content">
69 <div class="changeset-status-ico">
69 <div class="changeset-status-ico">
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
70 <div class="${'flag_status %s' % c.statuses[0]} pull-left"></div>
71 </div>
71 </div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
72 <div title="${_('Commit status')}" class="changeset-status-lbl">[${h.commit_status_lbl(c.statuses[0])}]</div>
73 </div>
73 </div>
74 </div>
74 </div>
75 %endif
75 %endif
76
76
77 <div class="fieldset">
77 <div class="fieldset">
78 <div class="left-label">
78 <div class="left-label">
79 ${_('References')}:
79 ${_('References')}:
80 </div>
80 </div>
81 <div class="right-content">
81 <div class="right-content">
82 <div class="tags">
82 <div class="tags">
83
83
84 %if c.commit.merge:
84 %if c.commit.merge:
85 <span class="mergetag tag">
85 <span class="mergetag tag">
86 <i class="icon-merge"></i>${_('merge')}
86 <i class="icon-merge"></i>${_('merge')}
87 </span>
87 </span>
88 %endif
88 %endif
89
89
90 %if h.is_hg(c.rhodecode_repo):
90 %if h.is_hg(c.rhodecode_repo):
91 %for book in c.commit.bookmarks:
91 %for book in c.commit.bookmarks:
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
92 <span class="booktag tag" title="${_('Bookmark %s') % book}">
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
93 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
94 </span>
94 </span>
95 %endfor
95 %endfor
96 %endif
96 %endif
97
97
98 %for tag in c.commit.tags:
98 %for tag in c.commit.tags:
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
99 <span class="tagtag tag" title="${_('Tag %s') % tag}">
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
100 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-tag"></i>${tag}</a>
101 </span>
101 </span>
102 %endfor
102 %endfor
103
103
104 %if c.commit.branch:
104 %if c.commit.branch:
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
105 <span class="branchtag tag" title="${_('Branch %s') % c.commit.branch}">
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
106 <a href="${h.url('files_home',repo_name=c.repo_name,revision=c.commit.raw_id)}"><i class="icon-code-fork"></i>${h.shorter(c.commit.branch)}</a>
107 </span>
107 </span>
108 %endif
108 %endif
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112
112
113 <div class="fieldset">
113 <div class="fieldset">
114 <div class="left-label">
114 <div class="left-label">
115 ${_('Diffs')}:
115 ${_('Diffs')}:
116 </div>
116 </div>
117 <div class="right-content">
117 <div class="right-content">
118 <div class="diff-actions">
118 <div class="diff-actions">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
119 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
120 ${_('Raw Diff')}
120 ${_('Raw Diff')}
121 </a>
121 </a>
122 |
122 |
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
123 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.commit.raw_id)}" class="tooltip" title="${h.tooltip(_('Patch diff'))}">
124 ${_('Patch Diff')}
124 ${_('Patch Diff')}
125 </a>
125 </a>
126 |
126 |
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
127 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.commit.raw_id,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
128 ${_('Download Diff')}
128 ${_('Download Diff')}
129 </a>
129 </a>
130 |
130 |
131 ${c.ignorews_url(request.GET)}
131 ${c.ignorews_url(request.GET)}
132 |
132 |
133 ${c.context_url(request.GET)}
133 ${c.context_url(request.GET)}
134 </div>
134 </div>
135 </div>
135 </div>
136 </div>
136 </div>
137
137
138 <div class="fieldset">
138 <div class="fieldset">
139 <div class="left-label">
139 <div class="left-label">
140 ${_('Comments')}:
140 ${_('Comments')}:
141 </div>
141 </div>
142 <div class="right-content">
142 <div class="right-content">
143 <div class="comments-number">
143 <div class="comments-number">
144 %if c.comments:
144 %if c.comments:
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
145 <a href="#comments">${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}</a>,
146 %else:
146 %else:
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
147 ${ungettext("%d Commit comment", "%d Commit comments", len(c.comments)) % len(c.comments)}
148 %endif
148 %endif
149 %if c.inline_cnt:
149 %if c.inline_cnt:
150 ## this is replaced with a proper link to first comment via JS linkifyComments() func
150 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
151 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
152 %else:
151 %else:
153 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
152 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
154 %endif
153 %endif
155 </div>
154 </div>
156 </div>
155 </div>
157 </div>
156 </div>
158
157
159 </div> <!-- end summary-detail -->
158 </div> <!-- end summary-detail -->
160
159
161 <div id="commit-stats" class="sidebar-right">
160 <div id="commit-stats" class="sidebar-right">
162 <div class="summary-detail-header">
161 <div class="summary-detail-header">
163 <h4 class="item">
162 <h4 class="item">
164 ${_('Author')}
163 ${_('Author')}
165 </h4>
164 </h4>
166 </div>
165 </div>
167 <div class="sidebar-right-content">
166 <div class="sidebar-right-content">
168 ${self.gravatar_with_user(c.commit.author)}
167 ${self.gravatar_with_user(c.commit.author)}
169 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
168 <div class="user-inline-data">- ${h.age_component(c.commit.date)}</div>
170 </div>
169 </div>
171 </div><!-- end sidebar -->
170 </div><!-- end sidebar -->
172 </div> <!-- end summary -->
171 </div> <!-- end summary -->
173 <div class="cs_files">
172 <div class="cs_files">
174 ${cbdiffs.render_diffset_menu()}
173 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
175
174 ${cbdiffs.render_diffset_menu()}
176 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
175 ${cbdiffs.render_diffset(
177 ${cbdiffs.render_diffset(c.changes[c.commit.raw_id], commit=c.commit)}
176 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True)}
178 </div>
177 </div>
179 </div>
180
178
181 ## template for inline comment form
179 ## template for inline comment form
182 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
180 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
183 ${comment.comment_inline_form()}
181 ${comment.comment_inline_form()}
184
182
185 ## render comments and inlines
183 ## ## render comments and inlines
186 ${comment.generate_comments()}
184 ${comment.generate_comments()}
187
185
188 ## main comment form and it status
186 ## main comment form and it status
189 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
187 ${comment.comments(h.url('changeset_comment', repo_name=c.repo_name, revision=c.commit.raw_id),
190 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
188 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
189 </div>
191
190
192 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
191 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
193 <script type="text/javascript">
192 <script type="text/javascript">
194
193
195 $(document).ready(function() {
194 $(document).ready(function() {
196
195
197 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
196 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
198 if($('#trimmed_message_box').height() === boxmax){
197 if($('#trimmed_message_box').height() === boxmax){
199 $('#message_expand').show();
198 $('#message_expand').show();
200 }
199 }
201
200
202 $('#message_expand').on('click', function(e){
201 $('#message_expand').on('click', function(e){
203 $('#trimmed_message_box').css('max-height', 'none');
202 $('#trimmed_message_box').css('max-height', 'none');
204 $(this).hide();
203 $(this).hide();
205 });
204 });
206
205
207 $('.show-inline-comments').on('click', function(e){
206 $('.show-inline-comments').on('click', function(e){
208 var boxid = $(this).attr('data-comment-id');
207 var boxid = $(this).attr('data-comment-id');
209 var button = $(this);
208 var button = $(this);
210
209
211 if(button.hasClass("comments-visible")) {
210 if(button.hasClass("comments-visible")) {
212 $('#{0} .inline-comments'.format(boxid)).each(function(index){
211 $('#{0} .inline-comments'.format(boxid)).each(function(index){
213 $(this).hide();
212 $(this).hide();
214 })
213 })
215 button.removeClass("comments-visible");
214 button.removeClass("comments-visible");
216 } else {
215 } else {
217 $('#{0} .inline-comments'.format(boxid)).each(function(index){
216 $('#{0} .inline-comments'.format(boxid)).each(function(index){
218 $(this).show();
217 $(this).show();
219 })
218 })
220 button.addClass("comments-visible");
219 button.addClass("comments-visible");
221 }
220 }
222 });
221 });
223
222
224
223
225 // next links
224 // next links
226 $('#child_link').on('click', function(e){
225 $('#child_link').on('click', function(e){
227 // fetch via ajax what is going to be the next link, if we have
226 // fetch via ajax what is going to be the next link, if we have
228 // >1 links show them to user to choose
227 // >1 links show them to user to choose
229 if(!$('#child_link').hasClass('disabled')){
228 if(!$('#child_link').hasClass('disabled')){
230 $.ajax({
229 $.ajax({
231 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
230 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
232 success: function(data) {
231 success: function(data) {
233 if(data.results.length === 0){
232 if(data.results.length === 0){
234 $('#child_link').html('${_('No Child Commits')}').addClass('disabled');
233 $('#child_link').html('${_('No Child Commits')}').addClass('disabled');
235 }
234 }
236 if(data.results.length === 1){
235 if(data.results.length === 1){
237 var commit = data.results[0];
236 var commit = data.results[0];
238 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
237 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
239 }
238 }
240 else if(data.results.length === 2){
239 else if(data.results.length === 2){
241 $('#child_link').addClass('disabled');
240 $('#child_link').addClass('disabled');
242 $('#child_link').addClass('double');
241 $('#child_link').addClass('double');
243 var _html = '';
242 var _html = '';
244 _html +='<a title="__title__" href="__url__">__rev__</a> '
243 _html +='<a title="__title__" href="__url__">__rev__</a> '
245 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
244 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
246 .replace('__title__', data.results[0].message)
245 .replace('__title__', data.results[0].message)
247 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
246 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
248 _html +=' | '
247 _html +=' | '
249 _html +='<a title="__title__" href="__url__">__rev__</a> '
248 _html +='<a title="__title__" href="__url__">__rev__</a> '
250 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
249 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
251 .replace('__title__', data.results[1].message)
250 .replace('__title__', data.results[1].message)
252 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
251 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
253 $('#child_link').html(_html);
252 $('#child_link').html(_html);
254 }
253 }
255 }
254 }
256 });
255 });
257 e.preventDefault();
256 e.preventDefault();
258 }
257 }
259 });
258 });
260
259
261 // prev links
260 // prev links
262 $('#parent_link').on('click', function(e){
261 $('#parent_link').on('click', function(e){
263 // fetch via ajax what is going to be the next link, if we have
262 // fetch via ajax what is going to be the next link, if we have
264 // >1 links show them to user to choose
263 // >1 links show them to user to choose
265 if(!$('#parent_link').hasClass('disabled')){
264 if(!$('#parent_link').hasClass('disabled')){
266 $.ajax({
265 $.ajax({
267 url: '${h.url('changeset_parents',repo_name=c.repo_name, revision=c.commit.raw_id)}',
266 url: '${h.url('changeset_parents',repo_name=c.repo_name, revision=c.commit.raw_id)}',
268 success: function(data) {
267 success: function(data) {
269 if(data.results.length === 0){
268 if(data.results.length === 0){
270 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
269 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
271 }
270 }
272 if(data.results.length === 1){
271 if(data.results.length === 1){
273 var commit = data.results[0];
272 var commit = data.results[0];
274 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
273 window.location = pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': commit.raw_id});
275 }
274 }
276 else if(data.results.length === 2){
275 else if(data.results.length === 2){
277 $('#parent_link').addClass('disabled');
276 $('#parent_link').addClass('disabled');
278 $('#parent_link').addClass('double');
277 $('#parent_link').addClass('double');
279 var _html = '';
278 var _html = '';
280 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
279 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
281 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
280 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
282 .replace('__title__', data.results[0].message)
281 .replace('__title__', data.results[0].message)
283 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
282 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[0].raw_id}));
284 _html +=' | '
283 _html +=' | '
285 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
284 _html +='<a title="__title__" href="__url__">Parent __rev__</a>'
286 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
285 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
287 .replace('__title__', data.results[1].message)
286 .replace('__title__', data.results[1].message)
288 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
287 .replace('__url__', pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}','revision': data.results[1].raw_id}));
289 $('#parent_link').html(_html);
288 $('#parent_link').html(_html);
290 }
289 }
291 }
290 }
292 });
291 });
293 e.preventDefault();
292 e.preventDefault();
294 }
293 }
295 });
294 });
296
295
297 if (location.hash) {
296 if (location.hash) {
298 var result = splitDelimitedHash(location.hash);
297 var result = splitDelimitedHash(location.hash);
299 var line = $('html').find(result.loc);
298 var line = $('html').find(result.loc);
300 if (line.length > 0){
299 if (line.length > 0){
301 offsetScroll(line, 70);
300 offsetScroll(line, 70);
302 }
301 }
303 }
302 }
304
303
305 // browse tree @ revision
304 // browse tree @ revision
306 $('#files_link').on('click', function(e){
305 $('#files_link').on('click', function(e){
307 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
306 window.location = '${h.url('files_home',repo_name=c.repo_name, revision=c.commit.raw_id, f_path='')}';
308 e.preventDefault();
307 e.preventDefault();
309 });
308 });
310
309
311 // inject comments into their proper positions
310 // inject comments into their proper positions
312 var file_comments = $('.inline-comment-placeholder');
311 var file_comments = $('.inline-comment-placeholder');
313 renderInlineComments(file_comments, true);
314 })
312 })
315 </script>
313 </script>
316
314
317 </%def>
315 </%def>
@@ -1,312 +1,287 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.html"/>
6 <%namespace name="base" file="/base/base.html"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <div class="comment ${'comment-inline' if inline else ''}" id="comment-${comment.comment_id}" line="${comment.line_no}" data-comment-id="${comment.comment_id}">
9 <div
10 class="comment
11 ${'comment-inline' if inline else ''}
12 ${'comment-outdated' if comment.outdated else 'comment-current'}"
13 "
14 id="comment-${comment.comment_id}"
15 line="${comment.line_no}"
16 data-comment-id="${comment.comment_id}">
10 <div class="meta">
17 <div class="meta">
11 <div class="author">
18 <div class="author">
12 ${base.gravatar_with_user(comment.author.email, 16)}
19 ${base.gravatar_with_user(comment.author.email, 16)}
13 </div>
20 </div>
14 <div class="date">
21 <div class="date">
15 ${h.age_component(comment.modified_at, time_is_local=True)}
22 ${h.age_component(comment.modified_at, time_is_local=True)}
16 </div>
23 </div>
17 <div class="status-change">
24 <div class="status-change">
18 %if comment.pull_request:
25 %if comment.pull_request:
19 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
26 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
20 %if comment.status_change:
27 %if comment.status_change:
21 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
28 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
22 %else:
29 %else:
23 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
30 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
24 %endif
31 %endif
25 </a>
32 </a>
26 %else:
33 %else:
27 %if comment.status_change:
34 %if comment.status_change:
28 ${_('Status change on commit')}:
35 ${_('Status change on commit')}:
29 %else:
36 %else:
30 ${_('Comment on commit')}
37 ${_('Comment on commit')}
31 %endif
38 %endif
32 %endif
39 %endif
33 </div>
40 </div>
34 %if comment.status_change:
41 %if comment.status_change:
35 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
42 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
36 <div title="${_('Commit status')}" class="changeset-status-lbl">
43 <div title="${_('Commit status')}" class="changeset-status-lbl">
37 ${comment.status_change[0].status_lbl}
44 ${comment.status_change[0].status_lbl}
38 </div>
45 </div>
39 %endif
46 %endif
40 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
47 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
41
48
42
49
43 <div class="comment-links-block">
50 <div class="comment-links-block">
44
51
45 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
52 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
46 ## only super-admin, repo admin OR comment owner can delete
53 ## only super-admin, repo admin OR comment owner can delete
47 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
54 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
48 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
55 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
49 <div onClick="deleteComment(${comment.comment_id})" class="delete-comment"> ${_('Delete')}</div>
56 ## TODO: dan: add edit comment here
50 %if inline:
57 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
51 <div class="comment-links-divider"> | </div>
58 %if not comment.outdated:
59 <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
60 <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
52 %endif
61 %endif
53 %endif
62 %endif
54 %endif
63 %endif
55
64
56 %if inline:
57
58 <div id="prev_c_${comment.comment_id}" class="comment-previous-link" title="${_('Previous comment')}">
59 <a class="arrow_comment_link disabled"><i class="icon-left"></i></a>
60 </div>
61
62 <div id="next_c_${comment.comment_id}" class="comment-next-link" title="${_('Next comment')}">
63 <a class="arrow_comment_link disabled"><i class="icon-right"></i></a>
64 </div>
65 %endif
66
67 </div>
65 </div>
68 </div>
66 </div>
69 <div class="text">
67 <div class="text">
70 ${comment.render(mentions=True)|n}
68 ${comment.render(mentions=True)|n}
71 </div>
69 </div>
72 </div>
70 </div>
73 </%def>
71 </%def>
74
72
75 <%def name="comment_block_outdated(comment)">
73 <%def name="comment_block_outdated(comment)">
76 <div class="comments" id="comment-${comment.comment_id}">
74 <div class="comments" id="comment-${comment.comment_id}">
77 <div class="comment comment-wrapp">
75 <div class="comment comment-wrapp">
78 <div class="meta">
76 <div class="meta">
79 <div class="author">
77 <div class="author">
80 ${base.gravatar_with_user(comment.author.email, 16)}
78 ${base.gravatar_with_user(comment.author.email, 16)}
81 </div>
79 </div>
82 <div class="date">
80 <div class="date">
83 ${h.age_component(comment.modified_at, time_is_local=True)}
81 ${h.age_component(comment.modified_at, time_is_local=True)}
84 </div>
82 </div>
85 %if comment.status_change:
83 %if comment.status_change:
86 <span class="changeset-status-container">
84 <span class="changeset-status-container">
87 <span class="changeset-status-ico">
85 <span class="changeset-status-ico">
88 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
86 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
89 </span>
87 </span>
90 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
88 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
91 </span>
89 </span>
92 %endif
90 %endif
93 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
91 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
94 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
95 ## only super-admin, repo admin OR comment owner can delete
93 ## only super-admin, repo admin OR comment owner can delete
96 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
94 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
97 <div class="comment-links-block">
95 <div class="comment-links-block">
98 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
99 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
97 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
100 %endif
98 %endif
101 </div>
99 </div>
102 %endif
100 %endif
103 </div>
101 </div>
104 <div class="text">
102 <div class="text">
105 ${comment.render(mentions=True)|n}
103 ${comment.render(mentions=True)|n}
106 </div>
104 </div>
107 </div>
105 </div>
108 </div>
106 </div>
109 </%def>
107 </%def>
110
108
111 <%def name="comment_inline_form()">
109 <%def name="comment_inline_form()">
112 <div id="comment-inline-form-template" style="display: none;">
110 <div id="comment-inline-form-template" style="display: none;">
113 <div class="comment-inline-form ac">
111 <div class="comment-inline-form ac">
114 %if c.rhodecode_user.username != h.DEFAULT_USER:
112 %if c.rhodecode_user.username != h.DEFAULT_USER:
115 ${h.form('#', class_='inline-form', method='get')}
113 ${h.form('#', class_='inline-form', method='get')}
116 <div id="edit-container_{1}" class="clearfix">
114 <div id="edit-container_{1}" class="clearfix">
117 <div class="comment-title pull-left">
115 <div class="comment-title pull-left">
118 ${_('Create a comment on line {1}.')}
116 ${_('Create a comment on line {1}.')}
119 </div>
117 </div>
120 <div class="comment-help pull-right">
118 <div class="comment-help pull-right">
121 ${(_('Comments parsed using %s syntax with %s support.') % (
119 ${(_('Comments parsed using %s syntax with %s support.') % (
122 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
120 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
123 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
121 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
124 )
122 )
125 )|n
123 )|n
126 }
124 }
127 </div>
125 </div>
128 <div style="clear: both"></div>
126 <div style="clear: both"></div>
129 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
127 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
130 </div>
128 </div>
131 <div id="preview-container_{1}" class="clearfix" style="display: none;">
129 <div id="preview-container_{1}" class="clearfix" style="display: none;">
132 <div class="comment-help">
130 <div class="comment-help">
133 ${_('Comment preview')}
131 ${_('Comment preview')}
134 </div>
132 </div>
135 <div id="preview-box_{1}" class="preview-box"></div>
133 <div id="preview-box_{1}" class="preview-box"></div>
136 </div>
134 </div>
137 <div class="comment-footer">
135 <div class="comment-footer">
138 <div class="comment-button hide-inline-form-button cancel-button">
136 <div class="comment-button hide-inline-form-button cancel-button">
139 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
137 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
140 </div>
138 </div>
141 <div class="action-buttons">
139 <div class="action-buttons">
142 <input type="hidden" name="f_path" value="{0}">
140 <input type="hidden" name="f_path" value="{0}">
143 <input type="hidden" name="line" value="{1}">
141 <input type="hidden" name="line" value="{1}">
144 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
142 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
145 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
143 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
146 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
144 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
147 </div>
145 </div>
148 ${h.end_form()}
146 ${h.end_form()}
149 </div>
147 </div>
150 %else:
148 %else:
151 ${h.form('', class_='inline-form comment-form-login', method='get')}
149 ${h.form('', class_='inline-form comment-form-login', method='get')}
152 <div class="pull-left">
150 <div class="pull-left">
153 <div class="comment-help pull-right">
151 <div class="comment-help pull-right">
154 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
152 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
155 </div>
153 </div>
156 </div>
154 </div>
157 <div class="comment-button pull-right">
155 <div class="comment-button pull-right">
158 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
156 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
159 </div>
157 </div>
160 <div class="clearfix"></div>
158 <div class="clearfix"></div>
161 ${h.end_form()}
159 ${h.end_form()}
162 %endif
160 %endif
163 </div>
161 </div>
164 </div>
162 </div>
165 </%def>
163 </%def>
166
164
167
165
168 ## generates inlines taken from c.comments var
166 ## generate main comments
169 <%def name="inlines(is_pull_request=False)">
170 %if is_pull_request:
171 <h2 id="comments">${ungettext("%d Pull Request Comment", "%d Pull Request Comments", len(c.comments)) % len(c.comments)}</h2>
172 %else:
173 <h2 id="comments">${ungettext("%d Commit Comment", "%d Commit Comments", len(c.comments)) % len(c.comments)}</h2>
174 %endif
175 %for path, lines_comments in c.inline_comments:
176 % for line, comments in lines_comments.iteritems():
177 <div style="display: none;" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
178 ## for each comment in particular line
179 %for comment in comments:
180 ${comment_block(comment, inline=True)}
181 %endfor
182 </div>
183 %endfor
184 %endfor
185
186 </%def>
187
188 ## generate inline comments and the main ones
189 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
167 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
190 ## generate inlines for this changeset
191 ${inlines(is_pull_request)}
192
193 %for comment in c.comments:
168 %for comment in c.comments:
194 <div id="comment-tr-${comment.comment_id}">
169 <div id="comment-tr-${comment.comment_id}">
195 ## only render comments that are not from pull request, or from
170 ## only render comments that are not from pull request, or from
196 ## pull request and a status change
171 ## pull request and a status change
197 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
172 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
198 ${comment_block(comment)}
173 ${comment_block(comment)}
199 %endif
174 %endif
200 </div>
175 </div>
201 %endfor
176 %endfor
202 ## to anchor ajax comments
177 ## to anchor ajax comments
203 <div id="injected_page_comments"></div>
178 <div id="injected_page_comments"></div>
204 </%def>
179 </%def>
205
180
206 ## MAIN COMMENT FORM
181 ## MAIN COMMENT FORM
207 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
182 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
208 %if is_compare:
183 %if is_compare:
209 <% form_id = "comments_form_compare" %>
184 <% form_id = "comments_form_compare" %>
210 %else:
185 %else:
211 <% form_id = "comments_form" %>
186 <% form_id = "comments_form" %>
212 %endif
187 %endif
213
188
214
189
215 %if is_pull_request:
190 %if is_pull_request:
216 <div class="pull-request-merge">
191 <div class="pull-request-merge">
217 %if c.allowed_to_merge:
192 %if c.allowed_to_merge:
218 <div class="pull-request-wrap">
193 <div class="pull-request-wrap">
219 <div class="pull-right">
194 <div class="pull-right">
220 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
195 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
221 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
196 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
222 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
197 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
223 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
198 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
224 ${h.end_form()}
199 ${h.end_form()}
225 </div>
200 </div>
226 </div>
201 </div>
227 %else:
202 %else:
228 <div class="pull-request-wrap">
203 <div class="pull-request-wrap">
229 <div class="pull-right">
204 <div class="pull-right">
230 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
205 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
231 </div>
206 </div>
232 </div>
207 </div>
233 %endif
208 %endif
234 </div>
209 </div>
235 %endif
210 %endif
236 <div class="comments">
211 <div class="comments">
237 %if c.rhodecode_user.username != h.DEFAULT_USER:
212 %if c.rhodecode_user.username != h.DEFAULT_USER:
238 <div class="comment-form ac">
213 <div class="comment-form ac">
239 ${h.secure_form(post_url, id_=form_id)}
214 ${h.secure_form(post_url, id_=form_id)}
240 <div id="edit-container" class="clearfix">
215 <div id="edit-container" class="clearfix">
241 <div class="comment-title pull-left">
216 <div class="comment-title pull-left">
242 %if is_pull_request:
217 %if is_pull_request:
243 ${(_('Create a comment on this Pull Request.'))}
218 ${(_('Create a comment on this Pull Request.'))}
244 %elif is_compare:
219 %elif is_compare:
245 ${(_('Create comments on this Commit range.'))}
220 ${(_('Create comments on this Commit range.'))}
246 %else:
221 %else:
247 ${(_('Create a comment on this Commit.'))}
222 ${(_('Create a comment on this Commit.'))}
248 %endif
223 %endif
249 </div>
224 </div>
250 <div class="comment-help pull-right">
225 <div class="comment-help pull-right">
251 ${(_('Comments parsed using %s syntax with %s support.') % (
226 ${(_('Comments parsed using %s syntax with %s support.') % (
252 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
227 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
253 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
228 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
254 )
229 )
255 )|n
230 )|n
256 }
231 }
257 </div>
232 </div>
258 <div style="clear: both"></div>
233 <div style="clear: both"></div>
259 ${h.textarea('text', class_="comment-block-ta")}
234 ${h.textarea('text', class_="comment-block-ta")}
260 </div>
235 </div>
261
236
262 <div id="preview-container" class="clearfix" style="display: none;">
237 <div id="preview-container" class="clearfix" style="display: none;">
263 <div class="comment-title">
238 <div class="comment-title">
264 ${_('Comment preview')}
239 ${_('Comment preview')}
265 </div>
240 </div>
266 <div id="preview-box" class="preview-box"></div>
241 <div id="preview-box" class="preview-box"></div>
267 </div>
242 </div>
268
243
269 <div id="comment_form_extras">
244 <div id="comment_form_extras">
270 %if form_extras and isinstance(form_extras, (list, tuple)):
245 %if form_extras and isinstance(form_extras, (list, tuple)):
271 % for form_ex_el in form_extras:
246 % for form_ex_el in form_extras:
272 ${form_ex_el|n}
247 ${form_ex_el|n}
273 % endfor
248 % endfor
274 %endif
249 %endif
275 </div>
250 </div>
276 <div class="comment-footer">
251 <div class="comment-footer">
277 %if change_status:
252 %if change_status:
278 <div class="status_box">
253 <div class="status_box">
279 <select id="change_status" name="changeset_status">
254 <select id="change_status" name="changeset_status">
280 <option></option> # Placeholder
255 <option></option> # Placeholder
281 %for status,lbl in c.commit_statuses:
256 %for status,lbl in c.commit_statuses:
282 <option value="${status}" data-status="${status}">${lbl}</option>
257 <option value="${status}" data-status="${status}">${lbl}</option>
283 %if is_pull_request and change_status and status in ('approved', 'rejected'):
258 %if is_pull_request and change_status and status in ('approved', 'rejected'):
284 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
259 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
285 %endif
260 %endif
286 %endfor
261 %endfor
287 </select>
262 </select>
288 </div>
263 </div>
289 %endif
264 %endif
290 <div class="action-buttons">
265 <div class="action-buttons">
291 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
266 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
292 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
267 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
293 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
268 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
294 </div>
269 </div>
295 </div>
270 </div>
296 ${h.end_form()}
271 ${h.end_form()}
297 </div>
272 </div>
298 %endif
273 %endif
299 </div>
274 </div>
300 <script>
275 <script>
301 // init active elements of commentForm
276 // init active elements of commentForm
302 var commitId = templateContext.commit_data.commit_id;
277 var commitId = templateContext.commit_data.commit_id;
303 var pullRequestId = templateContext.pull_request_data.pull_request_id;
278 var pullRequestId = templateContext.pull_request_data.pull_request_id;
304 var lineNo;
279 var lineNo;
305
280
306 var mainCommentForm = new CommentForm(
281 var mainCommentForm = new CommentForm(
307 "#${form_id}", commitId, pullRequestId, lineNo, true);
282 "#${form_id}", commitId, pullRequestId, lineNo, true);
308
283
309 mainCommentForm.initStatusChangeSelector();
284 mainCommentForm.initStatusChangeSelector();
310 bindToggleButtons();
285 bindToggleButtons();
311 </script>
286 </script>
312 </%def>
287 </%def>
@@ -1,71 +1,71 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Commits') % c.repo_name} -
5 ${_('%s Commits') % c.repo_name} -
6 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
6 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
7 ...
7 ...
8 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
8 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
9 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
10 %if c.rhodecode_name:
10 %if c.rhodecode_name:
11 &middot; ${h.branding(c.rhodecode_name)}
11 &middot; ${h.branding(c.rhodecode_name)}
12 %endif
12 %endif
13 </%def>
13 </%def>
14
14
15 <%def name="breadcrumbs_links()">
15 <%def name="breadcrumbs_links()">
16 ${_('Commits')} -
16 ${_('Commits')} -
17 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
17 r${c.commit_ranges[0].revision}:${h.short_id(c.commit_ranges[0].raw_id)}
18 ...
18 ...
19 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
19 r${c.commit_ranges[-1].revision}:${h.short_id(c.commit_ranges[-1].raw_id)}
20 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
20 ${ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
21 </%def>
21 </%def>
22
22
23 <%def name="menu_bar_nav()">
23 <%def name="menu_bar_nav()">
24 ${self.menu_items(active='repositories')}
24 ${self.menu_items(active='repositories')}
25 </%def>
25 </%def>
26
26
27 <%def name="menu_bar_subnav()">
27 <%def name="menu_bar_subnav()">
28 ${self.repo_menu(active='changelog')}
28 ${self.repo_menu(active='changelog')}
29 </%def>
29 </%def>
30
30
31 <%def name="main()">
31 <%def name="main()">
32 <div class="summary-header">
32 <div class="summary-header">
33 <div class="title">
33 <div class="title">
34 <div class="title-content">
34 <div class="title-content">
35 ${self.repo_page_title(c.rhodecode_db_repo)}
35 ${self.repo_page_title(c.rhodecode_db_repo)}
36 </div>
36 </div>
37 </div>
37 </div>
38 <div class="header-buttons">
38 <div class="header-buttons">
39 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}"
39 <a href="${h.url('compare_url', repo_name=c.repo_name, source_ref_type='rev', source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'), target_ref_type='rev', target_ref=c.commit_ranges[-1].raw_id)}"
40 class="btn btn-default">
40 class="btn btn-default">
41 ${_('Show combined compare')}
41 ${_('Show combined compare')}
42 </a>
42 </a>
43 </div>
43 </div>
44 </div>
44 </div>
45
45
46 <div class="summary-detail">
46 <div class="summary-detail">
47 <div class="title">
47 <div class="title">
48 <h2>
48 <h2>
49 ${self.breadcrumbs_links()}
49 ${self.breadcrumbs_links()}
50 </h2>
50 </h2>
51 </div>
51 </div>
52 </div>
52 </div>
53 <div id="changeset_compare_view_content">
53 <div id="changeset_compare_view_content">
54 ##CS
54 ##CS
55 <%include file="../compare/compare_commits.html"/>
55 <%include file="../compare/compare_commits.html"/>
56 <div class="cs_files">
56 <div class="cs_files">
57 ${cbdiffs.render_diffset_menu()}
58 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
57 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
59 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
58 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
60 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
59 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
60 ${cbdiffs.render_diffset_menu()}
61 %for commit in c.commit_ranges:
61 %for commit in c.commit_ranges:
62 ${cbdifss.render_diffset(
62 ${cbdiffs.render_diffset(
63 diffset=c.changes[commit.raw_id],
63 diffset=c.changes[commit.raw_id],
64 collapse_when_files_over=5,
64 collapse_when_files_over=5,
65 commit=commit,
65 commit=commit,
66 )}
66 )}
67 %endfor
67 %endfor
68 </table>
68 </table>
69 </div>
69 </div>
70 </div>
70 </div>
71 </%def>
71 </%def>
@@ -1,420 +1,542 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
38 use_comments=False,
39
37 )">
40 )">
41
42 %if use_comments:
43 <div id="cb-comments-inline-container-template" class="js-template">
44 ${inline_comments_container([])}
45 </div>
46 <div class="js-template" id="cb-comment-inline-form-template">
47 <div class="comment-inline-form ac">
48 %if c.rhodecode_user.username != h.DEFAULT_USER:
49 ${h.form('#', method='get')}
50 <div id="edit-container_{1}" class="clearfix">
51 <div class="comment-title pull-left">
52 ${_('Create a comment on line {1}.')}
53 </div>
54 <div class="comment-help pull-right">
55 ${(_('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())),
57 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
58 )
59 )|n
60 }
61 </div>
62 <div style="clear: both"></div>
63 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
64 </div>
65 <div id="preview-container_{1}" class="clearfix" style="display: none;">
66 <div class="comment-help">
67 ${_('Comment preview')}
68 </div>
69 <div id="preview-box_{1}" class="preview-box"></div>
70 </div>
71 <div class="comment-footer">
72 <div class="action-buttons">
73 <input type="hidden" name="f_path" value="{0}">
74 <input type="hidden" name="line" value="{1}">
75 <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>
77 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
78 </div>
79 <div class="comment-button">
80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 ${_('Cancel')}
82 </button>
83 </div>
84 ${h.end_form()}
85 </div>
86 %else:
87 ${h.form('', class_='inline-form comment-form-login', method='get')}
88 <div class="pull-left">
89 <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>
91 </div>
92 </div>
93 <div class="comment-button pull-right">
94 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
95 ${_('Cancel')}
96 </button>
97 </div>
98 <div class="clearfix"></div>
99 ${h.end_form()}
100 %endif
101 </div>
102 </div>
103
104 %endif
38 <%
105 <%
39 collapse_all = len(diffset.files) > collapse_when_files_over
106 collapse_all = len(diffset.files) > collapse_when_files_over
40 %>
107 %>
41
108
42 %if c.diffmode == 'sideside':
109 %if c.diffmode == 'sideside':
43 <style>
110 <style>
44 .wrapper {
111 .wrapper {
45 max-width: 1600px !important;
112 max-width: 1600px !important;
46 }
113 }
47 </style>
114 </style>
48 %endif
115 %endif
49 %if ruler_at_chars:
116 %if ruler_at_chars:
50 <style>
117 <style>
51 .diff table.cb .cb-content:after {
118 .diff table.cb .cb-content:after {
52 content: "";
119 content: "";
53 border-left: 1px solid blue;
120 border-left: 1px solid blue;
54 position: absolute;
121 position: absolute;
55 top: 0;
122 top: 0;
56 height: 18px;
123 height: 18px;
57 opacity: .2;
124 opacity: .2;
58 z-index: 10;
125 z-index: 10;
59 ## +5 to account for diff action (+/-)
126 ## +5 to account for diff action (+/-)
60 left: ${ruler_at_chars + 5}ch;
127 left: ${ruler_at_chars + 5}ch;
61 </style>
128 </style>
62 %endif
129 %endif
63
130
64 <div class="diffset">
131 <div class="diffset">
65 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
132 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
66 %if commit:
133 %if commit:
67 <div class="pull-right">
134 <div class="pull-right">
68 <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='')}">
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='')}">
69 ${_('Browse Files')}
136 ${_('Browse Files')}
70 </a>
137 </a>
71 </div>
138 </div>
72 %endif
139 %endif
73 <h2 class="clearinner">
140 <h2 class="clearinner">
74 %if commit:
141 %if commit:
75 <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> -
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> -
76 ${h.age_component(commit.date)} -
143 ${h.age_component(commit.date)} -
77 %endif
144 %endif
78 %if diffset.limited_diff:
145 %if diffset.limited_diff:
79 ${_('The requested commit is too big and content was truncated.')}
146 ${_('The requested commit is too big and content was truncated.')}
80
147
81 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
148 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
82 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
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>
83 %else:
150 %else:
84 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
151 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
85 '%(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}}
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}}
86 %endif
153 %endif
87 </h2>
154 </h2>
88 </div>
155 </div>
89
156
90 %if not diffset.files:
157 %if not diffset.files:
91 <p class="empty_data">${_('No files')}</p>
158 <p class="empty_data">${_('No files')}</p>
92 %endif
159 %endif
93
160
94 <div class="filediffs">
161 <div class="filediffs">
95 %for i, filediff in enumerate(diffset.files):
162 %for i, filediff in enumerate(diffset.files):
96 <%
163 <%
97 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
164 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
98 over_lines_changed_limit = lines_changed > lines_changed_limit
165 over_lines_changed_limit = lines_changed > lines_changed_limit
99 %>
166 %>
100 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
167 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
101 <div
168 <div
102 class="filediff"
169 class="filediff"
103 data-f-path="${filediff['patch']['filename']}"
170 data-f-path="${filediff['patch']['filename']}"
104 id="a_${h.FID(commit and commit.raw_id or '', filediff['patch']['filename'])}">
171 id="a_${h.FID('', filediff['patch']['filename'])}">
105 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
172 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
106 <div class="filediff-collapse-indicator"></div>
173 <div class="filediff-collapse-indicator"></div>
107 ${diff_ops(filediff)}
174 ${diff_ops(filediff)}
108 </label>
175 </label>
109 ${diff_menu(filediff)}
176 ${diff_menu(filediff, use_comments=use_comments)}
110 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
177 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
111 %if not filediff.hunks:
178 %if not filediff.hunks:
112 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
179 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
113 <tr>
180 <tr>
114 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
181 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
115 %if op_id == DEL_FILENODE:
182 %if op_id == DEL_FILENODE:
116 ${_('File was deleted')}
183 ${_('File was deleted')}
117 %elif op_id == BIN_FILENODE:
184 %elif op_id == BIN_FILENODE:
118 ${_('Binary file hidden')}
185 ${_('Binary file hidden')}
119 %else:
186 %else:
120 ${op_text}
187 ${op_text}
121 %endif
188 %endif
122 </td>
189 </td>
123 </tr>
190 </tr>
124 %endfor
191 %endfor
125 %endif
192 %endif
126 %if over_lines_changed_limit:
193 %if over_lines_changed_limit:
127 <tr class="cb-warning cb-collapser">
194 <tr class="cb-warning cb-collapser">
128 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
195 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
129 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
196 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
130 <a href="#" class="cb-expand"
197 <a href="#" class="cb-expand"
131 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
198 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
132 </a>
199 </a>
133 <a href="#" class="cb-collapse"
200 <a href="#" class="cb-collapse"
134 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
201 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
135 </a>
202 </a>
136 </td>
203 </td>
137 </tr>
204 </tr>
138 %endif
205 %endif
139 %if filediff.patch['is_limited_diff']:
206 %if filediff.patch['is_limited_diff']:
140 <tr class="cb-warning cb-collapser">
207 <tr class="cb-warning cb-collapser">
141 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
208 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
142 ${_('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>
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>
143 </td>
210 </td>
144 </tr>
211 </tr>
145 %endif
212 %endif
146 %for hunk in filediff.hunks:
213 %for hunk in filediff.hunks:
147 <tr class="cb-hunk">
214 <tr class="cb-hunk">
148 <td ${c.diffmode == 'unified' and 'colspan=2' or ''}>
215 <td ${c.diffmode == 'unified' and 'colspan=2' or ''}>
149 ## TODO: dan: add ajax loading of more context here
216 ## TODO: dan: add ajax loading of more context here
150 ## <a href="#">
217 ## <a href="#">
151 <i class="icon-more"></i>
218 <i class="icon-more"></i>
152 ## </a>
219 ## </a>
153 </td>
220 </td>
154 <td ${c.diffmode == 'sideside' and 'colspan=3' or ''}>
221 <td ${c.diffmode == 'sideside' and 'colspan=3' or ''}>
155 @@
222 @@
156 -${hunk.source_start},${hunk.source_length}
223 -${hunk.source_start},${hunk.source_length}
157 +${hunk.target_start},${hunk.target_length}
224 +${hunk.target_start},${hunk.target_length}
158 ${hunk.section_header}
225 ${hunk.section_header}
159 </td>
226 </td>
160 </tr>
227 </tr>
161 %if c.diffmode == 'unified':
228 %if c.diffmode == 'unified':
162 ${render_hunk_lines_unified(hunk)}
229 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
163 %elif c.diffmode == 'sideside':
230 %elif c.diffmode == 'sideside':
164 ${render_hunk_lines_sideside(hunk)}
231 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
165 %else:
232 %else:
166 <tr class="cb-line">
233 <tr class="cb-line">
167 <td>unknown diff mode</td>
234 <td>unknown diff mode</td>
168 </tr>
235 </tr>
169 %endif
236 %endif
170 %endfor
237 %endfor
171 </table>
238 </table>
172 </div>
239 </div>
173 %endfor
240 %endfor
174 </div>
241 </div>
175 </div>
242 </div>
176 </%def>
243 </%def>
177
244
178 <%def name="diff_ops(filediff)">
245 <%def name="diff_ops(filediff)">
179 <%
246 <%
180 stats = filediff['patch']['stats']
247 stats = filediff['patch']['stats']
181 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
248 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
182 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
249 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
183 %>
250 %>
184 <span class="pill">
251 <span class="pill">
185 %if filediff.source_file_path and filediff.target_file_path:
252 %if filediff.source_file_path and filediff.target_file_path:
186 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
253 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
187 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
254 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
188 %else:
255 %else:
189 ## file was modified
256 ## file was modified
190 <strong>${filediff.source_file_path}</strong>
257 <strong>${filediff.source_file_path}</strong>
191 %endif
258 %endif
192 %else:
259 %else:
193 %if filediff.source_file_path:
260 %if filediff.source_file_path:
194 ## file was deleted
261 ## file was deleted
195 <strong>${filediff.source_file_path}</strong>
262 <strong>${filediff.source_file_path}</strong>
196 %else:
263 %else:
197 ## file was added
264 ## file was added
198 <strong>${filediff.target_file_path}</strong>
265 <strong>${filediff.target_file_path}</strong>
199 %endif
266 %endif
200 %endif
267 %endif
201 </span>
268 </span>
202 <span class="pill-group" style="float: left">
269 <span class="pill-group" style="float: left">
203 %if filediff.patch['is_limited_diff']:
270 %if filediff.patch['is_limited_diff']:
204 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
271 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
205 %endif
272 %endif
206 %if RENAMED_FILENODE in stats['ops']:
273 %if RENAMED_FILENODE in stats['ops']:
207 <span class="pill" op="renamed">renamed</span>
274 <span class="pill" op="renamed">renamed</span>
208 %endif
275 %endif
209
276
210 %if NEW_FILENODE in stats['ops']:
277 %if NEW_FILENODE in stats['ops']:
211 <span class="pill" op="created">created</span>
278 <span class="pill" op="created">created</span>
212 %if filediff['target_mode'].startswith('120'):
279 %if filediff['target_mode'].startswith('120'):
213 <span class="pill" op="symlink">symlink</span>
280 <span class="pill" op="symlink">symlink</span>
214 %else:
281 %else:
215 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
282 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
216 %endif
283 %endif
217 %endif
284 %endif
218
285
219 %if DEL_FILENODE in stats['ops']:
286 %if DEL_FILENODE in stats['ops']:
220 <span class="pill" op="removed">removed</span>
287 <span class="pill" op="removed">removed</span>
221 %endif
288 %endif
222
289
223 %if CHMOD_FILENODE in stats['ops']:
290 %if CHMOD_FILENODE in stats['ops']:
224 <span class="pill" op="mode">
291 <span class="pill" op="mode">
225 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
292 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
226 </span>
293 </span>
227 %endif
294 %endif
228 </span>
295 </span>
229
296
230 <a class="pill filediff-anchor" href="#a_${h.FID(commit and commit.raw_id or '', filediff.patch['filename'])}">ΒΆ</a>
297 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
231
298
232 <span class="pill-group" style="float: right">
299 <span class="pill-group" style="float: right">
233 %if BIN_FILENODE in stats['ops']:
300 %if BIN_FILENODE in stats['ops']:
234 <span class="pill" op="binary">binary</span>
301 <span class="pill" op="binary">binary</span>
235 %if MOD_FILENODE in stats['ops']:
302 %if MOD_FILENODE in stats['ops']:
236 <span class="pill" op="modified">modified</span>
303 <span class="pill" op="modified">modified</span>
237 %endif
304 %endif
238 %endif
305 %endif
239 %if stats['added']:
306 %if stats['added']:
240 <span class="pill" op="added">+${stats['added']}</span>
307 <span class="pill" op="added">+${stats['added']}</span>
241 %endif
308 %endif
242 %if stats['deleted']:
309 %if stats['deleted']:
243 <span class="pill" op="deleted">-${stats['deleted']}</span>
310 <span class="pill" op="deleted">-${stats['deleted']}</span>
244 %endif
311 %endif
245 </span>
312 </span>
246
313
247 </%def>
314 </%def>
248
315
249 <%def name="nice_mode(filemode)">
316 <%def name="nice_mode(filemode)">
250 ${filemode.startswith('100') and filemode[3:] or filemode}
317 ${filemode.startswith('100') and filemode[3:] or filemode}
251 </%def>
318 </%def>
252
319
253 <%def name="diff_menu(filediff)">
320 <%def name="diff_menu(filediff, use_comments=False)">
254 <div class="filediff-menu">
321 <div class="filediff-menu">
255 %if filediff.diffset.source_ref:
322 %if filediff.diffset.source_ref:
256 %if filediff.patch['operation'] in ['D', 'M']:
323 %if filediff.patch['operation'] in ['D', 'M']:
257 <a
324 <a
258 class="tooltip"
325 class="tooltip"
259 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
326 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
260 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
327 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
261 >
328 >
262 ${_('Show file before')}
329 ${_('Show file before')}
263 </a>
330 </a>
264 %else:
331 %else:
265 <span
332 <span
266 class="tooltip"
333 class="tooltip"
267 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
334 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
268 >
335 >
269 ${_('Show file before')}
336 ${_('Show file before')}
270 </span>
337 </span>
271 %endif
338 %endif
272 %if filediff.patch['operation'] in ['A', 'M']:
339 %if filediff.patch['operation'] in ['A', 'M']:
273 <a
340 <a
274 class="tooltip"
341 class="tooltip"
275 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
342 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
276 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
343 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
277 >
344 >
278 ${_('Show file after')}
345 ${_('Show file after')}
279 </a>
346 </a>
280 %else:
347 %else:
281 <span
348 <span
282 class="tooltip"
349 class="tooltip"
283 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
350 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
284 >
351 >
285 ${_('Show file after')}
352 ${_('Show file after')}
286 </span>
353 </span>
287 %endif
354 %endif
288 <a
355 <a
289 class="tooltip"
356 class="tooltip"
290 title="${h.tooltip(_('Raw diff'))}"
357 title="${h.tooltip(_('Raw diff'))}"
291 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')}"
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')}"
292 >
359 >
293 ${_('Raw diff')}
360 ${_('Raw diff')}
294 </a>
361 </a>
295 <a
362 <a
296 class="tooltip"
363 class="tooltip"
297 title="${h.tooltip(_('Download diff'))}"
364 title="${h.tooltip(_('Download diff'))}"
298 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')}"
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')}"
299 >
366 >
300 ${_('Download diff')}
367 ${_('Download diff')}
301 </a>
368 </a>
369
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)
371 %if hasattr(c, 'ignorews_url'):
372 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
373 %endif
374 %if hasattr(c, 'context_url'):
375 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
376 %endif
377
378
379 %if use_comments:
380 <a href="#" onclick="$(this).closest('.filediff').toggleClass('hide-comments'); return false;">
381 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
382 </a>
383 %endif
302 %endif
384 %endif
303 </div>
385 </div>
304 </%def>
386 </%def>
305
387
306
388
307 <%def name="render_hunk_lines_sideside(hunk)">
389 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
390 <%def name="inline_comments_container(comments)">
391 <div class="inline-comments">
392 %for comment in comments:
393 ${commentblock.comment_block(comment, inline=True)}
394 %endfor
395 <span onclick="return Rhodecode.comments.createComment(this)"
396 class="btn btn-secondary cb-comment-add-button">
397 ${_('Add another comment')}
398 </span>
399 </div>
400 </%def>
401
402
403 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
308 %for i, line in enumerate(hunk.sideside):
404 %for i, line in enumerate(hunk.sideside):
309 <%
405 <%
310 old_line_anchor, new_line_anchor = None, None
406 old_line_anchor, new_line_anchor = None, None
311 if line.original.lineno:
407 if line.original.lineno:
312 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
408 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
313 if line.modified.lineno:
409 if line.modified.lineno:
314 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
410 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
315 %>
411 %>
316 <tr class="cb-line">
412 <tr class="cb-line">
317 <td class="cb-lineno ${action_class(line.original.action)}"
413 <td class="cb-lineno ${action_class(line.original.action)}"
318 data-line-number="${line.original.lineno}"
414 data-line-number="${line.original.lineno}"
319 %if old_line_anchor:
415 %if old_line_anchor:
320 id="${old_line_anchor}"
416 id="${old_line_anchor}"
321 %endif
417 %endif
322 >
418 >
323 %if line.original.lineno:
419 %if line.original.lineno:
324 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
420 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
325 %endif
421 %endif
326 </td>
422 </td>
327 <td class="cb-content ${action_class(line.original.action)}"
423 <td class="cb-content ${action_class(line.original.action)}"
328 data-line-number="o${line.original.lineno}"
424 data-line-number="o${line.original.lineno}"
329 ><span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
425 >
426 %if use_comments and line.original.lineno:
427 ${render_add_comment_button()}
428 %endif
429 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
430 %if use_comments and line.original.lineno and line.original.comments:
431 ${inline_comments_container(line.original.comments)}
432 %endif
330 </td>
433 </td>
331 <td class="cb-lineno ${action_class(line.modified.action)}"
434 <td class="cb-lineno ${action_class(line.modified.action)}"
332 data-line-number="${line.modified.lineno}"
435 data-line-number="${line.modified.lineno}"
333 %if new_line_anchor:
436 %if new_line_anchor:
334 id="${new_line_anchor}"
437 id="${new_line_anchor}"
335 %endif
438 %endif
336 >
439 >
337 %if line.modified.lineno:
440 %if line.modified.lineno:
338 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
441 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
339 %endif
442 %endif
340 </td>
443 </td>
341 <td class="cb-content ${action_class(line.modified.action)}"
444 <td class="cb-content ${action_class(line.modified.action)}"
342 data-line-number="n${line.modified.lineno}"
445 data-line-number="n${line.modified.lineno}"
343 >
446 >
447 %if use_comments and line.modified.lineno:
448 ${render_add_comment_button()}
449 %endif
344 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
450 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
451 %if use_comments and line.modified.lineno and line.modified.comments:
452 ${inline_comments_container(line.modified.comments)}
453 %endif
345 </td>
454 </td>
346 </tr>
455 </tr>
347 %endfor
456 %endfor
348 </%def>
457 </%def>
349
458
350
459
351 <%def name="render_hunk_lines_unified(hunk)">
460 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
352 %for old_line_no, new_line_no, action, content in hunk.unified:
461 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
353 <%
462 <%
354 old_line_anchor, new_line_anchor = None, None
463 old_line_anchor, new_line_anchor = None, None
355 if old_line_no:
464 if old_line_no:
356 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
465 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
357 if new_line_no:
466 if new_line_no:
358 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
467 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
359 %>
468 %>
360 <tr class="cb-line">
469 <tr class="cb-line">
361 <td class="cb-lineno ${action_class(action)}"
470 <td class="cb-lineno ${action_class(action)}"
362 data-line-number="${old_line_no}"
471 data-line-number="${old_line_no}"
363 %if old_line_anchor:
472 %if old_line_anchor:
364 id="${old_line_anchor}"
473 id="${old_line_anchor}"
365 %endif
474 %endif
366 >
475 >
367 %if old_line_anchor:
476 %if old_line_anchor:
368 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
477 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
369 %endif
478 %endif
370 </td>
479 </td>
371 <td class="cb-lineno ${action_class(action)}"
480 <td class="cb-lineno ${action_class(action)}"
372 data-line-number="${new_line_no}"
481 data-line-number="${new_line_no}"
373 %if new_line_anchor:
482 %if new_line_anchor:
374 id="${new_line_anchor}"
483 id="${new_line_anchor}"
375 %endif
484 %endif
376 >
485 >
377 %if new_line_anchor:
486 %if new_line_anchor:
378 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
487 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
379 %endif
488 %endif
380 </td>
489 </td>
381 <td class="cb-content ${action_class(action)}"
490 <td class="cb-content ${action_class(action)}"
382 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
491 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
383 ><span class="cb-code">${action} ${content or '' | n}</span>
492 >
384 </td>
493 %if use_comments:
494 ${render_add_comment_button()}
495 %endif
496 <span class="cb-code">${action} ${content or '' | n}</span>
497 %if use_comments and comments:
498 ${inline_comments_container(comments)}
499 %endif
500 </td>
385 </tr>
501 </tr>
386 %endfor
502 %endfor
387 </%def>
503 </%def>
388
504
505 <%def name="render_add_comment_button()">
506 <button
507 class="btn btn-small btn-primary cb-comment-box-opener"
508 onclick="return Rhodecode.comments.createComment(this)"
509 >+</button>
510 </%def>
389
511
390 <%def name="render_diffset_menu()">
512 <%def name="render_diffset_menu()">
391 <div class="diffset-menu clearinner">
513 <div class="diffset-menu clearinner">
392 <div class="pull-right">
514 <div class="pull-right">
393 <div class="btn-group">
515 <div class="btn-group">
394 <a
516 <a
395 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
517 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
396 title="${_('View side by side')}"
518 title="${_('View side by side')}"
397 href="${h.url_replace(diffmode='sideside')}">
519 href="${h.url_replace(diffmode='sideside')}">
398 <span>${_('Side by Side')}</span>
520 <span>${_('Side by Side')}</span>
399 </a>
521 </a>
400 <a
522 <a
401 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
523 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
402 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
524 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
403 <span>${_('Unified')}</span>
525 <span>${_('Unified')}</span>
404 </a>
526 </a>
405 </div>
527 </div>
406 </div>
528 </div>
407 <div class="pull-left">
529 <div class="pull-left">
408 <div class="btn-group">
530 <div class="btn-group">
409 <a
531 <a
410 class="btn"
532 class="btn"
411 href="#"
533 href="#"
412 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
534 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
413 <a
535 <a
414 class="btn"
536 class="btn"
415 href="#"
537 href="#"
416 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
538 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
417 </div>
539 </div>
418 </div>
540 </div>
419 </div>
541 </div>
420 </%def>
542 </%def>
@@ -1,299 +1,302 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 import pytest
21 import pytest
22
22
23 from rhodecode.lib.helpers import _shorten_commit_id
23 from rhodecode.lib.helpers import _shorten_commit_id
24 from rhodecode.tests import url
24 from rhodecode.tests import url
25
25
26
26
27 @pytest.mark.usefixtures("app")
27 @pytest.mark.usefixtures("app")
28 class TestChangesetController(object):
28 class TestChangesetController(object):
29
29
30 def test_index(self, backend):
30 def test_index(self, backend):
31 commit_id = self.commit_id[backend.alias]
31 commit_id = self.commit_id[backend.alias]
32 response = self.app.get(url(
32 response = self.app.get(url(
33 controller='changeset', action='index',
33 controller='changeset', action='index',
34 repo_name=backend.repo_name, revision=commit_id))
34 repo_name=backend.repo_name, revision=commit_id))
35 response.mustcontain('Added a symlink')
35 response.mustcontain('Added a symlink')
36 response.mustcontain(commit_id)
36 response.mustcontain(commit_id)
37 response.mustcontain('No newline at end of file')
37 response.mustcontain('No newline at end of file')
38
38
39 def test_index_raw(self, backend):
39 def test_index_raw(self, backend):
40 commit_id = self.commit_id[backend.alias]
40 commit_id = self.commit_id[backend.alias]
41 response = self.app.get(url(
41 response = self.app.get(url(
42 controller='changeset', action='changeset_raw',
42 controller='changeset', action='changeset_raw',
43 repo_name=backend.repo_name, revision=commit_id))
43 repo_name=backend.repo_name, revision=commit_id))
44 assert response.body == self.diffs[backend.alias]
44 assert response.body == self.diffs[backend.alias]
45
45
46 def test_index_raw_patch(self, backend):
46 def test_index_raw_patch(self, backend):
47 response = self.app.get(url(
47 response = self.app.get(url(
48 controller='changeset', action='changeset_patch',
48 controller='changeset', action='changeset_patch',
49 repo_name=backend.repo_name,
49 repo_name=backend.repo_name,
50 revision=self.commit_id[backend.alias]))
50 revision=self.commit_id[backend.alias]))
51 assert response.body == self.patches[backend.alias]
51 assert response.body == self.patches[backend.alias]
52
52
53 def test_index_changeset_download(self, backend):
53 def test_index_changeset_download(self, backend):
54 response = self.app.get(url(
54 response = self.app.get(url(
55 controller='changeset', action='changeset_download',
55 controller='changeset', action='changeset_download',
56 repo_name=backend.repo_name,
56 repo_name=backend.repo_name,
57 revision=self.commit_id[backend.alias]))
57 revision=self.commit_id[backend.alias]))
58 assert response.body == self.diffs[backend.alias]
58 assert response.body == self.diffs[backend.alias]
59
59
60 def test_single_commit_page_different_ops(self, backend):
60 def test_single_commit_page_different_ops(self, backend):
61 commit_id = {
61 commit_id = {
62 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
62 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
63 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
63 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
64 'svn': '337',
64 'svn': '337',
65 }
65 }
66 commit_id = commit_id[backend.alias]
66 commit_id = commit_id[backend.alias]
67 response = self.app.get(url(
67 response = self.app.get(url(
68 controller='changeset', action='index',
68 controller='changeset', action='index',
69 repo_name=backend.repo_name, revision=commit_id))
69 repo_name=backend.repo_name, revision=commit_id))
70
70
71 response.mustcontain(_shorten_commit_id(commit_id))
71 response.mustcontain(_shorten_commit_id(commit_id))
72 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
72 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
73
73
74 # files op files
74 # files op files
75 response.mustcontain('File no longer present at commit: %s' %
75 response.mustcontain('File no longer present at commit: %s' %
76 _shorten_commit_id(commit_id))
76 _shorten_commit_id(commit_id))
77
77
78 # svn uses a different filename
78 # svn uses a different filename
79 if backend.alias == 'svn':
79 if backend.alias == 'svn':
80 response.mustcontain('new file 10644')
80 response.mustcontain('new file 10644')
81 else:
81 else:
82 response.mustcontain('new file 100644')
82 response.mustcontain('new file 100644')
83 response.mustcontain('Changed theme to ADC theme') # commit msg
83 response.mustcontain('Changed theme to ADC theme') # commit msg
84
84
85 self._check_diff_menus(response, right_menu=True)
85 self._check_new_diff_menus(response, right_menu=True)
86
86
87 def test_commit_range_page_different_ops(self, backend):
87 def test_commit_range_page_different_ops(self, backend):
88 commit_id_range = {
88 commit_id_range = {
89 'hg': (
89 'hg': (
90 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
90 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
92 'git': (
92 'git': (
93 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
93 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
95 'svn': (
95 'svn': (
96 '335',
96 '335',
97 '337'),
97 '337'),
98 }
98 }
99 commit_ids = commit_id_range[backend.alias]
99 commit_ids = commit_id_range[backend.alias]
100 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
100 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
101 response = self.app.get(url(
101 response = self.app.get(url(
102 controller='changeset', action='index',
102 controller='changeset', action='index',
103 repo_name=backend.repo_name, revision=commit_id))
103 repo_name=backend.repo_name, revision=commit_id))
104
104
105 response.mustcontain(_shorten_commit_id(commit_ids[0]))
105 response.mustcontain(_shorten_commit_id(commit_ids[0]))
106 response.mustcontain(_shorten_commit_id(commit_ids[1]))
106 response.mustcontain(_shorten_commit_id(commit_ids[1]))
107
107
108 # svn is special
108 # svn is special
109 if backend.alias == 'svn':
109 if backend.alias == 'svn':
110 response.mustcontain('new file 10644')
110 response.mustcontain('new file 10644')
111 response.mustcontain('34 files changed: 1184 inserted, 311 deleted')
111 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
112 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
113 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
112 else:
114 else:
113 response.mustcontain('new file 100644')
115 response.mustcontain('new file 100644')
114 response.mustcontain('33 files changed: 1165 inserted, 308 deleted')
116 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
117 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
115
118
116 # files op files
119 # files op files
117 response.mustcontain('File no longer present at commit: %s' %
120 response.mustcontain('File no longer present at commit: %s' %
118 _shorten_commit_id(commit_ids[1]))
121 _shorten_commit_id(commit_ids[1]))
119 response.mustcontain('Added docstrings to vcs.cli') # commit msg
122 response.mustcontain('Added docstrings to vcs.cli') # commit msg
120 response.mustcontain('Changed theme to ADC theme') # commit msg
123 response.mustcontain('Changed theme to ADC theme') # commit msg
121
124
122 self._check_diff_menus(response)
125 self._check_new_diff_menus(response)
123
126
124 def test_combined_compare_commit_page_different_ops(self, backend):
127 def test_combined_compare_commit_page_different_ops(self, backend):
125 commit_id_range = {
128 commit_id_range = {
126 'hg': (
129 'hg': (
127 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
130 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
128 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
131 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
129 'git': (
132 'git': (
130 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
133 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
131 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
134 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
132 'svn': (
135 'svn': (
133 '335',
136 '335',
134 '337'),
137 '337'),
135 }
138 }
136 commit_ids = commit_id_range[backend.alias]
139 commit_ids = commit_id_range[backend.alias]
137 response = self.app.get(url(
140 response = self.app.get(url(
138 controller='compare', action='compare',
141 controller='compare', action='compare',
139 repo_name=backend.repo_name,
142 repo_name=backend.repo_name,
140 source_ref_type='rev', source_ref=commit_ids[0],
143 source_ref_type='rev', source_ref=commit_ids[0],
141 target_ref_type='rev', target_ref=commit_ids[1], ))
144 target_ref_type='rev', target_ref=commit_ids[1], ))
142
145
143 response.mustcontain(_shorten_commit_id(commit_ids[0]))
146 response.mustcontain(_shorten_commit_id(commit_ids[0]))
144 response.mustcontain(_shorten_commit_id(commit_ids[1]))
147 response.mustcontain(_shorten_commit_id(commit_ids[1]))
145
148
146 # files op files
149 # files op files
147 response.mustcontain('File no longer present at commit: %s' %
150 response.mustcontain('File no longer present at commit: %s' %
148 _shorten_commit_id(commit_ids[1]))
151 _shorten_commit_id(commit_ids[1]))
149
152
150 # svn is special
153 # svn is special
151 if backend.alias == 'svn':
154 if backend.alias == 'svn':
152 response.mustcontain('new file 10644')
155 response.mustcontain('new file 10644')
153 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
156 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
154 else:
157 else:
155 response.mustcontain('new file 100644')
158 response.mustcontain('new file 100644')
156 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
159 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
157
160
158 response.mustcontain('Added docstrings to vcs.cli') # commit msg
161 response.mustcontain('Added docstrings to vcs.cli') # commit msg
159 response.mustcontain('Changed theme to ADC theme') # commit msg
162 response.mustcontain('Changed theme to ADC theme') # commit msg
160
163
161 self._check_new_diff_menus(response)
164 self._check_new_diff_menus(response)
162
165
163 def test_changeset_range(self, backend):
166 def test_changeset_range(self, backend):
164 self._check_changeset_range(
167 self._check_changeset_range(
165 backend, self.commit_id_range, self.commit_id_range_result)
168 backend, self.commit_id_range, self.commit_id_range_result)
166
169
167 def test_changeset_range_with_initial_commit(self, backend):
170 def test_changeset_range_with_initial_commit(self, backend):
168 commit_id_range = {
171 commit_id_range = {
169 'hg': (
172 'hg': (
170 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
173 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
171 '...6cba7170863a2411822803fa77a0a264f1310b35'),
174 '...6cba7170863a2411822803fa77a0a264f1310b35'),
172 'git': (
175 'git': (
173 'c1214f7e79e02fc37156ff215cd71275450cffc3'
176 'c1214f7e79e02fc37156ff215cd71275450cffc3'
174 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
177 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
175 'svn': '1...3',
178 'svn': '1...3',
176 }
179 }
177 commit_id_range_result = {
180 commit_id_range_result = {
178 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
181 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
179 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
182 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
180 'svn': ['1', '2', '3'],
183 'svn': ['1', '2', '3'],
181 }
184 }
182 self._check_changeset_range(
185 self._check_changeset_range(
183 backend, commit_id_range, commit_id_range_result)
186 backend, commit_id_range, commit_id_range_result)
184
187
185 def _check_changeset_range(
188 def _check_changeset_range(
186 self, backend, commit_id_ranges, commit_id_range_result):
189 self, backend, commit_id_ranges, commit_id_range_result):
187 response = self.app.get(
190 response = self.app.get(
188 url(controller='changeset', action='index',
191 url(controller='changeset', action='index',
189 repo_name=backend.repo_name,
192 repo_name=backend.repo_name,
190 revision=commit_id_ranges[backend.alias]))
193 revision=commit_id_ranges[backend.alias]))
191 expected_result = commit_id_range_result[backend.alias]
194 expected_result = commit_id_range_result[backend.alias]
192 response.mustcontain('{} commits'.format(len(expected_result)))
195 response.mustcontain('{} commits'.format(len(expected_result)))
193 for commit_id in expected_result:
196 for commit_id in expected_result:
194 response.mustcontain(commit_id)
197 response.mustcontain(commit_id)
195
198
196 commit_id = {
199 commit_id = {
197 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
200 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
198 'svn': '393',
201 'svn': '393',
199 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
202 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
200 }
203 }
201
204
202 commit_id_range = {
205 commit_id_range = {
203 'hg': (
206 'hg': (
204 'a53d9201d4bc278910d416d94941b7ea007ecd52'
207 'a53d9201d4bc278910d416d94941b7ea007ecd52'
205 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
208 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
206 'git': (
209 'git': (
207 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
210 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
208 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
211 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
209 'svn': '391...393',
212 'svn': '391...393',
210 }
213 }
211
214
212 commit_id_range_result = {
215 commit_id_range_result = {
213 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
216 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
214 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
217 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
215 'svn': ['391', '392', '393'],
218 'svn': ['391', '392', '393'],
216 }
219 }
217
220
218 diffs = {
221 diffs = {
219 'hg': r"""diff --git a/README b/README
222 'hg': r"""diff --git a/README b/README
220 new file mode 120000
223 new file mode 120000
221 --- /dev/null
224 --- /dev/null
222 +++ b/README
225 +++ b/README
223 @@ -0,0 +1,1 @@
226 @@ -0,0 +1,1 @@
224 +README.rst
227 +README.rst
225 \ No newline at end of file
228 \ No newline at end of file
226 """,
229 """,
227 'git': r"""diff --git a/README b/README
230 'git': r"""diff --git a/README b/README
228 new file mode 120000
231 new file mode 120000
229 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
232 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
230 --- /dev/null
233 --- /dev/null
231 +++ b/README
234 +++ b/README
232 @@ -0,0 +1 @@
235 @@ -0,0 +1 @@
233 +README.rst
236 +README.rst
234 \ No newline at end of file
237 \ No newline at end of file
235 """,
238 """,
236 'svn': """Index: README
239 'svn': """Index: README
237 ===================================================================
240 ===================================================================
238 diff --git a/README b/README
241 diff --git a/README b/README
239 new file mode 10644
242 new file mode 10644
240 --- /dev/null\t(revision 0)
243 --- /dev/null\t(revision 0)
241 +++ b/README\t(revision 393)
244 +++ b/README\t(revision 393)
242 @@ -0,0 +1 @@
245 @@ -0,0 +1 @@
243 +link README.rst
246 +link README.rst
244 \\ No newline at end of file
247 \\ No newline at end of file
245 """,
248 """,
246 }
249 }
247
250
248 patches = {
251 patches = {
249 'hg': r"""# HG changeset patch
252 'hg': r"""# HG changeset patch
250 # User Marcin Kuzminski <marcin@python-works.com>
253 # User Marcin Kuzminski <marcin@python-works.com>
251 # Date 2014-01-07 12:21:40
254 # Date 2014-01-07 12:21:40
252 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
255 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
253 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
256 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
254
257
255 Added a symlink
258 Added a symlink
256
259
257 """ + diffs['hg'],
260 """ + diffs['hg'],
258 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
261 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
259 From: Marcin Kuzminski <marcin@python-works.com>
262 From: Marcin Kuzminski <marcin@python-works.com>
260 Date: 2014-01-07 12:22:20
263 Date: 2014-01-07 12:22:20
261 Subject: [PATCH] Added a symlink
264 Subject: [PATCH] Added a symlink
262
265
263 ---
266 ---
264
267
265 """ + diffs['git'],
268 """ + diffs['git'],
266 'svn': r"""# SVN changeset patch
269 'svn': r"""# SVN changeset patch
267 # User marcin
270 # User marcin
268 # Date 2014-09-02 12:25:22.071142
271 # Date 2014-09-02 12:25:22.071142
269 # Revision 393
272 # Revision 393
270
273
271 Added a symlink
274 Added a symlink
272
275
273 """ + diffs['svn'],
276 """ + diffs['svn'],
274 }
277 }
275
278
276 def _check_diff_menus(self, response, right_menu=False,):
279 def _check_diff_menus(self, response, right_menu=False,):
277 # diff menus
280 # diff menus
278 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
281 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
279 'Raw Diff', 'Download Diff']:
282 'Raw Diff', 'Download Diff']:
280 response.mustcontain(elem)
283 response.mustcontain(elem)
281
284
282 # right pane diff menus
285 # right pane diff menus
283 if right_menu:
286 if right_menu:
284 for elem in ['Ignore whitespace', 'Increase context',
287 for elem in ['Ignore whitespace', 'Increase context',
285 'Hide comments']:
288 'Hide comments']:
286 response.mustcontain(elem)
289 response.mustcontain(elem)
287
290
288
291
289 def _check_new_diff_menus(self, response, right_menu=False,):
292 def _check_new_diff_menus(self, response, right_menu=False,):
290 # diff menus
293 # diff menus
291 for elem in ['Show file before', 'Show file after',
294 for elem in ['Show file before', 'Show file after',
292 'Raw diff', 'Download diff']:
295 'Raw diff', 'Download diff']:
293 response.mustcontain(elem)
296 response.mustcontain(elem)
294
297
295 # right pane diff menus
298 # right pane diff menus
296 if right_menu:
299 if right_menu:
297 for elem in ['Ignore whitespace', 'Increase context',
300 for elem in ['Ignore whitespace', 'Increase context',
298 'Hide comments']:
301 'Hide comments']:
299 response.mustcontain(elem)
302 response.mustcontain(elem)
@@ -1,277 +1,282 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 from pylons.i18n import ungettext
21 from pylons.i18n import ungettext
22 import pytest
22 import pytest
23
23
24 from rhodecode.tests import *
24 from rhodecode.tests import *
25 from rhodecode.model.db import (
25 from rhodecode.model.db import (
26 ChangesetComment, Notification, UserNotification)
26 ChangesetComment, Notification, UserNotification)
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29
29
30
30
31 @pytest.mark.backends("git", "hg", "svn")
31 @pytest.mark.backends("git", "hg", "svn")
32 class TestCommitCommentsController(TestController):
32 class TestCommitCommentsController(TestController):
33
33
34 @pytest.fixture(autouse=True)
34 @pytest.fixture(autouse=True)
35 def prepare(self, request, pylonsapp):
35 def prepare(self, request, pylonsapp):
36 for x in ChangesetComment.query().all():
36 for x in ChangesetComment.query().all():
37 Session().delete(x)
37 Session().delete(x)
38 Session().commit()
38 Session().commit()
39
39
40 for x in Notification.query().all():
40 for x in Notification.query().all():
41 Session().delete(x)
41 Session().delete(x)
42 Session().commit()
42 Session().commit()
43
43
44 request.addfinalizer(self.cleanup)
44 request.addfinalizer(self.cleanup)
45
45
46 def cleanup(self):
46 def cleanup(self):
47 for x in ChangesetComment.query().all():
47 for x in ChangesetComment.query().all():
48 Session().delete(x)
48 Session().delete(x)
49 Session().commit()
49 Session().commit()
50
50
51 for x in Notification.query().all():
51 for x in Notification.query().all():
52 Session().delete(x)
52 Session().delete(x)
53 Session().commit()
53 Session().commit()
54
54
55 def test_create(self, backend):
55 def test_create(self, backend):
56 self.log_user()
56 self.log_user()
57 commit = backend.repo.get_commit('300')
57 commit = backend.repo.get_commit('300')
58 commit_id = commit.raw_id
58 commit_id = commit.raw_id
59 text = u'CommentOnCommit'
59 text = u'CommentOnCommit'
60
60
61 params = {'text': text, 'csrf_token': self.csrf_token}
61 params = {'text': text, 'csrf_token': self.csrf_token}
62 self.app.post(
62 self.app.post(
63 url(controller='changeset', action='comment',
63 url(controller='changeset', action='comment',
64 repo_name=backend.repo_name, revision=commit_id), params=params)
64 repo_name=backend.repo_name, revision=commit_id), params=params)
65
65
66 response = self.app.get(
66 response = self.app.get(
67 url(controller='changeset', action='index',
67 url(controller='changeset', action='index',
68 repo_name=backend.repo_name, revision=commit_id))
68 repo_name=backend.repo_name, revision=commit_id))
69
69
70 # test DB
70 # test DB
71 assert ChangesetComment.query().count() == 1
71 assert ChangesetComment.query().count() == 1
72 assert_comment_links(response, ChangesetComment.query().count(), 0)
72 assert_comment_links(response, ChangesetComment.query().count(), 0)
73
73
74 assert Notification.query().count() == 1
74 assert Notification.query().count() == 1
75 assert ChangesetComment.query().count() == 1
75 assert ChangesetComment.query().count() == 1
76
76
77 notification = Notification.query().all()[0]
77 notification = Notification.query().all()[0]
78
78
79 comment_id = ChangesetComment.query().first().comment_id
79 comment_id = ChangesetComment.query().first().comment_id
80 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
80 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
81
81
82 sbj = 'commented on commit `{0}` in the {1} repository'.format(
82 sbj = 'commented on commit `{0}` in the {1} repository'.format(
83 h.show_id(commit), backend.repo_name)
83 h.show_id(commit), backend.repo_name)
84 assert sbj in notification.subject
84 assert sbj in notification.subject
85
85
86 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
86 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
87 backend.repo_name, commit_id, comment_id))
87 backend.repo_name, commit_id, comment_id))
88 assert lnk in notification.body
88 assert lnk in notification.body
89
89
90 def test_create_inline(self, backend):
90 def test_create_inline(self, backend):
91 self.log_user()
91 self.log_user()
92 commit = backend.repo.get_commit('300')
92 commit = backend.repo.get_commit('300')
93 commit_id = commit.raw_id
93 commit_id = commit.raw_id
94 text = u'CommentOnCommit'
94 text = u'CommentOnCommit'
95 f_path = 'vcs/web/simplevcs/views/repository.py'
95 f_path = 'vcs/web/simplevcs/views/repository.py'
96 line = 'n1'
96 line = 'n1'
97
97
98 params = {'text': text, 'f_path': f_path, 'line': line,
98 params = {'text': text, 'f_path': f_path, 'line': line,
99 'csrf_token': self.csrf_token}
99 'csrf_token': self.csrf_token}
100
100
101 self.app.post(
101 self.app.post(
102 url(controller='changeset', action='comment',
102 url(controller='changeset', action='comment',
103 repo_name=backend.repo_name, revision=commit_id), params=params)
103 repo_name=backend.repo_name, revision=commit_id), params=params)
104
104
105 response = self.app.get(
105 response = self.app.get(
106 url(controller='changeset', action='index',
106 url(controller='changeset', action='index',
107 repo_name=backend.repo_name, revision=commit_id))
107 repo_name=backend.repo_name, revision=commit_id))
108
108
109 # test DB
109 # test DB
110 assert ChangesetComment.query().count() == 1
110 assert ChangesetComment.query().count() == 1
111 assert_comment_links(response, 0, ChangesetComment.query().count())
111 assert_comment_links(response, 0, ChangesetComment.query().count())
112 response.mustcontain(
112
113 '''class="inline-comment-placeholder" '''
113 if backend.alias == 'svn':
114 '''path="vcs/web/simplevcs/views/repository.py" '''
114 response.mustcontain(
115 '''target_id="vcswebsimplevcsviewsrepositorypy"'''
115 '''data-f-path="vcs/commands/summary.py" '''
116 )
116 '''id="a_c--ad05457a43f8"'''
117 )
118 else:
119 response.mustcontain(
120 '''data-f-path="vcs/backends/hg.py" '''
121 '''id="a_c--9c390eb52cd6"'''
122 )
117
123
118 assert Notification.query().count() == 1
124 assert Notification.query().count() == 1
119 assert ChangesetComment.query().count() == 1
125 assert ChangesetComment.query().count() == 1
120
126
121 notification = Notification.query().all()[0]
127 notification = Notification.query().all()[0]
122 comment = ChangesetComment.query().first()
128 comment = ChangesetComment.query().first()
123 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
129 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
124
130
125 assert comment.revision == commit_id
131 assert comment.revision == commit_id
126 sbj = 'commented on commit `{commit}` ' \
132 sbj = 'commented on commit `{commit}` ' \
127 '(file: `{f_path}`) in the {repo} repository'.format(
133 '(file: `{f_path}`) in the {repo} repository'.format(
128 commit=h.show_id(commit),
134 commit=h.show_id(commit),
129 f_path=f_path, line=line, repo=backend.repo_name)
135 f_path=f_path, line=line, repo=backend.repo_name)
130 assert sbj in notification.subject
136 assert sbj in notification.subject
131
137
132 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
138 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
133 backend.repo_name, commit_id, comment.comment_id))
139 backend.repo_name, commit_id, comment.comment_id))
134 assert lnk in notification.body
140 assert lnk in notification.body
135 assert 'on line n1' in notification.body
141 assert 'on line n1' in notification.body
136
142
137 def test_create_with_mention(self, backend):
143 def test_create_with_mention(self, backend):
138 self.log_user()
144 self.log_user()
139
145
140 commit_id = backend.repo.get_commit('300').raw_id
146 commit_id = backend.repo.get_commit('300').raw_id
141 text = u'@test_regular check CommentOnCommit'
147 text = u'@test_regular check CommentOnCommit'
142
148
143 params = {'text': text, 'csrf_token': self.csrf_token}
149 params = {'text': text, 'csrf_token': self.csrf_token}
144 self.app.post(
150 self.app.post(
145 url(controller='changeset', action='comment',
151 url(controller='changeset', action='comment',
146 repo_name=backend.repo_name, revision=commit_id), params=params)
152 repo_name=backend.repo_name, revision=commit_id), params=params)
147
153
148 response = self.app.get(
154 response = self.app.get(
149 url(controller='changeset', action='index',
155 url(controller='changeset', action='index',
150 repo_name=backend.repo_name, revision=commit_id))
156 repo_name=backend.repo_name, revision=commit_id))
151 # test DB
157 # test DB
152 assert ChangesetComment.query().count() == 1
158 assert ChangesetComment.query().count() == 1
153 assert_comment_links(response, ChangesetComment.query().count(), 0)
159 assert_comment_links(response, ChangesetComment.query().count(), 0)
154
160
155 notification = Notification.query().one()
161 notification = Notification.query().one()
156
162
157 assert len(notification.recipients) == 2
163 assert len(notification.recipients) == 2
158 users = [x.username for x in notification.recipients]
164 users = [x.username for x in notification.recipients]
159
165
160 # test_regular gets notification by @mention
166 # test_regular gets notification by @mention
161 assert sorted(users) == [u'test_admin', u'test_regular']
167 assert sorted(users) == [u'test_admin', u'test_regular']
162
168
163 def test_create_with_status_change(self, backend):
169 def test_create_with_status_change(self, backend):
164 self.log_user()
170 self.log_user()
165 commit = backend.repo.get_commit('300')
171 commit = backend.repo.get_commit('300')
166 commit_id = commit.raw_id
172 commit_id = commit.raw_id
167 text = u'CommentOnCommit'
173 text = u'CommentOnCommit'
168 f_path = 'vcs/web/simplevcs/views/repository.py'
174 f_path = 'vcs/web/simplevcs/views/repository.py'
169 line = 'n1'
175 line = 'n1'
170
176
171 params = {'text': text, 'changeset_status': 'approved',
177 params = {'text': text, 'changeset_status': 'approved',
172 'csrf_token': self.csrf_token}
178 'csrf_token': self.csrf_token}
173
179
174 self.app.post(
180 self.app.post(
175 url(controller='changeset', action='comment',
181 url(controller='changeset', action='comment',
176 repo_name=backend.repo_name, revision=commit_id), params=params)
182 repo_name=backend.repo_name, revision=commit_id), params=params)
177
183
178 response = self.app.get(
184 response = self.app.get(
179 url(controller='changeset', action='index',
185 url(controller='changeset', action='index',
180 repo_name=backend.repo_name, revision=commit_id))
186 repo_name=backend.repo_name, revision=commit_id))
181
187
182 # test DB
188 # test DB
183 assert ChangesetComment.query().count() == 1
189 assert ChangesetComment.query().count() == 1
184 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 assert_comment_links(response, ChangesetComment.query().count(), 0)
185
191
186 assert Notification.query().count() == 1
192 assert Notification.query().count() == 1
187 assert ChangesetComment.query().count() == 1
193 assert ChangesetComment.query().count() == 1
188
194
189 notification = Notification.query().all()[0]
195 notification = Notification.query().all()[0]
190
196
191 comment_id = ChangesetComment.query().first().comment_id
197 comment_id = ChangesetComment.query().first().comment_id
192 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
198 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
193
199
194 sbj = 'commented on commit `{0}` (status: Approved) ' \
200 sbj = 'commented on commit `{0}` (status: Approved) ' \
195 'in the {1} repository'.format(
201 'in the {1} repository'.format(
196 h.show_id(commit), backend.repo_name)
202 h.show_id(commit), backend.repo_name)
197 assert sbj in notification.subject
203 assert sbj in notification.subject
198
204
199 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
205 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
200 backend.repo_name, commit_id, comment_id))
206 backend.repo_name, commit_id, comment_id))
201 assert lnk in notification.body
207 assert lnk in notification.body
202
208
203 def test_delete(self, backend):
209 def test_delete(self, backend):
204 self.log_user()
210 self.log_user()
205 commit_id = backend.repo.get_commit('300').raw_id
211 commit_id = backend.repo.get_commit('300').raw_id
206 text = u'CommentOnCommit'
212 text = u'CommentOnCommit'
207
213
208 params = {'text': text, 'csrf_token': self.csrf_token}
214 params = {'text': text, 'csrf_token': self.csrf_token}
209 self.app.post(
215 self.app.post(
210 url(
216 url(
211 controller='changeset', action='comment',
217 controller='changeset', action='comment',
212 repo_name=backend.repo_name, revision=commit_id),
218 repo_name=backend.repo_name, revision=commit_id),
213 params=params)
219 params=params)
214
220
215 comments = ChangesetComment.query().all()
221 comments = ChangesetComment.query().all()
216 assert len(comments) == 1
222 assert len(comments) == 1
217 comment_id = comments[0].comment_id
223 comment_id = comments[0].comment_id
218
224
219 self.app.post(
225 self.app.post(
220 url(controller='changeset', action='delete_comment',
226 url(controller='changeset', action='delete_comment',
221 repo_name=backend.repo_name, comment_id=comment_id),
227 repo_name=backend.repo_name, comment_id=comment_id),
222 params={'_method': 'delete', 'csrf_token': self.csrf_token})
228 params={'_method': 'delete', 'csrf_token': self.csrf_token})
223
229
224 comments = ChangesetComment.query().all()
230 comments = ChangesetComment.query().all()
225 assert len(comments) == 0
231 assert len(comments) == 0
226
232
227 response = self.app.get(
233 response = self.app.get(
228 url(controller='changeset', action='index',
234 url(controller='changeset', action='index',
229 repo_name=backend.repo_name, revision=commit_id))
235 repo_name=backend.repo_name, revision=commit_id))
230 assert_comment_links(response, 0, 0)
236 assert_comment_links(response, 0, 0)
231
237
232 @pytest.mark.parametrize('renderer, input, output', [
238 @pytest.mark.parametrize('renderer, input, output', [
233 ('rst', 'plain text', '<p>plain text</p>'),
239 ('rst', 'plain text', '<p>plain text</p>'),
234 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
240 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
235 ('rst', '*italics*', '<em>italics</em>'),
241 ('rst', '*italics*', '<em>italics</em>'),
236 ('rst', '**bold**', '<strong>bold</strong>'),
242 ('rst', '**bold**', '<strong>bold</strong>'),
237 ('markdown', 'plain text', '<p>plain text</p>'),
243 ('markdown', 'plain text', '<p>plain text</p>'),
238 ('markdown', '# header', '<h1>header</h1>'),
244 ('markdown', '# header', '<h1>header</h1>'),
239 ('markdown', '*italics*', '<em>italics</em>'),
245 ('markdown', '*italics*', '<em>italics</em>'),
240 ('markdown', '**bold**', '<strong>bold</strong>'),
246 ('markdown', '**bold**', '<strong>bold</strong>'),
241 ])
247 ])
242 def test_preview(self, renderer, input, output, backend):
248 def test_preview(self, renderer, input, output, backend):
243 self.log_user()
249 self.log_user()
244 params = {
250 params = {
245 'renderer': renderer,
251 'renderer': renderer,
246 'text': input,
252 'text': input,
247 'csrf_token': self.csrf_token
253 'csrf_token': self.csrf_token
248 }
254 }
249 environ = {
255 environ = {
250 'HTTP_X_PARTIAL_XHR': 'true'
256 'HTTP_X_PARTIAL_XHR': 'true'
251 }
257 }
252 response = self.app.post(
258 response = self.app.post(
253 url(controller='changeset',
259 url(controller='changeset',
254 action='preview_comment',
260 action='preview_comment',
255 repo_name=backend.repo_name),
261 repo_name=backend.repo_name),
256 params=params,
262 params=params,
257 extra_environ=environ)
263 extra_environ=environ)
258
264
259 response.mustcontain(output)
265 response.mustcontain(output)
260
266
261
267
262 def assert_comment_links(response, comments, inline_comments):
268 def assert_comment_links(response, comments, inline_comments):
263 comments_text = ungettext("%d Commit comment",
269 comments_text = ungettext("%d Commit comment",
264 "%d Commit comments", comments) % comments
270 "%d Commit comments", comments) % comments
265 if comments:
271 if comments:
266 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
272 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
267 else:
273 else:
268 response.mustcontain(comments_text)
274 response.mustcontain(comments_text)
269
275
270 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
276 inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
271 inline_comments) % inline_comments
277 inline_comments) % inline_comments
272 if inline_comments:
278 if inline_comments:
273 response.mustcontain(
279 response.mustcontain(
274 '<a href="#inline-comments" '
280 'id="inline-comments-counter">%s</' % inline_comments_text)
275 'id="inline-comments-counter">%s</a>' % inline_comments_text)
276 else:
281 else:
277 response.mustcontain(inline_comments_text)
282 response.mustcontain(inline_comments_text)
General Comments 0
You need to be logged in to leave comments. Login now