##// END OF EJS Templates
pull-requests: updated versioning support....
marcink -
r1268:7eb711c7 default
parent child Browse files
Show More
@@ -1,464 +1,468 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import ChangesetCommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 159
160 160 # fetch global flags of ignore ws or context lines
161 161 context_lcl = get_line_ctx('', request.GET)
162 162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163 163
164 164 # diff_limit will cut off the whole diff if the limit is applied
165 165 # otherwise it will just hide the big files from the front-end
166 166 diff_limit = self.cut_off_limit_diff
167 167 file_limit = self.cut_off_limit_file
168 168
169 169 # get ranges of commit ids if preset
170 170 commit_range = commit_id_range.split('...')[:2]
171 171
172 172 try:
173 173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 174 'message', 'parents']
175 175
176 176 if len(commit_range) == 2:
177 177 commits = c.rhodecode_repo.get_commits(
178 178 start_id=commit_range[0], end_id=commit_range[1],
179 179 pre_load=pre_load)
180 180 commits = list(commits)
181 181 else:
182 182 commits = [c.rhodecode_repo.get_commit(
183 183 commit_id=commit_id_range, pre_load=pre_load)]
184 184
185 185 c.commit_ranges = commits
186 186 if not c.commit_ranges:
187 187 raise RepositoryError(
188 188 'The commit range returned an empty result')
189 189 except CommitDoesNotExistError:
190 190 msg = _('No such commit exists for this repository')
191 191 h.flash(msg, category='error')
192 192 raise HTTPNotFound()
193 193 except Exception:
194 194 log.exception("General failure")
195 195 raise HTTPNotFound()
196 196
197 197 c.changes = OrderedDict()
198 198 c.lines_added = 0
199 199 c.lines_deleted = 0
200 200
201 # auto collapse if we have more than limit
202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204
201 205 c.commit_statuses = ChangesetStatus.STATUSES
202 206 c.inline_comments = []
203 207 c.files = []
204 208
205 209 c.statuses = []
206 210 c.comments = []
207 211 if len(c.commit_ranges) == 1:
208 212 commit = c.commit_ranges[0]
209 213 c.comments = ChangesetCommentsModel().get_comments(
210 214 c.rhodecode_db_repo.repo_id,
211 215 revision=commit.raw_id)
212 216 c.statuses.append(ChangesetStatusModel().get_status(
213 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
214 218 # comments from PR
215 219 statuses = ChangesetStatusModel().get_statuses(
216 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
217 221 with_revisions=True)
218 222 prs = set(st.pull_request for st in statuses
219 223 if st.pull_request is not None)
220 224 # from associated statuses, check the pull requests, and
221 225 # show comments from them
222 226 for pr in prs:
223 227 c.comments.extend(pr.comments)
224 228
225 229 # Iterate over ranges (default commit view is always one commit)
226 230 for commit in c.commit_ranges:
227 231 c.changes[commit.raw_id] = []
228 232
229 233 commit2 = commit
230 234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
231 235
232 236 _diff = c.rhodecode_repo.get_diff(
233 237 commit1, commit2,
234 238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
235 239 diff_processor = diffs.DiffProcessor(
236 240 _diff, format='newdiff', diff_limit=diff_limit,
237 241 file_limit=file_limit, show_full_diff=fulldiff)
238 242
239 243 commit_changes = OrderedDict()
240 244 if method == 'show':
241 245 _parsed = diff_processor.prepare()
242 246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
243 247
244 248 _parsed = diff_processor.prepare()
245 249
246 250 def _node_getter(commit):
247 251 def get_node(fname):
248 252 try:
249 253 return commit.get_node(fname)
250 254 except NodeDoesNotExistError:
251 255 return None
252 256 return get_node
253 257
254 258 inline_comments = ChangesetCommentsModel().get_inline_comments(
255 259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
256 260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
257 261 inline_comments)
258 262
259 263 diffset = codeblocks.DiffSet(
260 264 repo_name=c.repo_name,
261 265 source_node_getter=_node_getter(commit1),
262 266 target_node_getter=_node_getter(commit2),
263 267 comments=inline_comments
264 268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
265 269 c.changes[commit.raw_id] = diffset
266 270 else:
267 271 # downloads/raw we only need RAW diff nothing else
268 272 diff = diff_processor.as_raw()
269 273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
270 274
271 275 # sort comments by how they were generated
272 276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
273 277
274 278
275 279 if len(c.commit_ranges) == 1:
276 280 c.commit = c.commit_ranges[0]
277 281 c.parent_tmpl = ''.join(
278 282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 283 if method == 'download':
280 284 response.content_type = 'text/plain'
281 285 response.content_disposition = (
282 286 'attachment; filename=%s.diff' % commit_id_range[:12])
283 287 return diff
284 288 elif method == 'patch':
285 289 response.content_type = 'text/plain'
286 290 c.diff = safe_unicode(diff)
287 291 return render('changeset/patch_changeset.html')
288 292 elif method == 'raw':
289 293 response.content_type = 'text/plain'
290 294 return diff
291 295 elif method == 'show':
292 296 if len(c.commit_ranges) == 1:
293 297 return render('changeset/changeset.html')
294 298 else:
295 299 c.ancestor = None
296 300 c.target_repo = c.rhodecode_db_repo
297 301 return render('changeset/changeset_range.html')
298 302
299 303 @LoginRequired()
300 304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 305 'repository.admin')
302 306 def index(self, revision, method='show'):
303 307 return self._index(revision, method=method)
304 308
305 309 @LoginRequired()
306 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 311 'repository.admin')
308 312 def changeset_raw(self, revision):
309 313 return self._index(revision, method='raw')
310 314
311 315 @LoginRequired()
312 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 317 'repository.admin')
314 318 def changeset_patch(self, revision):
315 319 return self._index(revision, method='patch')
316 320
317 321 @LoginRequired()
318 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 323 'repository.admin')
320 324 def changeset_download(self, revision):
321 325 return self._index(revision, method='download')
322 326
323 327 @LoginRequired()
324 328 @NotAnonymous()
325 329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 330 'repository.admin')
327 331 @auth.CSRFRequired()
328 332 @jsonify
329 333 def comment(self, repo_name, revision):
330 334 commit_id = revision
331 335 status = request.POST.get('changeset_status', None)
332 336 text = request.POST.get('text')
333 337 if status:
334 338 text = text or (_('Status change %(transition_icon)s %(status)s')
335 339 % {'transition_icon': '>',
336 340 'status': ChangesetStatus.get_status_lbl(status)})
337 341
338 342 multi_commit_ids = filter(
339 343 lambda s: s not in ['', None],
340 344 request.POST.get('commit_ids', '').split(','),)
341 345
342 346 commit_ids = multi_commit_ids or [commit_id]
343 347 comment = None
344 348 for current_id in filter(None, commit_ids):
345 349 c.co = comment = ChangesetCommentsModel().create(
346 350 text=text,
347 351 repo=c.rhodecode_db_repo.repo_id,
348 352 user=c.rhodecode_user.user_id,
349 353 revision=current_id,
350 354 f_path=request.POST.get('f_path'),
351 355 line_no=request.POST.get('line'),
352 356 status_change=(ChangesetStatus.get_status_lbl(status)
353 357 if status else None),
354 358 status_change_type=status
355 359 )
356 360 # get status if set !
357 361 if status:
358 362 # if latest status was from pull request and it's closed
359 363 # disallow changing status !
360 364 # dont_allow_on_closed_pull_request = True !
361 365
362 366 try:
363 367 ChangesetStatusModel().set_status(
364 368 c.rhodecode_db_repo.repo_id,
365 369 status,
366 370 c.rhodecode_user.user_id,
367 371 comment,
368 372 revision=current_id,
369 373 dont_allow_on_closed_pull_request=True
370 374 )
371 375 except StatusChangeOnClosedPullRequestError:
372 376 msg = _('Changing the status of a commit associated with '
373 377 'a closed pull request is not allowed')
374 378 log.exception(msg)
375 379 h.flash(msg, category='warning')
376 380 return redirect(h.url(
377 381 'changeset_home', repo_name=repo_name,
378 382 revision=current_id))
379 383
380 384 # finalize, commit and redirect
381 385 Session().commit()
382 386
383 387 data = {
384 388 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 389 }
386 390 if comment:
387 391 data.update(comment.get_dict())
388 392 data.update({'rendered_text':
389 393 render('changeset/changeset_comment_block.html')})
390 394
391 395 return data
392 396
393 397 @LoginRequired()
394 398 @NotAnonymous()
395 399 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 400 'repository.admin')
397 401 @auth.CSRFRequired()
398 402 def preview_comment(self):
399 403 # Technically a CSRF token is not needed as no state changes with this
400 404 # call. However, as this is a POST is better to have it, so automated
401 405 # tools don't flag it as potential CSRF.
402 406 # Post is required because the payload could be bigger than the maximum
403 407 # allowed by GET.
404 408 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 409 raise HTTPBadRequest()
406 410 text = request.POST.get('text')
407 411 renderer = request.POST.get('renderer') or 'rst'
408 412 if text:
409 413 return h.render(text, renderer=renderer, mentions=True)
410 414 return ''
411 415
412 416 @LoginRequired()
413 417 @NotAnonymous()
414 418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 419 'repository.admin')
416 420 @auth.CSRFRequired()
417 421 @jsonify
418 422 def delete_comment(self, repo_name, comment_id):
419 423 comment = ChangesetComment.get(comment_id)
420 424 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 425 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 426 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 427 ChangesetCommentsModel().delete(comment=comment)
424 428 Session().commit()
425 429 return True
426 430 else:
427 431 raise HTTPForbidden()
428 432
429 433 @LoginRequired()
430 434 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 435 'repository.admin')
432 436 @jsonify
433 437 def changeset_info(self, repo_name, revision):
434 438 if request.is_xhr:
435 439 try:
436 440 return c.rhodecode_repo.get_commit(commit_id=revision)
437 441 except CommitDoesNotExistError as e:
438 442 return EmptyCommit(message=str(e))
439 443 else:
440 444 raise HTTPBadRequest()
441 445
442 446 @LoginRequired()
443 447 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 448 'repository.admin')
445 449 @jsonify
446 450 def changeset_children(self, repo_name, revision):
447 451 if request.is_xhr:
448 452 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 453 result = {"results": commit.children}
450 454 return result
451 455 else:
452 456 raise HTTPBadRequest()
453 457
454 458 @LoginRequired()
455 459 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 460 'repository.admin')
457 461 @jsonify
458 462 def changeset_parents(self, repo_name, revision):
459 463 if request.is_xhr:
460 464 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 465 result = {"results": commit.parents}
462 466 return result
463 467 else:
464 468 raise HTTPBadRequest()
@@ -1,277 +1,282 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Compare controller for showing differences between two commits/refs/tags etc.
23 23 """
24 24
25 25 import logging
26 26
27 27 from webob.exc import HTTPBadRequest
28 28 from pylons import request, tmpl_context as c, url
29 29 from pylons.controllers.util import redirect
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import diffs, codeblocks
35 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.utils import safe_str
38 38 from rhodecode.lib.utils2 import safe_unicode, str2bool
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
41 41 NodeDoesNotExistError)
42 42 from rhodecode.model.db import Repository, ChangesetStatus
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class CompareController(BaseRepoController):
48 48
49 49 def __before__(self):
50 50 super(CompareController, self).__before__()
51 51
52 52 def _get_commit_or_redirect(
53 53 self, ref, ref_type, repo, redirect_after=True, partial=False):
54 54 """
55 55 This is a safe way to get a commit. If an error occurs it
56 56 redirects to a commit with a proper message. If partial is set
57 57 then it does not do redirect raise and throws an exception instead.
58 58 """
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 redirect(url('summary_home', repo_name=repo.repo_name))
67 67
68 68 except RepositoryError as e:
69 69 msg = safe_str(e)
70 70 log.exception(msg)
71 71 h.flash(msg, category='warning')
72 72 if not partial:
73 73 redirect(h.url('summary_home', repo_name=repo.repo_name))
74 74 raise HTTPBadRequest()
75 75
76 76 @LoginRequired()
77 77 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 78 'repository.admin')
79 79 def index(self, repo_name):
80 80 c.compare_home = True
81 81 c.commit_ranges = []
82 c.collapse_all_commits = False
82 83 c.diffset = None
83 84 c.limited_diff = False
84 85 source_repo = c.rhodecode_db_repo.repo_name
85 86 target_repo = request.GET.get('target_repo', source_repo)
86 87 c.source_repo = Repository.get_by_repo_name(source_repo)
87 88 c.target_repo = Repository.get_by_repo_name(target_repo)
88 89 c.source_ref = c.target_ref = _('Select commit')
89 90 c.source_ref_type = ""
90 91 c.target_ref_type = ""
91 92 c.commit_statuses = ChangesetStatus.STATUSES
92 93 c.preview_mode = False
93 94 c.file_path = None
94 95 return render('compare/compare_diff.html')
95 96
96 97 @LoginRequired()
97 98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
98 99 'repository.admin')
99 100 def compare(self, repo_name, source_ref_type, source_ref,
100 101 target_ref_type, target_ref):
101 102 # source_ref will be evaluated in source_repo
102 103 source_repo_name = c.rhodecode_db_repo.repo_name
103 104 source_path, source_id = parse_path_ref(source_ref)
104 105
105 106 # target_ref will be evaluated in target_repo
106 107 target_repo_name = request.GET.get('target_repo', source_repo_name)
107 108 target_path, target_id = parse_path_ref(
108 109 target_ref, default_path=request.GET.get('f_path', ''))
109 110
110 111 c.file_path = target_path
111 112 c.commit_statuses = ChangesetStatus.STATUSES
112 113
113 114 # if merge is True
114 115 # Show what changes since the shared ancestor commit of target/source
115 116 # the source would get if it was merged with target. Only commits
116 117 # which are in target but not in source will be shown.
117 118 merge = str2bool(request.GET.get('merge'))
118 119 # if merge is False
119 120 # Show a raw diff of source/target refs even if no ancestor exists
120 121
121 122 # c.fulldiff disables cut_off_limit
122 123 c.fulldiff = str2bool(request.GET.get('fulldiff'))
123 124
124 125 # if partial, returns just compare_commits.html (commits log)
125 126 partial = request.is_xhr
126 127
127 128 # swap url for compare_diff page
128 129 c.swap_url = h.url(
129 130 'compare_url',
130 131 repo_name=target_repo_name,
131 132 source_ref_type=target_ref_type,
132 133 source_ref=target_ref,
133 134 target_repo=source_repo_name,
134 135 target_ref_type=source_ref_type,
135 136 target_ref=source_ref,
136 137 merge=merge and '1' or '',
137 138 f_path=target_path)
138 139
139 140 source_repo = Repository.get_by_repo_name(source_repo_name)
140 141 target_repo = Repository.get_by_repo_name(target_repo_name)
141 142
142 143 if source_repo is None:
143 144 msg = _('Could not find the original repo: %(repo)s') % {
144 145 'repo': source_repo}
145 146
146 147 log.error(msg)
147 148 h.flash(msg, category='error')
148 149 return redirect(url('compare_home', repo_name=c.repo_name))
149 150
150 151 if target_repo is None:
151 152 msg = _('Could not find the other repo: %(repo)s') % {
152 153 'repo': target_repo_name}
153 154 log.error(msg)
154 155 h.flash(msg, category='error')
155 156 return redirect(url('compare_home', repo_name=c.repo_name))
156 157
157 158 source_scm = source_repo.scm_instance()
158 159 target_scm = target_repo.scm_instance()
159 160
160 161 source_alias = source_scm.alias
161 162 target_alias = target_scm.alias
162 163 if source_alias != target_alias:
163 164 msg = _('The comparison of two different kinds of remote repos '
164 165 'is not available')
165 166 log.error(msg)
166 167 h.flash(msg, category='error')
167 168 return redirect(url('compare_home', repo_name=c.repo_name))
168 169
169 170 source_commit = self._get_commit_or_redirect(
170 171 ref=source_id, ref_type=source_ref_type, repo=source_repo,
171 172 partial=partial)
172 173 target_commit = self._get_commit_or_redirect(
173 174 ref=target_id, ref_type=target_ref_type, repo=target_repo,
174 175 partial=partial)
175 176
176 177 c.compare_home = False
177 178 c.source_repo = source_repo
178 179 c.target_repo = target_repo
179 180 c.source_ref = source_ref
180 181 c.target_ref = target_ref
181 182 c.source_ref_type = source_ref_type
182 183 c.target_ref_type = target_ref_type
183 184
184 185 pre_load = ["author", "branch", "date", "message"]
185 186 c.ancestor = None
186 187
187 188 if c.file_path:
188 189 if source_commit == target_commit:
189 190 c.commit_ranges = []
190 191 else:
191 192 c.commit_ranges = [target_commit]
192 193 else:
193 194 try:
194 195 c.commit_ranges = source_scm.compare(
195 196 source_commit.raw_id, target_commit.raw_id,
196 197 target_scm, merge, pre_load=pre_load)
197 198 if merge:
198 199 c.ancestor = source_scm.get_common_ancestor(
199 200 source_commit.raw_id, target_commit.raw_id, target_scm)
200 201 except RepositoryRequirementError:
201 202 msg = _('Could not compare repos with different '
202 203 'large file settings')
203 204 log.error(msg)
204 205 if partial:
205 206 return msg
206 207 h.flash(msg, category='error')
207 208 return redirect(url('compare_home', repo_name=c.repo_name))
208 209
209 210 c.statuses = c.rhodecode_db_repo.statuses(
210 211 [x.raw_id for x in c.commit_ranges])
211 212
213 # auto collapse if we have more than limit
214 collapse_limit = diffs.DiffProcessor._collapse_commits_over
215 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
216
212 217 if partial: # for PR ajax commits loader
213 218 if not c.ancestor:
214 219 return '' # cannot merge if there is no ancestor
215 220 return render('compare/compare_commits.html')
216 221
217 222 if c.ancestor:
218 223 # case we want a simple diff without incoming commits,
219 224 # previewing what will be merged.
220 225 # Make the diff on target repo (which is known to have target_ref)
221 226 log.debug('Using ancestor %s as source_ref instead of %s'
222 227 % (c.ancestor, source_ref))
223 228 source_repo = target_repo
224 229 source_commit = target_repo.get_commit(commit_id=c.ancestor)
225 230
226 231 # diff_limit will cut off the whole diff if the limit is applied
227 232 # otherwise it will just hide the big files from the front-end
228 233 diff_limit = self.cut_off_limit_diff
229 234 file_limit = self.cut_off_limit_file
230 235
231 236 log.debug('calculating diff between '
232 237 'source_ref:%s and target_ref:%s for repo `%s`',
233 238 source_commit, target_commit,
234 239 safe_unicode(source_repo.scm_instance().path))
235 240
236 241 if source_commit.repository != target_commit.repository:
237 242 msg = _(
238 243 "Repositories unrelated. "
239 244 "Cannot compare commit %(commit1)s from repository %(repo1)s "
240 245 "with commit %(commit2)s from repository %(repo2)s.") % {
241 246 'commit1': h.show_id(source_commit),
242 247 'repo1': source_repo.repo_name,
243 248 'commit2': h.show_id(target_commit),
244 249 'repo2': target_repo.repo_name,
245 250 }
246 251 h.flash(msg, category='error')
247 252 raise HTTPBadRequest()
248 253
249 254 txtdiff = source_repo.scm_instance().get_diff(
250 255 commit1=source_commit, commit2=target_commit,
251 256 path=target_path, path1=source_path)
252 257
253 258 diff_processor = diffs.DiffProcessor(
254 259 txtdiff, format='newdiff', diff_limit=diff_limit,
255 260 file_limit=file_limit, show_full_diff=c.fulldiff)
256 261 _parsed = diff_processor.prepare()
257 262
258 263 def _node_getter(commit):
259 264 """ Returns a function that returns a node for a commit or None """
260 265 def get_node(fname):
261 266 try:
262 267 return commit.get_node(fname)
263 268 except NodeDoesNotExistError:
264 269 return None
265 270 return get_node
266 271
267 272 c.diffset = codeblocks.DiffSet(
268 273 repo_name=source_repo.repo_name,
269 274 source_node_getter=_node_getter(source_commit),
270 275 target_node_getter=_node_getter(target_commit),
271 276 ).render_patchset(_parsed, source_ref, target_ref)
272 277
273 278 c.preview_mode = merge
274 279 c.source_commit = source_commit
275 280 c.target_commit = target_commit
276 281
277 282 return render('compare/compare_diff.html')
@@ -1,1003 +1,1020 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29
29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.compat import OrderedDict
49 48 from rhodecode.lib.utils import jsonify
50 49 from rhodecode.lib.utils2 import (
51 50 safe_int, safe_str, str2bool, safe_unicode)
52 51 from rhodecode.lib.vcs.backends.base import (
53 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
54 53 from rhodecode.lib.vcs.exceptions import (
55 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
56 55 NodeDoesNotExistError)
57 56
58 57 from rhodecode.model.changeset_status import ChangesetStatusModel
59 58 from rhodecode.model.comment import ChangesetCommentsModel
60 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
61 60 Repository, PullRequestVersion)
62 61 from rhodecode.model.forms import PullRequestForm
63 62 from rhodecode.model.meta import Session
64 63 from rhodecode.model.pull_request import PullRequestModel
65 64
66 65 log = logging.getLogger(__name__)
67 66
68 67
69 68 class PullrequestsController(BaseRepoController):
70 69 def __before__(self):
71 70 super(PullrequestsController, self).__before__()
72 71
73 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
72 def _load_compare_data(self, pull_request, inline_comments):
74 73 """
75 74 Load context data needed for generating compare diff
76 75
77 76 :param pull_request: object related to the request
78 77 :param enable_comments: flag to determine if comments are included
79 78 """
80 79 source_repo = pull_request.source_repo
81 80 source_ref_id = pull_request.source_ref_parts.commit_id
82 81
83 82 target_repo = pull_request.target_repo
84 83 target_ref_id = pull_request.target_ref_parts.commit_id
85 84
86 85 # despite opening commits for bookmarks/branches/tags, we always
87 86 # convert this to rev to prevent changes after bookmark or branch change
88 87 c.source_ref_type = 'rev'
89 88 c.source_ref = source_ref_id
90 89
91 90 c.target_ref_type = 'rev'
92 91 c.target_ref = target_ref_id
93 92
94 93 c.source_repo = source_repo
95 94 c.target_repo = target_repo
96 95
97 96 c.fulldiff = bool(request.GET.get('fulldiff'))
98 97
99 98 # diff_limit is the old behavior, will cut off the whole diff
100 99 # if the limit is applied otherwise will just hide the
101 100 # big files from the front-end
102 101 diff_limit = self.cut_off_limit_diff
103 102 file_limit = self.cut_off_limit_file
104 103
105 104 pre_load = ["author", "branch", "date", "message"]
106 105
107 106 c.commit_ranges = []
108 107 source_commit = EmptyCommit()
109 108 target_commit = EmptyCommit()
110 109 c.missing_requirements = False
111 110 try:
112 111 c.commit_ranges = [
113 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
114 113 for rev in pull_request.revisions]
115 114
116 115 c.statuses = source_repo.statuses(
117 116 [x.raw_id for x in c.commit_ranges])
118 117
119 118 target_commit = source_repo.get_commit(
120 119 commit_id=safe_str(target_ref_id))
121 120 source_commit = source_repo.get_commit(
122 121 commit_id=safe_str(source_ref_id))
123 122 except RepositoryRequirementError:
124 123 c.missing_requirements = True
125 124
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
126 129 c.changes = {}
127 130 c.missing_commits = False
128 131 if (c.missing_requirements or
129 132 isinstance(source_commit, EmptyCommit) or
130 133 source_commit == target_commit):
131 134 _parsed = []
132 135 c.missing_commits = True
133 136 else:
134 137 vcs_diff = PullRequestModel().get_diff(pull_request)
135 138 diff_processor = diffs.DiffProcessor(
136 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
137 140 file_limit=file_limit, show_full_diff=c.fulldiff)
138 _parsed = diff_processor.prepare()
139 141
140 commit_changes = OrderedDict()
141 142 _parsed = diff_processor.prepare()
142 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 144
144 _parsed = diff_processor.prepare()
145 included_files = {}
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
148
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
151
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
145 159
146 160 def _node_getter(commit):
147 161 def get_node(fname):
148 162 try:
149 163 return commit.get_node(fname)
150 164 except NodeDoesNotExistError:
151 165 return None
152 166 return get_node
153 167
154 168 c.diffset = codeblocks.DiffSet(
155 169 repo_name=c.repo_name,
156 170 source_repo_name=c.source_repo.repo_name,
157 171 source_node_getter=_node_getter(target_commit),
158 172 target_node_getter=_node_getter(source_commit),
159 173 comments=inline_comments
160 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
161 175
162 c.included_files = []
163 c.deleted_files = []
164
165 for f in _parsed:
166 st = f['stats']
167 fid = h.FID('', f['filename'])
168 c.included_files.append(f['filename'])
169
170 176 def _extract_ordering(self, request):
171 177 column_index = safe_int(request.GET.get('order[0][column]'))
172 178 order_dir = request.GET.get('order[0][dir]', 'desc')
173 179 order_by = request.GET.get(
174 180 'columns[%s][data][sort]' % column_index, 'name_raw')
175 181 return order_by, order_dir
176 182
177 183 @LoginRequired()
178 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
179 185 'repository.admin')
180 186 @HasAcceptedRepoType('git', 'hg')
181 187 def show_all(self, repo_name):
182 188 # filter types
183 189 c.active = 'open'
184 190 c.source = str2bool(request.GET.get('source'))
185 191 c.closed = str2bool(request.GET.get('closed'))
186 192 c.my = str2bool(request.GET.get('my'))
187 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
188 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
189 195 c.repo_name = repo_name
190 196
191 197 opened_by = None
192 198 if c.my:
193 199 c.active = 'my'
194 200 opened_by = [c.rhodecode_user.user_id]
195 201
196 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 203 if c.closed:
198 204 c.active = 'closed'
199 205 statuses = [PullRequest.STATUS_CLOSED]
200 206
201 207 if c.awaiting_review and not c.source:
202 208 c.active = 'awaiting'
203 209 if c.source and not c.awaiting_review:
204 210 c.active = 'source'
205 211 if c.awaiting_my_review:
206 212 c.active = 'awaiting_my'
207 213
208 214 data = self._get_pull_requests_list(
209 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
210 216 if not request.is_xhr:
211 217 c.data = json.dumps(data['data'])
212 218 c.records_total = data['recordsTotal']
213 219 return render('/pullrequests/pullrequests.html')
214 220 else:
215 221 return json.dumps(data)
216 222
217 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
218 224 # pagination
219 225 start = safe_int(request.GET.get('start'), 0)
220 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
221 227 order_by, order_dir = self._extract_ordering(request)
222 228
223 229 if c.awaiting_review:
224 230 pull_requests = PullRequestModel().get_awaiting_review(
225 231 repo_name, source=c.source, opened_by=opened_by,
226 232 statuses=statuses, offset=start, length=length,
227 233 order_by=order_by, order_dir=order_dir)
228 234 pull_requests_total_count = PullRequestModel(
229 235 ).count_awaiting_review(
230 236 repo_name, source=c.source, statuses=statuses,
231 237 opened_by=opened_by)
232 238 elif c.awaiting_my_review:
233 239 pull_requests = PullRequestModel().get_awaiting_my_review(
234 240 repo_name, source=c.source, opened_by=opened_by,
235 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
236 242 offset=start, length=length, order_by=order_by,
237 243 order_dir=order_dir)
238 244 pull_requests_total_count = PullRequestModel(
239 245 ).count_awaiting_my_review(
240 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
241 247 statuses=statuses, opened_by=opened_by)
242 248 else:
243 249 pull_requests = PullRequestModel().get_all(
244 250 repo_name, source=c.source, opened_by=opened_by,
245 251 statuses=statuses, offset=start, length=length,
246 252 order_by=order_by, order_dir=order_dir)
247 253 pull_requests_total_count = PullRequestModel().count_all(
248 254 repo_name, source=c.source, statuses=statuses,
249 255 opened_by=opened_by)
250 256
251 257 from rhodecode.lib.utils import PartialRenderer
252 258 _render = PartialRenderer('data_table/_dt_elements.html')
253 259 data = []
254 260 for pr in pull_requests:
255 261 comments = ChangesetCommentsModel().get_all_comments(
256 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
257 263
258 264 data.append({
259 265 'name': _render('pullrequest_name',
260 266 pr.pull_request_id, pr.target_repo.repo_name),
261 267 'name_raw': pr.pull_request_id,
262 268 'status': _render('pullrequest_status',
263 269 pr.calculated_review_status()),
264 270 'title': _render(
265 271 'pullrequest_title', pr.title, pr.description),
266 272 'description': h.escape(pr.description),
267 273 'updated_on': _render('pullrequest_updated_on',
268 274 h.datetime_to_time(pr.updated_on)),
269 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
270 276 'created_on': _render('pullrequest_updated_on',
271 277 h.datetime_to_time(pr.created_on)),
272 278 'created_on_raw': h.datetime_to_time(pr.created_on),
273 279 'author': _render('pullrequest_author',
274 280 pr.author.full_contact, ),
275 281 'author_raw': pr.author.full_name,
276 282 'comments': _render('pullrequest_comments', len(comments)),
277 283 'comments_raw': len(comments),
278 284 'closed': pr.is_closed(),
279 285 })
280 286 # json used to render the grid
281 287 data = ({
282 288 'data': data,
283 289 'recordsTotal': pull_requests_total_count,
284 290 'recordsFiltered': pull_requests_total_count,
285 291 })
286 292 return data
287 293
288 294 @LoginRequired()
289 295 @NotAnonymous()
290 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 297 'repository.admin')
292 298 @HasAcceptedRepoType('git', 'hg')
293 299 def index(self):
294 300 source_repo = c.rhodecode_db_repo
295 301
296 302 try:
297 303 source_repo.scm_instance().get_commit()
298 304 except EmptyRepositoryError:
299 305 h.flash(h.literal(_('There are no commits yet')),
300 306 category='warning')
301 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
302 308
303 309 commit_id = request.GET.get('commit')
304 310 branch_ref = request.GET.get('branch')
305 311 bookmark_ref = request.GET.get('bookmark')
306 312
307 313 try:
308 314 source_repo_data = PullRequestModel().generate_repo_data(
309 315 source_repo, commit_id=commit_id,
310 316 branch=branch_ref, bookmark=bookmark_ref)
311 317 except CommitDoesNotExistError as e:
312 318 log.exception(e)
313 319 h.flash(_('Commit does not exist'), 'error')
314 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
315 321
316 322 default_target_repo = source_repo
317 323
318 324 if source_repo.parent:
319 325 parent_vcs_obj = source_repo.parent.scm_instance()
320 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
321 327 # change default if we have a parent repo
322 328 default_target_repo = source_repo.parent
323 329
324 330 target_repo_data = PullRequestModel().generate_repo_data(
325 331 default_target_repo)
326 332
327 333 selected_source_ref = source_repo_data['refs']['selected_ref']
328 334
329 335 title_source_ref = selected_source_ref.split(':', 2)[1]
330 336 c.default_title = PullRequestModel().generate_pullrequest_title(
331 337 source=source_repo.repo_name,
332 338 source_ref=title_source_ref,
333 339 target=default_target_repo.repo_name
334 340 )
335 341
336 342 c.default_repo_data = {
337 343 'source_repo_name': source_repo.repo_name,
338 344 'source_refs_json': json.dumps(source_repo_data),
339 345 'target_repo_name': default_target_repo.repo_name,
340 346 'target_refs_json': json.dumps(target_repo_data),
341 347 }
342 348 c.default_source_ref = selected_source_ref
343 349
344 350 return render('/pullrequests/pullrequest.html')
345 351
346 352 @LoginRequired()
347 353 @NotAnonymous()
348 354 @XHRRequired()
349 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 356 'repository.admin')
351 357 @jsonify
352 358 def get_repo_refs(self, repo_name, target_repo_name):
353 359 repo = Repository.get_by_repo_name(target_repo_name)
354 360 if not repo:
355 361 raise HTTPNotFound
356 362 return PullRequestModel().generate_repo_data(repo)
357 363
358 364 @LoginRequired()
359 365 @NotAnonymous()
360 366 @XHRRequired()
361 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
362 368 'repository.admin')
363 369 @jsonify
364 370 def get_repo_destinations(self, repo_name):
365 371 repo = Repository.get_by_repo_name(repo_name)
366 372 if not repo:
367 373 raise HTTPNotFound
368 374 filter_query = request.GET.get('query')
369 375
370 376 query = Repository.query() \
371 377 .order_by(func.length(Repository.repo_name)) \
372 378 .filter(or_(
373 379 Repository.repo_name == repo.repo_name,
374 380 Repository.fork_id == repo.repo_id))
375 381
376 382 if filter_query:
377 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
378 384 query = query.filter(
379 385 Repository.repo_name.ilike(ilike_expression))
380 386
381 387 add_parent = False
382 388 if repo.parent:
383 389 if filter_query in repo.parent.repo_name:
384 390 parent_vcs_obj = repo.parent.scm_instance()
385 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
386 392 add_parent = True
387 393
388 394 limit = 20 - 1 if add_parent else 20
389 395 all_repos = query.limit(limit).all()
390 396 if add_parent:
391 397 all_repos += [repo.parent]
392 398
393 399 repos = []
394 400 for obj in self.scm_model.get_repos(all_repos):
395 401 repos.append({
396 402 'id': obj['name'],
397 403 'text': obj['name'],
398 404 'type': 'repo',
399 405 'obj': obj['dbrepo']
400 406 })
401 407
402 408 data = {
403 409 'more': False,
404 410 'results': [{
405 411 'text': _('Repositories'),
406 412 'children': repos
407 413 }] if repos else []
408 414 }
409 415 return data
410 416
411 417 @LoginRequired()
412 418 @NotAnonymous()
413 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
414 420 'repository.admin')
415 421 @HasAcceptedRepoType('git', 'hg')
416 422 @auth.CSRFRequired()
417 423 def create(self, repo_name):
418 424 repo = Repository.get_by_repo_name(repo_name)
419 425 if not repo:
420 426 raise HTTPNotFound
421 427
422 428 controls = peppercorn.parse(request.POST.items())
423 429
424 430 try:
425 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
426 432 except formencode.Invalid as errors:
427 433 if errors.error_dict.get('revisions'):
428 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
429 435 elif errors.error_dict.get('pullrequest_title'):
430 436 msg = _('Pull request requires a title with min. 3 chars')
431 437 else:
432 438 msg = _('Error creating pull request: {}').format(errors)
433 439 log.exception(msg)
434 440 h.flash(msg, 'error')
435 441
436 442 # would rather just go back to form ...
437 443 return redirect(url('pullrequest_home', repo_name=repo_name))
438 444
439 445 source_repo = _form['source_repo']
440 446 source_ref = _form['source_ref']
441 447 target_repo = _form['target_repo']
442 448 target_ref = _form['target_ref']
443 449 commit_ids = _form['revisions'][::-1]
444 450 reviewers = [
445 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
446 452
447 453 # find the ancestor for this pr
448 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
449 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
450 456
451 457 source_scm = source_db_repo.scm_instance()
452 458 target_scm = target_db_repo.scm_instance()
453 459
454 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
455 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
456 462
457 463 ancestor = source_scm.get_common_ancestor(
458 464 source_commit.raw_id, target_commit.raw_id, target_scm)
459 465
460 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
461 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
462 468
463 469 pullrequest_title = _form['pullrequest_title']
464 470 title_source_ref = source_ref.split(':', 2)[1]
465 471 if not pullrequest_title:
466 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
467 473 source=source_repo,
468 474 source_ref=title_source_ref,
469 475 target=target_repo
470 476 )
471 477
472 478 description = _form['pullrequest_desc']
473 479 try:
474 480 pull_request = PullRequestModel().create(
475 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
476 482 target_ref, commit_ids, reviewers, pullrequest_title,
477 483 description
478 484 )
479 485 Session().commit()
480 486 h.flash(_('Successfully opened new pull request'),
481 487 category='success')
482 488 except Exception as e:
483 489 msg = _('Error occurred during sending pull request')
484 490 log.exception(msg)
485 491 h.flash(msg, category='error')
486 492 return redirect(url('pullrequest_home', repo_name=repo_name))
487 493
488 494 return redirect(url('pullrequest_show', repo_name=target_repo,
489 495 pull_request_id=pull_request.pull_request_id))
490 496
491 497 @LoginRequired()
492 498 @NotAnonymous()
493 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
494 500 'repository.admin')
495 501 @auth.CSRFRequired()
496 502 @jsonify
497 503 def update(self, repo_name, pull_request_id):
498 504 pull_request_id = safe_int(pull_request_id)
499 505 pull_request = PullRequest.get_or_404(pull_request_id)
500 506 # only owner or admin can update it
501 507 allowed_to_update = PullRequestModel().check_user_update(
502 508 pull_request, c.rhodecode_user)
503 509 if allowed_to_update:
504 510 controls = peppercorn.parse(request.POST.items())
505 511
506 512 if 'review_members' in controls:
507 513 self._update_reviewers(
508 514 pull_request_id, controls['review_members'])
509 515 elif str2bool(request.POST.get('update_commits', 'false')):
510 516 self._update_commits(pull_request)
511 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
512 518 self._reject_close(pull_request)
513 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
514 520 self._edit_pull_request(pull_request)
515 521 else:
516 522 raise HTTPBadRequest()
517 523 return True
518 524 raise HTTPForbidden()
519 525
520 526 def _edit_pull_request(self, pull_request):
521 527 try:
522 528 PullRequestModel().edit(
523 529 pull_request, request.POST.get('title'),
524 530 request.POST.get('description'))
525 531 except ValueError:
526 532 msg = _(u'Cannot update closed pull requests.')
527 533 h.flash(msg, category='error')
528 534 return
529 535 else:
530 536 Session().commit()
531 537
532 538 msg = _(u'Pull request title & description updated.')
533 539 h.flash(msg, category='success')
534 540 return
535 541
536 542 def _update_commits(self, pull_request):
537 543 resp = PullRequestModel().update_commits(pull_request)
538 544
539 545 if resp.executed:
540 546 msg = _(
541 547 u'Pull request updated to "{source_commit_id}" with '
542 548 u'{count_added} added, {count_removed} removed commits.')
543 549 msg = msg.format(
544 550 source_commit_id=pull_request.source_ref_parts.commit_id,
545 551 count_added=len(resp.changes.added),
546 552 count_removed=len(resp.changes.removed))
547 553 h.flash(msg, category='success')
548 554
549 555 registry = get_current_registry()
550 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
551 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
552 558 if channelstream_config.get('enabled'):
553 559 message = msg + (
554 560 ' - <a onclick="window.location.reload()">'
555 561 '<strong>{}</strong></a>'.format(_('Reload page')))
556 562 channel = '/repo${}$/pr/{}'.format(
557 563 pull_request.target_repo.repo_name,
558 564 pull_request.pull_request_id
559 565 )
560 566 payload = {
561 567 'type': 'message',
562 568 'user': 'system',
563 569 'exclude_users': [request.user.username],
564 570 'channel': channel,
565 571 'message': {
566 572 'message': message,
567 573 'level': 'success',
568 574 'topic': '/notifications'
569 575 }
570 576 }
571 577 channelstream_request(
572 578 channelstream_config, [payload], '/message',
573 579 raise_exc=False)
574 580 else:
575 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
576 582 warning_reasons = [
577 583 UpdateFailureReason.NO_CHANGE,
578 584 UpdateFailureReason.WRONG_REF_TPYE,
579 585 ]
580 586 category = 'warning' if resp.reason in warning_reasons else 'error'
581 587 h.flash(msg, category=category)
582 588
583 589 @auth.CSRFRequired()
584 590 @LoginRequired()
585 591 @NotAnonymous()
586 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
587 593 'repository.admin')
588 594 def merge(self, repo_name, pull_request_id):
589 595 """
590 596 POST /{repo_name}/pull-request/{pull_request_id}
591 597
592 598 Merge will perform a server-side merge of the specified
593 599 pull request, if the pull request is approved and mergeable.
594 600 After succesfull merging, the pull request is automatically
595 601 closed, with a relevant comment.
596 602 """
597 603 pull_request_id = safe_int(pull_request_id)
598 604 pull_request = PullRequest.get_or_404(pull_request_id)
599 605 user = c.rhodecode_user
600 606
601 607 if self._meets_merge_pre_conditions(pull_request, user):
602 608 log.debug("Pre-conditions checked, trying to merge.")
603 609 extras = vcs_operation_context(
604 610 request.environ, repo_name=pull_request.target_repo.repo_name,
605 611 username=user.username, action='push',
606 612 scm=pull_request.target_repo.repo_type)
607 613 self._merge_pull_request(pull_request, user, extras)
608 614
609 615 return redirect(url(
610 616 'pullrequest_show',
611 617 repo_name=pull_request.target_repo.repo_name,
612 618 pull_request_id=pull_request.pull_request_id))
613 619
614 620 def _meets_merge_pre_conditions(self, pull_request, user):
615 621 if not PullRequestModel().check_user_merge(pull_request, user):
616 622 raise HTTPForbidden()
617 623
618 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
619 625 if not merge_status:
620 626 log.debug("Cannot merge, not mergeable.")
621 627 h.flash(msg, category='error')
622 628 return False
623 629
624 630 if (pull_request.calculated_review_status()
625 631 is not ChangesetStatus.STATUS_APPROVED):
626 632 log.debug("Cannot merge, approval is pending.")
627 633 msg = _('Pull request reviewer approval is pending.')
628 634 h.flash(msg, category='error')
629 635 return False
630 636 return True
631 637
632 638 def _merge_pull_request(self, pull_request, user, extras):
633 639 merge_resp = PullRequestModel().merge(
634 640 pull_request, user, extras=extras)
635 641
636 642 if merge_resp.executed:
637 643 log.debug("The merge was successful, closing the pull request.")
638 644 PullRequestModel().close_pull_request(
639 645 pull_request.pull_request_id, user)
640 646 Session().commit()
641 647 msg = _('Pull request was successfully merged and closed.')
642 648 h.flash(msg, category='success')
643 649 else:
644 650 log.debug(
645 651 "The merge was not successful. Merge response: %s",
646 652 merge_resp)
647 653 msg = PullRequestModel().merge_status_message(
648 654 merge_resp.failure_reason)
649 655 h.flash(msg, category='error')
650 656
651 657 def _update_reviewers(self, pull_request_id, review_members):
652 658 reviewers = [
653 659 (int(r['user_id']), r['reasons']) for r in review_members]
654 660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
655 661 Session().commit()
656 662
657 663 def _reject_close(self, pull_request):
658 664 if pull_request.is_closed():
659 665 raise HTTPForbidden()
660 666
661 667 PullRequestModel().close_pull_request_with_comment(
662 668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
663 669 Session().commit()
664 670
665 671 @LoginRequired()
666 672 @NotAnonymous()
667 673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
668 674 'repository.admin')
669 675 @auth.CSRFRequired()
670 676 @jsonify
671 677 def delete(self, repo_name, pull_request_id):
672 678 pull_request_id = safe_int(pull_request_id)
673 679 pull_request = PullRequest.get_or_404(pull_request_id)
674 680 # only owner can delete it !
675 681 if pull_request.author.user_id == c.rhodecode_user.user_id:
676 682 PullRequestModel().delete(pull_request)
677 683 Session().commit()
678 684 h.flash(_('Successfully deleted pull request'),
679 685 category='success')
680 686 return redirect(url('my_account_pullrequests'))
681 687 raise HTTPForbidden()
682 688
683 689 def _get_pr_version(self, pull_request_id, version=None):
684 690 pull_request_id = safe_int(pull_request_id)
685 691 at_version = None
686 692
687 693 if version and version == 'latest':
688 694 pull_request_ver = PullRequest.get(pull_request_id)
689 695 pull_request_obj = pull_request_ver
690 696 _org_pull_request_obj = pull_request_obj
691 697 at_version = 'latest'
692 698 elif version:
693 699 pull_request_ver = PullRequestVersion.get_or_404(version)
694 700 pull_request_obj = pull_request_ver
695 701 _org_pull_request_obj = pull_request_ver.pull_request
696 702 at_version = pull_request_ver.pull_request_version_id
697 703 else:
698 704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
699 705
700 706 pull_request_display_obj = PullRequest.get_pr_display_object(
701 707 pull_request_obj, _org_pull_request_obj)
702 708 return _org_pull_request_obj, pull_request_obj, \
703 709 pull_request_display_obj, at_version
704 710
705 711 def _get_pr_version_changes(self, version, pull_request_latest):
706 712 """
707 713 Generate changes commits, and diff data based on the current pr version
708 714 """
709 715
710 716 #TODO(marcink): save those changes as JSON metadata for chaching later.
711 717
712 718 # fake the version to add the "initial" state object
713 719 pull_request_initial = PullRequest.get_pr_display_object(
714 720 pull_request_latest, pull_request_latest,
715 721 internal_methods=['get_commit', 'versions'])
716 722 pull_request_initial.revisions = []
717 723 pull_request_initial.source_repo.get_commit = types.MethodType(
718 724 lambda *a, **k: EmptyCommit(), pull_request_initial)
719 725 pull_request_initial.source_repo.scm_instance = types.MethodType(
720 726 lambda *a, **k: EmptyRepository(), pull_request_initial)
721 727
722 728 _changes_versions = [pull_request_latest] + \
723 729 list(reversed(c.versions)) + \
724 730 [pull_request_initial]
725 731
726 732 if version == 'latest':
727 733 index = 0
728 734 else:
729 735 for pos, prver in enumerate(_changes_versions):
730 736 ver = getattr(prver, 'pull_request_version_id', -1)
731 737 if ver == safe_int(version):
732 738 index = pos
733 739 break
734 740 else:
735 741 index = 0
736 742
737 743 cur_obj = _changes_versions[index]
738 744 prev_obj = _changes_versions[index + 1]
739 745
740 746 old_commit_ids = set(prev_obj.revisions)
741 747 new_commit_ids = set(cur_obj.revisions)
742 748
743 749 changes = PullRequestModel()._calculate_commit_id_changes(
744 750 old_commit_ids, new_commit_ids)
745 751
746 752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
747 753 cur_obj, prev_obj)
748 754 file_changes = PullRequestModel()._calculate_file_changes(
749 755 old_diff_data, new_diff_data)
750 756 return changes, file_changes
751 757
752 758 @LoginRequired()
753 759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
754 760 'repository.admin')
755 761 def show(self, repo_name, pull_request_id):
756 762 pull_request_id = safe_int(pull_request_id)
757 763 version = request.GET.get('version')
758 764
759 765 (pull_request_latest,
760 766 pull_request_at_ver,
761 767 pull_request_display_obj,
762 768 at_version) = self._get_pr_version(pull_request_id, version=version)
763 769
764 770 c.template_context['pull_request_data']['pull_request_id'] = \
765 771 pull_request_id
766 772
767 773 # pull_requests repo_name we opened it against
768 774 # ie. target_repo must match
769 775 if repo_name != pull_request_at_ver.target_repo.repo_name:
770 776 raise HTTPNotFound
771 777
772 778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
773 779 pull_request_at_ver)
774 780
775 781 pr_closed = pull_request_latest.is_closed()
776 782 if at_version and not at_version == 'latest':
777 783 c.allowed_to_change_status = False
778 784 c.allowed_to_update = False
779 785 c.allowed_to_merge = False
780 786 c.allowed_to_delete = False
781 787 c.allowed_to_comment = False
782 788 else:
783 789 c.allowed_to_change_status = PullRequestModel(). \
784 790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
785 791 c.allowed_to_update = PullRequestModel().check_user_update(
786 792 pull_request_latest, c.rhodecode_user) and not pr_closed
787 793 c.allowed_to_merge = PullRequestModel().check_user_merge(
788 794 pull_request_latest, c.rhodecode_user) and not pr_closed
789 795 c.allowed_to_delete = PullRequestModel().check_user_delete(
790 796 pull_request_latest, c.rhodecode_user) and not pr_closed
791 797 c.allowed_to_comment = not pr_closed
792 798
793 799 cc_model = ChangesetCommentsModel()
794 800
795 801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
796 802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
797 803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
798 804 pull_request_at_ver)
799 805 c.approval_msg = None
800 806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
801 807 c.approval_msg = _('Reviewer approval is pending.')
802 808 c.pr_merge_status = False
803 809
804 810 # inline comments
805 c.inline_comments = cc_model.get_inline_comments(
806 c.rhodecode_db_repo.repo_id,
807 pull_request=pull_request_id)
811 inline_comments = cc_model.get_inline_comments(
812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813
814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 inline_comments, version=at_version, include_aggregates=True)
816
817 c.at_version_num = at_version if at_version and at_version != 'latest' else None
818 is_outdated = lambda co: \
819 not c.at_version_num \
820 or co.pull_request_version_id <= c.at_version_num
808 821
809 c.inline_cnt = cc_model.get_inline_comments_count(
810 c.inline_comments, version=at_version)
811
812 # load compare data into template context
813 enable_comments = not pr_closed
814 self._load_compare_data(
815 pull_request_at_ver,
816 c.inline_comments, enable_comments=enable_comments)
822 # inline_comments_until_version
823 if c.at_version_num:
824 # if we use version, then do not show later comments
825 # than current version
826 paths = collections.defaultdict(lambda: collections.defaultdict(list))
827 for fname, per_line_comments in inline_comments.iteritems():
828 for lno, comments in per_line_comments.iteritems():
829 for co in comments:
830 if co.pull_request_version_id and is_outdated(co):
831 paths[co.f_path][co.line_no].append(co)
832 inline_comments = paths
817 833
818 834 # outdated comments
819 c.outdated_comments = {}
820 835 c.outdated_cnt = 0
821
822 836 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
823 c.outdated_comments = cc_model.get_outdated_comments(
837 outdated_comments = cc_model.get_outdated_comments(
824 838 c.rhodecode_db_repo.repo_id,
825 839 pull_request=pull_request_at_ver)
826 840
827 841 # Count outdated comments and check for deleted files
828 for file_name, lines in c.outdated_comments.iteritems():
842 is_outdated = lambda co: \
843 not c.at_version_num \
844 or co.pull_request_version_id < c.at_version_num
845 for file_name, lines in outdated_comments.iteritems():
829 846 for comments in lines.values():
830 comments = [comm for comm in comments
831 if comm.outdated_at_version(at_version)]
847 comments = [comm for comm in comments if is_outdated(comm)]
832 848 c.outdated_cnt += len(comments)
833 if file_name not in c.included_files:
834 c.deleted_files.append(file_name)
849
850 # load compare data into template context
851 self._load_compare_data(pull_request_at_ver, inline_comments)
835 852
836 853 # this is a hack to properly display links, when creating PR, the
837 854 # compare view and others uses different notation, and
838 855 # compare_commits.html renders links based on the target_repo.
839 856 # We need to swap that here to generate it properly on the html side
840 857 c.target_repo = c.source_repo
841 858
842 # comments
843 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
844 pull_request=pull_request_id)
859 # general comments
860 c.comments = cc_model.get_comments(
861 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
845 862
846 863 if c.allowed_to_update:
847 864 force_close = ('forced_closed', _('Close Pull Request'))
848 865 statuses = ChangesetStatus.STATUSES + [force_close]
849 866 else:
850 867 statuses = ChangesetStatus.STATUSES
851 868 c.commit_statuses = statuses
852 869
853 870 c.ancestor = None # TODO: add ancestor here
854 871 c.pull_request = pull_request_display_obj
855 872 c.pull_request_latest = pull_request_latest
856 873 c.at_version = at_version
857 874
858 875 c.versions = pull_request_display_obj.versions()
859 876 c.changes = None
860 877 c.file_changes = None
861 878
862 c.show_version_changes = 1
879 c.show_version_changes = 1 # control flag, not used yet
863 880
864 881 if at_version and c.show_version_changes:
865 882 c.changes, c.file_changes = self._get_pr_version_changes(
866 883 version, pull_request_latest)
867 884
868 885 return render('/pullrequests/pullrequest_show.html')
869 886
870 887 @LoginRequired()
871 888 @NotAnonymous()
872 889 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
873 890 'repository.admin')
874 891 @auth.CSRFRequired()
875 892 @jsonify
876 893 def comment(self, repo_name, pull_request_id):
877 894 pull_request_id = safe_int(pull_request_id)
878 895 pull_request = PullRequest.get_or_404(pull_request_id)
879 896 if pull_request.is_closed():
880 897 raise HTTPForbidden()
881 898
882 899 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
883 900 # as a changeset status, still we want to send it in one value.
884 901 status = request.POST.get('changeset_status', None)
885 902 text = request.POST.get('text')
886 903 if status and '_closed' in status:
887 904 close_pr = True
888 905 status = status.replace('_closed', '')
889 906 else:
890 907 close_pr = False
891 908
892 909 forced = (status == 'forced')
893 910 if forced:
894 911 status = 'rejected'
895 912
896 913 allowed_to_change_status = PullRequestModel().check_user_change_status(
897 914 pull_request, c.rhodecode_user)
898 915
899 916 if status and allowed_to_change_status:
900 917 message = (_('Status change %(transition_icon)s %(status)s')
901 918 % {'transition_icon': '>',
902 919 'status': ChangesetStatus.get_status_lbl(status)})
903 920 if close_pr:
904 921 message = _('Closing with') + ' ' + message
905 922 text = text or message
906 923 comm = ChangesetCommentsModel().create(
907 924 text=text,
908 925 repo=c.rhodecode_db_repo.repo_id,
909 926 user=c.rhodecode_user.user_id,
910 927 pull_request=pull_request_id,
911 928 f_path=request.POST.get('f_path'),
912 929 line_no=request.POST.get('line'),
913 930 status_change=(ChangesetStatus.get_status_lbl(status)
914 931 if status and allowed_to_change_status else None),
915 932 status_change_type=(status
916 933 if status and allowed_to_change_status else None),
917 934 closing_pr=close_pr
918 935 )
919 936
920 937 if allowed_to_change_status:
921 938 old_calculated_status = pull_request.calculated_review_status()
922 939 # get status if set !
923 940 if status:
924 941 ChangesetStatusModel().set_status(
925 942 c.rhodecode_db_repo.repo_id,
926 943 status,
927 944 c.rhodecode_user.user_id,
928 945 comm,
929 946 pull_request=pull_request_id
930 947 )
931 948
932 949 Session().flush()
933 950 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
934 951 # we now calculate the status of pull request, and based on that
935 952 # calculation we set the commits status
936 953 calculated_status = pull_request.calculated_review_status()
937 954 if old_calculated_status != calculated_status:
938 955 PullRequestModel()._trigger_pull_request_hook(
939 956 pull_request, c.rhodecode_user, 'review_status_change')
940 957
941 958 calculated_status_lbl = ChangesetStatus.get_status_lbl(
942 959 calculated_status)
943 960
944 961 if close_pr:
945 962 status_completed = (
946 963 calculated_status in [ChangesetStatus.STATUS_APPROVED,
947 964 ChangesetStatus.STATUS_REJECTED])
948 965 if forced or status_completed:
949 966 PullRequestModel().close_pull_request(
950 967 pull_request_id, c.rhodecode_user)
951 968 else:
952 969 h.flash(_('Closing pull request on other statuses than '
953 970 'rejected or approved is forbidden. '
954 971 'Calculated status from all reviewers '
955 972 'is currently: %s') % calculated_status_lbl,
956 973 category='warning')
957 974
958 975 Session().commit()
959 976
960 977 if not request.is_xhr:
961 978 return redirect(h.url('pullrequest_show', repo_name=repo_name,
962 979 pull_request_id=pull_request_id))
963 980
964 981 data = {
965 982 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
966 983 }
967 984 if comm:
968 985 c.co = comm
969 986 data.update(comm.get_dict())
970 987 data.update({'rendered_text':
971 988 render('changeset/changeset_comment_block.html')})
972 989
973 990 return data
974 991
975 992 @LoginRequired()
976 993 @NotAnonymous()
977 994 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
978 995 'repository.admin')
979 996 @auth.CSRFRequired()
980 997 @jsonify
981 998 def delete_comment(self, repo_name, comment_id):
982 999 return self._delete_comment(comment_id)
983 1000
984 1001 def _delete_comment(self, comment_id):
985 1002 comment_id = safe_int(comment_id)
986 1003 co = ChangesetComment.get_or_404(comment_id)
987 1004 if co.pull_request.is_closed():
988 1005 # don't allow deleting comments on closed pull request
989 1006 raise HTTPForbidden()
990 1007
991 1008 is_owner = co.author.user_id == c.rhodecode_user.user_id
992 1009 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
993 1010 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
994 1011 old_calculated_status = co.pull_request.calculated_review_status()
995 1012 ChangesetCommentsModel().delete(comment=co)
996 1013 Session().commit()
997 1014 calculated_status = co.pull_request.calculated_review_status()
998 1015 if old_calculated_status != calculated_status:
999 1016 PullRequestModel()._trigger_pull_request_hook(
1000 1017 co.pull_request, c.rhodecode_user, 'review_status_change')
1001 1018 return True
1002 1019 else:
1003 1020 raise HTTPForbidden()
@@ -1,1161 +1,1164 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of diffing helpers, previously part of vcs
24 24 """
25 25
26 26 import collections
27 27 import re
28 28 import difflib
29 29 import logging
30 30
31 31 from itertools import tee, imap
32 32
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.vcs.exceptions import VCSError
36 36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib.helpers import escape
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43 # define max context, a file with more than this numbers of lines is unusable
44 44 # in browser anyway
45 45 MAX_CONTEXT = 1024 * 1014
46 46
47 47
48 48 class OPS(object):
49 49 ADD = 'A'
50 50 MOD = 'M'
51 51 DEL = 'D'
52 52
53 53
54 54 def wrap_to_table(str_):
55 55 return '''<table class="code-difftable">
56 56 <tr class="line no-comment">
57 57 <td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
58 58 <td></td>
59 59 <td class="lineno new"></td>
60 60 <td class="code no-comment"><pre>%s</pre></td>
61 61 </tr>
62 62 </table>''' % (_('Click to comment'), str_)
63 63
64 64
65 65 def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None,
66 66 show_full_diff=False, ignore_whitespace=True, line_context=3,
67 67 enable_comments=False):
68 68 """
69 69 returns a wrapped diff into a table, checks for cut_off_limit for file and
70 70 whole diff and presents proper message
71 71 """
72 72
73 73 if filenode_old is None:
74 74 filenode_old = FileNode(filenode_new.path, '', EmptyCommit())
75 75
76 76 if filenode_old.is_binary or filenode_new.is_binary:
77 77 diff = wrap_to_table(_('Binary file'))
78 78 stats = None
79 79 size = 0
80 80 data = None
81 81
82 82 elif diff_limit != -1 and (diff_limit is None or
83 83 (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
84 84
85 85 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
86 86 ignore_whitespace=ignore_whitespace,
87 87 context=line_context)
88 88 diff_processor = DiffProcessor(
89 89 f_gitdiff, format='gitdiff', diff_limit=diff_limit,
90 90 file_limit=file_limit, show_full_diff=show_full_diff)
91 91 _parsed = diff_processor.prepare()
92 92
93 93 diff = diff_processor.as_html(enable_comments=enable_comments)
94 94 stats = _parsed[0]['stats'] if _parsed else None
95 95 size = len(diff or '')
96 96 data = _parsed[0] if _parsed else None
97 97 else:
98 98 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
99 99 'diff menu to display this diff'))
100 100 stats = None
101 101 size = 0
102 102 data = None
103 103 if not diff:
104 104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 105 [filenode_new, filenode_old])
106 106 if submodules:
107 107 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
108 108 else:
109 109 diff = wrap_to_table(_('No changes detected'))
110 110
111 111 cs1 = filenode_old.commit.raw_id
112 112 cs2 = filenode_new.commit.raw_id
113 113
114 114 return size, cs1, cs2, diff, stats, data
115 115
116 116
117 117 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
118 118 """
119 119 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
120 120
121 121 :param ignore_whitespace: ignore whitespaces in diff
122 122 """
123 123 # make sure we pass in default context
124 124 context = context or 3
125 125 # protect against IntOverflow when passing HUGE context
126 126 if context > MAX_CONTEXT:
127 127 context = MAX_CONTEXT
128 128
129 129 submodules = filter(lambda o: isinstance(o, SubModuleNode),
130 130 [filenode_new, filenode_old])
131 131 if submodules:
132 132 return ''
133 133
134 134 for filenode in (filenode_old, filenode_new):
135 135 if not isinstance(filenode, FileNode):
136 136 raise VCSError(
137 137 "Given object should be FileNode object, not %s"
138 138 % filenode.__class__)
139 139
140 140 repo = filenode_new.commit.repository
141 141 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
142 142 new_commit = filenode_new.commit
143 143
144 144 vcs_gitdiff = repo.get_diff(
145 145 old_commit, new_commit, filenode_new.path,
146 146 ignore_whitespace, context, path1=filenode_old.path)
147 147 return vcs_gitdiff
148 148
149 149 NEW_FILENODE = 1
150 150 DEL_FILENODE = 2
151 151 MOD_FILENODE = 3
152 152 RENAMED_FILENODE = 4
153 153 COPIED_FILENODE = 5
154 154 CHMOD_FILENODE = 6
155 155 BIN_FILENODE = 7
156 156
157 157
158 158 class LimitedDiffContainer(object):
159 159
160 160 def __init__(self, diff_limit, cur_diff_size, diff):
161 161 self.diff = diff
162 162 self.diff_limit = diff_limit
163 163 self.cur_diff_size = cur_diff_size
164 164
165 165 def __getitem__(self, key):
166 166 return self.diff.__getitem__(key)
167 167
168 168 def __iter__(self):
169 169 for l in self.diff:
170 170 yield l
171 171
172 172
173 173 class Action(object):
174 174 """
175 175 Contains constants for the action value of the lines in a parsed diff.
176 176 """
177 177
178 178 ADD = 'add'
179 179 DELETE = 'del'
180 180 UNMODIFIED = 'unmod'
181 181
182 182 CONTEXT = 'context'
183 183 OLD_NO_NL = 'old-no-nl'
184 184 NEW_NO_NL = 'new-no-nl'
185 185
186 186
187 187 class DiffProcessor(object):
188 188 """
189 189 Give it a unified or git diff and it returns a list of the files that were
190 190 mentioned in the diff together with a dict of meta information that
191 191 can be used to render it in a HTML template.
192 192
193 193 .. note:: Unicode handling
194 194
195 195 The original diffs are a byte sequence and can contain filenames
196 196 in mixed encodings. This class generally returns `unicode` objects
197 197 since the result is intended for presentation to the user.
198 198
199 199 """
200 200 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
201 201 _newline_marker = re.compile(r'^\\ No newline at end of file')
202 202
203 203 # used for inline highlighter word split
204 204 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
205 205
206 # collapse ranges of commits over given number
207 _collapse_commits_over = 5
208
206 209 def __init__(self, diff, format='gitdiff', diff_limit=None,
207 210 file_limit=None, show_full_diff=True):
208 211 """
209 212 :param diff: A `Diff` object representing a diff from a vcs backend
210 213 :param format: format of diff passed, `udiff` or `gitdiff`
211 214 :param diff_limit: define the size of diff that is considered "big"
212 215 based on that parameter cut off will be triggered, set to None
213 216 to show full diff
214 217 """
215 218 self._diff = diff
216 219 self._format = format
217 220 self.adds = 0
218 221 self.removes = 0
219 222 # calculate diff size
220 223 self.diff_limit = diff_limit
221 224 self.file_limit = file_limit
222 225 self.show_full_diff = show_full_diff
223 226 self.cur_diff_size = 0
224 227 self.parsed = False
225 228 self.parsed_diff = []
226 229
227 230 if format == 'gitdiff':
228 231 self.differ = self._highlight_line_difflib
229 232 self._parser = self._parse_gitdiff
230 233 else:
231 234 self.differ = self._highlight_line_udiff
232 235 self._parser = self._new_parse_gitdiff
233 236
234 237 def _copy_iterator(self):
235 238 """
236 239 make a fresh copy of generator, we should not iterate thru
237 240 an original as it's needed for repeating operations on
238 241 this instance of DiffProcessor
239 242 """
240 243 self.__udiff, iterator_copy = tee(self.__udiff)
241 244 return iterator_copy
242 245
243 246 def _escaper(self, string):
244 247 """
245 248 Escaper for diff escapes special chars and checks the diff limit
246 249
247 250 :param string:
248 251 """
249 252
250 253 self.cur_diff_size += len(string)
251 254
252 255 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
253 256 raise DiffLimitExceeded('Diff Limit Exceeded')
254 257
255 258 return safe_unicode(string)\
256 259 .replace('&', '&amp;')\
257 260 .replace('<', '&lt;')\
258 261 .replace('>', '&gt;')
259 262
260 263 def _line_counter(self, l):
261 264 """
262 265 Checks each line and bumps total adds/removes for this diff
263 266
264 267 :param l:
265 268 """
266 269 if l.startswith('+') and not l.startswith('+++'):
267 270 self.adds += 1
268 271 elif l.startswith('-') and not l.startswith('---'):
269 272 self.removes += 1
270 273 return safe_unicode(l)
271 274
272 275 def _highlight_line_difflib(self, line, next_):
273 276 """
274 277 Highlight inline changes in both lines.
275 278 """
276 279
277 280 if line['action'] == Action.DELETE:
278 281 old, new = line, next_
279 282 else:
280 283 old, new = next_, line
281 284
282 285 oldwords = self._token_re.split(old['line'])
283 286 newwords = self._token_re.split(new['line'])
284 287 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
285 288
286 289 oldfragments, newfragments = [], []
287 290 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
288 291 oldfrag = ''.join(oldwords[i1:i2])
289 292 newfrag = ''.join(newwords[j1:j2])
290 293 if tag != 'equal':
291 294 if oldfrag:
292 295 oldfrag = '<del>%s</del>' % oldfrag
293 296 if newfrag:
294 297 newfrag = '<ins>%s</ins>' % newfrag
295 298 oldfragments.append(oldfrag)
296 299 newfragments.append(newfrag)
297 300
298 301 old['line'] = "".join(oldfragments)
299 302 new['line'] = "".join(newfragments)
300 303
301 304 def _highlight_line_udiff(self, line, next_):
302 305 """
303 306 Highlight inline changes in both lines.
304 307 """
305 308 start = 0
306 309 limit = min(len(line['line']), len(next_['line']))
307 310 while start < limit and line['line'][start] == next_['line'][start]:
308 311 start += 1
309 312 end = -1
310 313 limit -= start
311 314 while -end <= limit and line['line'][end] == next_['line'][end]:
312 315 end -= 1
313 316 end += 1
314 317 if start or end:
315 318 def do(l):
316 319 last = end + len(l['line'])
317 320 if l['action'] == Action.ADD:
318 321 tag = 'ins'
319 322 else:
320 323 tag = 'del'
321 324 l['line'] = '%s<%s>%s</%s>%s' % (
322 325 l['line'][:start],
323 326 tag,
324 327 l['line'][start:last],
325 328 tag,
326 329 l['line'][last:]
327 330 )
328 331 do(line)
329 332 do(next_)
330 333
331 334 def _clean_line(self, line, command):
332 335 if command in ['+', '-', ' ']:
333 336 # only modify the line if it's actually a diff thing
334 337 line = line[1:]
335 338 return line
336 339
337 340 def _parse_gitdiff(self, inline_diff=True):
338 341 _files = []
339 342 diff_container = lambda arg: arg
340 343
341 344 for chunk in self._diff.chunks():
342 345 head = chunk.header
343 346
344 347 diff = imap(self._escaper, chunk.diff.splitlines(1))
345 348 raw_diff = chunk.raw
346 349 limited_diff = False
347 350 exceeds_limit = False
348 351
349 352 op = None
350 353 stats = {
351 354 'added': 0,
352 355 'deleted': 0,
353 356 'binary': False,
354 357 'ops': {},
355 358 }
356 359
357 360 if head['deleted_file_mode']:
358 361 op = OPS.DEL
359 362 stats['binary'] = True
360 363 stats['ops'][DEL_FILENODE] = 'deleted file'
361 364
362 365 elif head['new_file_mode']:
363 366 op = OPS.ADD
364 367 stats['binary'] = True
365 368 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
366 369 else: # modify operation, can be copy, rename or chmod
367 370
368 371 # CHMOD
369 372 if head['new_mode'] and head['old_mode']:
370 373 op = OPS.MOD
371 374 stats['binary'] = True
372 375 stats['ops'][CHMOD_FILENODE] = (
373 376 'modified file chmod %s => %s' % (
374 377 head['old_mode'], head['new_mode']))
375 378 # RENAME
376 379 if head['rename_from'] != head['rename_to']:
377 380 op = OPS.MOD
378 381 stats['binary'] = True
379 382 stats['ops'][RENAMED_FILENODE] = (
380 383 'file renamed from %s to %s' % (
381 384 head['rename_from'], head['rename_to']))
382 385 # COPY
383 386 if head.get('copy_from') and head.get('copy_to'):
384 387 op = OPS.MOD
385 388 stats['binary'] = True
386 389 stats['ops'][COPIED_FILENODE] = (
387 390 'file copied from %s to %s' % (
388 391 head['copy_from'], head['copy_to']))
389 392
390 393 # If our new parsed headers didn't match anything fallback to
391 394 # old style detection
392 395 if op is None:
393 396 if not head['a_file'] and head['b_file']:
394 397 op = OPS.ADD
395 398 stats['binary'] = True
396 399 stats['ops'][NEW_FILENODE] = 'new file'
397 400
398 401 elif head['a_file'] and not head['b_file']:
399 402 op = OPS.DEL
400 403 stats['binary'] = True
401 404 stats['ops'][DEL_FILENODE] = 'deleted file'
402 405
403 406 # it's not ADD not DELETE
404 407 if op is None:
405 408 op = OPS.MOD
406 409 stats['binary'] = True
407 410 stats['ops'][MOD_FILENODE] = 'modified file'
408 411
409 412 # a real non-binary diff
410 413 if head['a_file'] or head['b_file']:
411 414 try:
412 415 raw_diff, chunks, _stats = self._parse_lines(diff)
413 416 stats['binary'] = False
414 417 stats['added'] = _stats[0]
415 418 stats['deleted'] = _stats[1]
416 419 # explicit mark that it's a modified file
417 420 if op == OPS.MOD:
418 421 stats['ops'][MOD_FILENODE] = 'modified file'
419 422 exceeds_limit = len(raw_diff) > self.file_limit
420 423
421 424 # changed from _escaper function so we validate size of
422 425 # each file instead of the whole diff
423 426 # diff will hide big files but still show small ones
424 427 # from my tests, big files are fairly safe to be parsed
425 428 # but the browser is the bottleneck
426 429 if not self.show_full_diff and exceeds_limit:
427 430 raise DiffLimitExceeded('File Limit Exceeded')
428 431
429 432 except DiffLimitExceeded:
430 433 diff_container = lambda _diff: \
431 434 LimitedDiffContainer(
432 435 self.diff_limit, self.cur_diff_size, _diff)
433 436
434 437 exceeds_limit = len(raw_diff) > self.file_limit
435 438 limited_diff = True
436 439 chunks = []
437 440
438 441 else: # GIT format binary patch, or possibly empty diff
439 442 if head['bin_patch']:
440 443 # we have operation already extracted, but we mark simply
441 444 # it's a diff we wont show for binary files
442 445 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
443 446 chunks = []
444 447
445 448 if chunks and not self.show_full_diff and op == OPS.DEL:
446 449 # if not full diff mode show deleted file contents
447 450 # TODO: anderson: if the view is not too big, there is no way
448 451 # to see the content of the file
449 452 chunks = []
450 453
451 454 chunks.insert(0, [{
452 455 'old_lineno': '',
453 456 'new_lineno': '',
454 457 'action': Action.CONTEXT,
455 458 'line': msg,
456 459 } for _op, msg in stats['ops'].iteritems()
457 460 if _op not in [MOD_FILENODE]])
458 461
459 462 _files.append({
460 463 'filename': safe_unicode(head['b_path']),
461 464 'old_revision': head['a_blob_id'],
462 465 'new_revision': head['b_blob_id'],
463 466 'chunks': chunks,
464 467 'raw_diff': safe_unicode(raw_diff),
465 468 'operation': op,
466 469 'stats': stats,
467 470 'exceeds_limit': exceeds_limit,
468 471 'is_limited_diff': limited_diff,
469 472 })
470 473
471 474 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
472 475 OPS.DEL: 2}.get(info['operation'])
473 476
474 477 if not inline_diff:
475 478 return diff_container(sorted(_files, key=sorter))
476 479
477 480 # highlight inline changes
478 481 for diff_data in _files:
479 482 for chunk in diff_data['chunks']:
480 483 lineiter = iter(chunk)
481 484 try:
482 485 while 1:
483 486 line = lineiter.next()
484 487 if line['action'] not in (
485 488 Action.UNMODIFIED, Action.CONTEXT):
486 489 nextline = lineiter.next()
487 490 if nextline['action'] in ['unmod', 'context'] or \
488 491 nextline['action'] == line['action']:
489 492 continue
490 493 self.differ(line, nextline)
491 494 except StopIteration:
492 495 pass
493 496
494 497 return diff_container(sorted(_files, key=sorter))
495 498
496 499
497 500 # FIXME: NEWDIFFS: dan: this replaces the old _escaper function
498 501 def _process_line(self, string):
499 502 """
500 503 Process a diff line, checks the diff limit
501 504
502 505 :param string:
503 506 """
504 507
505 508 self.cur_diff_size += len(string)
506 509
507 510 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
508 511 raise DiffLimitExceeded('Diff Limit Exceeded')
509 512
510 513 return safe_unicode(string)
511 514
512 515 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
513 516 def _new_parse_gitdiff(self, inline_diff=True):
514 517 _files = []
515 518 diff_container = lambda arg: arg
516 519 for chunk in self._diff.chunks():
517 520 head = chunk.header
518 521 log.debug('parsing diff %r' % head)
519 522
520 523 diff = imap(self._process_line, chunk.diff.splitlines(1))
521 524 raw_diff = chunk.raw
522 525 limited_diff = False
523 526 exceeds_limit = False
524 527 # if 'empty_file_to_modify_and_rename' in head['a_path']:
525 528 # 1/0
526 529 op = None
527 530 stats = {
528 531 'added': 0,
529 532 'deleted': 0,
530 533 'binary': False,
531 534 'old_mode': None,
532 535 'new_mode': None,
533 536 'ops': {},
534 537 }
535 538 if head['old_mode']:
536 539 stats['old_mode'] = head['old_mode']
537 540 if head['new_mode']:
538 541 stats['new_mode'] = head['new_mode']
539 542 if head['b_mode']:
540 543 stats['new_mode'] = head['b_mode']
541 544
542 545 if head['deleted_file_mode']:
543 546 op = OPS.DEL
544 547 stats['binary'] = True
545 548 stats['ops'][DEL_FILENODE] = 'deleted file'
546 549
547 550 elif head['new_file_mode']:
548 551 op = OPS.ADD
549 552 stats['binary'] = True
550 553 stats['old_mode'] = None
551 554 stats['new_mode'] = head['new_file_mode']
552 555 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
553 556 else: # modify operation, can be copy, rename or chmod
554 557
555 558 # CHMOD
556 559 if head['new_mode'] and head['old_mode']:
557 560 op = OPS.MOD
558 561 stats['binary'] = True
559 562 stats['ops'][CHMOD_FILENODE] = (
560 563 'modified file chmod %s => %s' % (
561 564 head['old_mode'], head['new_mode']))
562 565
563 566 # RENAME
564 567 if head['rename_from'] != head['rename_to']:
565 568 op = OPS.MOD
566 569 stats['binary'] = True
567 570 stats['renamed'] = (head['rename_from'], head['rename_to'])
568 571 stats['ops'][RENAMED_FILENODE] = (
569 572 'file renamed from %s to %s' % (
570 573 head['rename_from'], head['rename_to']))
571 574 # COPY
572 575 if head.get('copy_from') and head.get('copy_to'):
573 576 op = OPS.MOD
574 577 stats['binary'] = True
575 578 stats['copied'] = (head['copy_from'], head['copy_to'])
576 579 stats['ops'][COPIED_FILENODE] = (
577 580 'file copied from %s to %s' % (
578 581 head['copy_from'], head['copy_to']))
579 582
580 583 # If our new parsed headers didn't match anything fallback to
581 584 # old style detection
582 585 if op is None:
583 586 if not head['a_file'] and head['b_file']:
584 587 op = OPS.ADD
585 588 stats['binary'] = True
586 589 stats['new_file'] = True
587 590 stats['ops'][NEW_FILENODE] = 'new file'
588 591
589 592 elif head['a_file'] and not head['b_file']:
590 593 op = OPS.DEL
591 594 stats['binary'] = True
592 595 stats['ops'][DEL_FILENODE] = 'deleted file'
593 596
594 597 # it's not ADD not DELETE
595 598 if op is None:
596 599 op = OPS.MOD
597 600 stats['binary'] = True
598 601 stats['ops'][MOD_FILENODE] = 'modified file'
599 602
600 603 # a real non-binary diff
601 604 if head['a_file'] or head['b_file']:
602 605 try:
603 606 raw_diff, chunks, _stats = self._new_parse_lines(diff)
604 607 stats['binary'] = False
605 608 stats['added'] = _stats[0]
606 609 stats['deleted'] = _stats[1]
607 610 # explicit mark that it's a modified file
608 611 if op == OPS.MOD:
609 612 stats['ops'][MOD_FILENODE] = 'modified file'
610 613 exceeds_limit = len(raw_diff) > self.file_limit
611 614
612 615 # changed from _escaper function so we validate size of
613 616 # each file instead of the whole diff
614 617 # diff will hide big files but still show small ones
615 618 # from my tests, big files are fairly safe to be parsed
616 619 # but the browser is the bottleneck
617 620 if not self.show_full_diff and exceeds_limit:
618 621 raise DiffLimitExceeded('File Limit Exceeded')
619 622
620 623 except DiffLimitExceeded:
621 624 diff_container = lambda _diff: \
622 625 LimitedDiffContainer(
623 626 self.diff_limit, self.cur_diff_size, _diff)
624 627
625 628 exceeds_limit = len(raw_diff) > self.file_limit
626 629 limited_diff = True
627 630 chunks = []
628 631
629 632 else: # GIT format binary patch, or possibly empty diff
630 633 if head['bin_patch']:
631 634 # we have operation already extracted, but we mark simply
632 635 # it's a diff we wont show for binary files
633 636 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
634 637 chunks = []
635 638
636 639 if chunks and not self.show_full_diff and op == OPS.DEL:
637 640 # if not full diff mode show deleted file contents
638 641 # TODO: anderson: if the view is not too big, there is no way
639 642 # to see the content of the file
640 643 chunks = []
641 644
642 645 chunks.insert(0, [{
643 646 'old_lineno': '',
644 647 'new_lineno': '',
645 648 'action': Action.CONTEXT,
646 649 'line': msg,
647 650 } for _op, msg in stats['ops'].iteritems()
648 651 if _op not in [MOD_FILENODE]])
649 652
650 653 original_filename = safe_unicode(head['a_path'])
651 654 _files.append({
652 655 'original_filename': original_filename,
653 656 'filename': safe_unicode(head['b_path']),
654 657 'old_revision': head['a_blob_id'],
655 658 'new_revision': head['b_blob_id'],
656 659 'chunks': chunks,
657 660 'raw_diff': safe_unicode(raw_diff),
658 661 'operation': op,
659 662 'stats': stats,
660 663 'exceeds_limit': exceeds_limit,
661 664 'is_limited_diff': limited_diff,
662 665 })
663 666
664 667
665 668 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
666 669 OPS.DEL: 2}.get(info['operation'])
667 670
668 671 return diff_container(sorted(_files, key=sorter))
669 672
670 673 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
671 674 def _parse_lines(self, diff):
672 675 """
673 676 Parse the diff an return data for the template.
674 677 """
675 678
676 679 lineiter = iter(diff)
677 680 stats = [0, 0]
678 681 chunks = []
679 682 raw_diff = []
680 683
681 684 try:
682 685 line = lineiter.next()
683 686
684 687 while line:
685 688 raw_diff.append(line)
686 689 lines = []
687 690 chunks.append(lines)
688 691
689 692 match = self._chunk_re.match(line)
690 693
691 694 if not match:
692 695 break
693 696
694 697 gr = match.groups()
695 698 (old_line, old_end,
696 699 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
697 700 old_line -= 1
698 701 new_line -= 1
699 702
700 703 context = len(gr) == 5
701 704 old_end += old_line
702 705 new_end += new_line
703 706
704 707 if context:
705 708 # skip context only if it's first line
706 709 if int(gr[0]) > 1:
707 710 lines.append({
708 711 'old_lineno': '...',
709 712 'new_lineno': '...',
710 713 'action': Action.CONTEXT,
711 714 'line': line,
712 715 })
713 716
714 717 line = lineiter.next()
715 718
716 719 while old_line < old_end or new_line < new_end:
717 720 command = ' '
718 721 if line:
719 722 command = line[0]
720 723
721 724 affects_old = affects_new = False
722 725
723 726 # ignore those if we don't expect them
724 727 if command in '#@':
725 728 continue
726 729 elif command == '+':
727 730 affects_new = True
728 731 action = Action.ADD
729 732 stats[0] += 1
730 733 elif command == '-':
731 734 affects_old = True
732 735 action = Action.DELETE
733 736 stats[1] += 1
734 737 else:
735 738 affects_old = affects_new = True
736 739 action = Action.UNMODIFIED
737 740
738 741 if not self._newline_marker.match(line):
739 742 old_line += affects_old
740 743 new_line += affects_new
741 744 lines.append({
742 745 'old_lineno': affects_old and old_line or '',
743 746 'new_lineno': affects_new and new_line or '',
744 747 'action': action,
745 748 'line': self._clean_line(line, command)
746 749 })
747 750 raw_diff.append(line)
748 751
749 752 line = lineiter.next()
750 753
751 754 if self._newline_marker.match(line):
752 755 # we need to append to lines, since this is not
753 756 # counted in the line specs of diff
754 757 lines.append({
755 758 'old_lineno': '...',
756 759 'new_lineno': '...',
757 760 'action': Action.CONTEXT,
758 761 'line': self._clean_line(line, command)
759 762 })
760 763
761 764 except StopIteration:
762 765 pass
763 766 return ''.join(raw_diff), chunks, stats
764 767
765 768 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
766 769 def _new_parse_lines(self, diff):
767 770 """
768 771 Parse the diff an return data for the template.
769 772 """
770 773
771 774 lineiter = iter(diff)
772 775 stats = [0, 0]
773 776 chunks = []
774 777 raw_diff = []
775 778
776 779 try:
777 780 line = lineiter.next()
778 781
779 782 while line:
780 783 raw_diff.append(line)
781 784 match = self._chunk_re.match(line)
782 785
783 786 if not match:
784 787 break
785 788
786 789 gr = match.groups()
787 790 (old_line, old_end,
788 791 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
789 792
790 793 lines = []
791 794 hunk = {
792 795 'section_header': gr[-1],
793 796 'source_start': old_line,
794 797 'source_length': old_end,
795 798 'target_start': new_line,
796 799 'target_length': new_end,
797 800 'lines': lines,
798 801 }
799 802 chunks.append(hunk)
800 803
801 804 old_line -= 1
802 805 new_line -= 1
803 806
804 807 context = len(gr) == 5
805 808 old_end += old_line
806 809 new_end += new_line
807 810
808 811 line = lineiter.next()
809 812
810 813 while old_line < old_end or new_line < new_end:
811 814 command = ' '
812 815 if line:
813 816 command = line[0]
814 817
815 818 affects_old = affects_new = False
816 819
817 820 # ignore those if we don't expect them
818 821 if command in '#@':
819 822 continue
820 823 elif command == '+':
821 824 affects_new = True
822 825 action = Action.ADD
823 826 stats[0] += 1
824 827 elif command == '-':
825 828 affects_old = True
826 829 action = Action.DELETE
827 830 stats[1] += 1
828 831 else:
829 832 affects_old = affects_new = True
830 833 action = Action.UNMODIFIED
831 834
832 835 if not self._newline_marker.match(line):
833 836 old_line += affects_old
834 837 new_line += affects_new
835 838 lines.append({
836 839 'old_lineno': affects_old and old_line or '',
837 840 'new_lineno': affects_new and new_line or '',
838 841 'action': action,
839 842 'line': self._clean_line(line, command)
840 843 })
841 844 raw_diff.append(line)
842 845
843 846 line = lineiter.next()
844 847
845 848 if self._newline_marker.match(line):
846 849 # we need to append to lines, since this is not
847 850 # counted in the line specs of diff
848 851 if affects_old:
849 852 action = Action.OLD_NO_NL
850 853 elif affects_new:
851 854 action = Action.NEW_NO_NL
852 855 else:
853 856 raise Exception('invalid context for no newline')
854 857
855 858 lines.append({
856 859 'old_lineno': None,
857 860 'new_lineno': None,
858 861 'action': action,
859 862 'line': self._clean_line(line, command)
860 863 })
861 864
862 865 except StopIteration:
863 866 pass
864 867 return ''.join(raw_diff), chunks, stats
865 868
866 869 def _safe_id(self, idstring):
867 870 """Make a string safe for including in an id attribute.
868 871
869 872 The HTML spec says that id attributes 'must begin with
870 873 a letter ([A-Za-z]) and may be followed by any number
871 874 of letters, digits ([0-9]), hyphens ("-"), underscores
872 875 ("_"), colons (":"), and periods (".")'. These regexps
873 876 are slightly over-zealous, in that they remove colons
874 877 and periods unnecessarily.
875 878
876 879 Whitespace is transformed into underscores, and then
877 880 anything which is not a hyphen or a character that
878 881 matches \w (alphanumerics and underscore) is removed.
879 882
880 883 """
881 884 # Transform all whitespace to underscore
882 885 idstring = re.sub(r'\s', "_", '%s' % idstring)
883 886 # Remove everything that is not a hyphen or a member of \w
884 887 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
885 888 return idstring
886 889
887 890 def prepare(self, inline_diff=True):
888 891 """
889 892 Prepare the passed udiff for HTML rendering.
890 893
891 894 :return: A list of dicts with diff information.
892 895 """
893 896 parsed = self._parser(inline_diff=inline_diff)
894 897 self.parsed = True
895 898 self.parsed_diff = parsed
896 899 return parsed
897 900
898 901 def as_raw(self, diff_lines=None):
899 902 """
900 903 Returns raw diff as a byte string
901 904 """
902 905 return self._diff.raw
903 906
904 907 def as_html(self, table_class='code-difftable', line_class='line',
905 908 old_lineno_class='lineno old', new_lineno_class='lineno new',
906 909 code_class='code', enable_comments=False, parsed_lines=None):
907 910 """
908 911 Return given diff as html table with customized css classes
909 912 """
910 913 def _link_to_if(condition, label, url):
911 914 """
912 915 Generates a link if condition is meet or just the label if not.
913 916 """
914 917
915 918 if condition:
916 919 return '''<a href="%(url)s" class="tooltip"
917 920 title="%(title)s">%(label)s</a>''' % {
918 921 'title': _('Click to select line'),
919 922 'url': url,
920 923 'label': label
921 924 }
922 925 else:
923 926 return label
924 927 if not self.parsed:
925 928 self.prepare()
926 929
927 930 diff_lines = self.parsed_diff
928 931 if parsed_lines:
929 932 diff_lines = parsed_lines
930 933
931 934 _html_empty = True
932 935 _html = []
933 936 _html.append('''<table class="%(table_class)s">\n''' % {
934 937 'table_class': table_class
935 938 })
936 939
937 940 for diff in diff_lines:
938 941 for line in diff['chunks']:
939 942 _html_empty = False
940 943 for change in line:
941 944 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
942 945 'lc': line_class,
943 946 'action': change['action']
944 947 })
945 948 anchor_old_id = ''
946 949 anchor_new_id = ''
947 950 anchor_old = "%(filename)s_o%(oldline_no)s" % {
948 951 'filename': self._safe_id(diff['filename']),
949 952 'oldline_no': change['old_lineno']
950 953 }
951 954 anchor_new = "%(filename)s_n%(oldline_no)s" % {
952 955 'filename': self._safe_id(diff['filename']),
953 956 'oldline_no': change['new_lineno']
954 957 }
955 958 cond_old = (change['old_lineno'] != '...' and
956 959 change['old_lineno'])
957 960 cond_new = (change['new_lineno'] != '...' and
958 961 change['new_lineno'])
959 962 if cond_old:
960 963 anchor_old_id = 'id="%s"' % anchor_old
961 964 if cond_new:
962 965 anchor_new_id = 'id="%s"' % anchor_new
963 966
964 967 if change['action'] != Action.CONTEXT:
965 968 anchor_link = True
966 969 else:
967 970 anchor_link = False
968 971
969 972 ###########################################################
970 973 # COMMENT ICONS
971 974 ###########################################################
972 975 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
973 976
974 977 if enable_comments and change['action'] != Action.CONTEXT:
975 978 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
976 979
977 980 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
978 981
979 982 ###########################################################
980 983 # OLD LINE NUMBER
981 984 ###########################################################
982 985 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
983 986 'a_id': anchor_old_id,
984 987 'olc': old_lineno_class
985 988 })
986 989
987 990 _html.append('''%(link)s''' % {
988 991 'link': _link_to_if(anchor_link, change['old_lineno'],
989 992 '#%s' % anchor_old)
990 993 })
991 994 _html.append('''</td>\n''')
992 995 ###########################################################
993 996 # NEW LINE NUMBER
994 997 ###########################################################
995 998
996 999 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
997 1000 'a_id': anchor_new_id,
998 1001 'nlc': new_lineno_class
999 1002 })
1000 1003
1001 1004 _html.append('''%(link)s''' % {
1002 1005 'link': _link_to_if(anchor_link, change['new_lineno'],
1003 1006 '#%s' % anchor_new)
1004 1007 })
1005 1008 _html.append('''</td>\n''')
1006 1009 ###########################################################
1007 1010 # CODE
1008 1011 ###########################################################
1009 1012 code_classes = [code_class]
1010 1013 if (not enable_comments or
1011 1014 change['action'] == Action.CONTEXT):
1012 1015 code_classes.append('no-comment')
1013 1016 _html.append('\t<td class="%s">' % ' '.join(code_classes))
1014 1017 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1015 1018 'code': change['line']
1016 1019 })
1017 1020
1018 1021 _html.append('''\t</td>''')
1019 1022 _html.append('''\n</tr>\n''')
1020 1023 _html.append('''</table>''')
1021 1024 if _html_empty:
1022 1025 return None
1023 1026 return ''.join(_html)
1024 1027
1025 1028 def stat(self):
1026 1029 """
1027 1030 Returns tuple of added, and removed lines for this instance
1028 1031 """
1029 1032 return self.adds, self.removes
1030 1033
1031 1034 def get_context_of_line(
1032 1035 self, path, diff_line=None, context_before=3, context_after=3):
1033 1036 """
1034 1037 Returns the context lines for the specified diff line.
1035 1038
1036 1039 :type diff_line: :class:`DiffLineNumber`
1037 1040 """
1038 1041 assert self.parsed, "DiffProcessor is not initialized."
1039 1042
1040 1043 if None not in diff_line:
1041 1044 raise ValueError(
1042 1045 "Cannot specify both line numbers: {}".format(diff_line))
1043 1046
1044 1047 file_diff = self._get_file_diff(path)
1045 1048 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1046 1049
1047 1050 first_line_to_include = max(idx - context_before, 0)
1048 1051 first_line_after_context = idx + context_after + 1
1049 1052 context_lines = chunk[first_line_to_include:first_line_after_context]
1050 1053
1051 1054 line_contents = [
1052 1055 _context_line(line) for line in context_lines
1053 1056 if _is_diff_content(line)]
1054 1057 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1055 1058 # Once they are fixed, we can drop this line here.
1056 1059 if line_contents:
1057 1060 line_contents[-1] = (
1058 1061 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1059 1062 return line_contents
1060 1063
1061 1064 def find_context(self, path, context, offset=0):
1062 1065 """
1063 1066 Finds the given `context` inside of the diff.
1064 1067
1065 1068 Use the parameter `offset` to specify which offset the target line has
1066 1069 inside of the given `context`. This way the correct diff line will be
1067 1070 returned.
1068 1071
1069 1072 :param offset: Shall be used to specify the offset of the main line
1070 1073 within the given `context`.
1071 1074 """
1072 1075 if offset < 0 or offset >= len(context):
1073 1076 raise ValueError(
1074 1077 "Only positive values up to the length of the context "
1075 1078 "minus one are allowed.")
1076 1079
1077 1080 matches = []
1078 1081 file_diff = self._get_file_diff(path)
1079 1082
1080 1083 for chunk in file_diff['chunks']:
1081 1084 context_iter = iter(context)
1082 1085 for line_idx, line in enumerate(chunk):
1083 1086 try:
1084 1087 if _context_line(line) == context_iter.next():
1085 1088 continue
1086 1089 except StopIteration:
1087 1090 matches.append((line_idx, chunk))
1088 1091 context_iter = iter(context)
1089 1092
1090 1093 # Increment position and triger StopIteration
1091 1094 # if we had a match at the end
1092 1095 line_idx += 1
1093 1096 try:
1094 1097 context_iter.next()
1095 1098 except StopIteration:
1096 1099 matches.append((line_idx, chunk))
1097 1100
1098 1101 effective_offset = len(context) - offset
1099 1102 found_at_diff_lines = [
1100 1103 _line_to_diff_line_number(chunk[idx - effective_offset])
1101 1104 for idx, chunk in matches]
1102 1105
1103 1106 return found_at_diff_lines
1104 1107
1105 1108 def _get_file_diff(self, path):
1106 1109 for file_diff in self.parsed_diff:
1107 1110 if file_diff['filename'] == path:
1108 1111 break
1109 1112 else:
1110 1113 raise FileNotInDiffException("File {} not in diff".format(path))
1111 1114 return file_diff
1112 1115
1113 1116 def _find_chunk_line_index(self, file_diff, diff_line):
1114 1117 for chunk in file_diff['chunks']:
1115 1118 for idx, line in enumerate(chunk):
1116 1119 if line['old_lineno'] == diff_line.old:
1117 1120 return chunk, idx
1118 1121 if line['new_lineno'] == diff_line.new:
1119 1122 return chunk, idx
1120 1123 raise LineNotInDiffException(
1121 1124 "The line {} is not part of the diff.".format(diff_line))
1122 1125
1123 1126
1124 1127 def _is_diff_content(line):
1125 1128 return line['action'] in (
1126 1129 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1127 1130
1128 1131
1129 1132 def _context_line(line):
1130 1133 return (line['action'], line['line'])
1131 1134
1132 1135
1133 1136 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1134 1137
1135 1138
1136 1139 def _line_to_diff_line_number(line):
1137 1140 new_line_no = line['new_lineno'] or None
1138 1141 old_line_no = line['old_lineno'] or None
1139 1142 return DiffLineNumber(old=old_line_no, new=new_line_no)
1140 1143
1141 1144
1142 1145 class FileNotInDiffException(Exception):
1143 1146 """
1144 1147 Raised when the context for a missing file is requested.
1145 1148
1146 1149 If you request the context for a line in a file which is not part of the
1147 1150 given diff, then this exception is raised.
1148 1151 """
1149 1152
1150 1153
1151 1154 class LineNotInDiffException(Exception):
1152 1155 """
1153 1156 Raised when the context for a missing line is requested.
1154 1157
1155 1158 If you request the context for a line in a file and this line is not
1156 1159 part of the given diff, then this exception is raised.
1157 1160 """
1158 1161
1159 1162
1160 1163 class DiffLimitExceeded(Exception):
1161 1164 pass
@@ -1,525 +1,530 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from datetime import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pyramid.threadlocal import get_current_registry
33 33 from sqlalchemy.sql.expression import null
34 34 from sqlalchemy.sql.functions import coalesce
35 35
36 36 from rhodecode.lib import helpers as h, diffs
37 37 from rhodecode.lib.channelstream import channelstream_request
38 38 from rhodecode.lib.utils import action_logger
39 39 from rhodecode.lib.utils2 import extract_mentioned_users
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.db import (
42 42 ChangesetComment, User, Notification, PullRequest)
43 43 from rhodecode.model.notification import NotificationModel
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import VcsSettingsModel
46 46 from rhodecode.model.notification import EmailNotificationModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class ChangesetCommentsModel(BaseModel):
52 52
53 53 cls = ChangesetComment
54 54
55 55 DIFF_CONTEXT_BEFORE = 3
56 56 DIFF_CONTEXT_AFTER = 3
57 57
58 58 def __get_commit_comment(self, changeset_comment):
59 59 return self._get_instance(ChangesetComment, changeset_comment)
60 60
61 61 def __get_pull_request(self, pull_request):
62 62 return self._get_instance(PullRequest, pull_request)
63 63
64 64 def _extract_mentions(self, s):
65 65 user_objects = []
66 66 for username in extract_mentioned_users(s):
67 67 user_obj = User.get_by_username(username, case_insensitive=True)
68 68 if user_obj:
69 69 user_objects.append(user_obj)
70 70 return user_objects
71 71
72 72 def _get_renderer(self, global_renderer='rst'):
73 73 try:
74 74 # try reading from visual context
75 75 from pylons import tmpl_context
76 76 global_renderer = tmpl_context.visual.default_renderer
77 77 except AttributeError:
78 78 log.debug("Renderer not set, falling back "
79 79 "to default renderer '%s'", global_renderer)
80 80 except Exception:
81 81 log.error(traceback.format_exc())
82 82 return global_renderer
83 83
84 84 def create(self, text, repo, user, revision=None, pull_request=None,
85 85 f_path=None, line_no=None, status_change=None,
86 86 status_change_type=None, closing_pr=False,
87 87 send_email=True, renderer=None):
88 88 """
89 89 Creates new comment for commit or pull request.
90 90 IF status_change is not none this comment is associated with a
91 91 status change of commit or commit associated with pull request
92 92
93 93 :param text:
94 94 :param repo:
95 95 :param user:
96 96 :param revision:
97 97 :param pull_request:
98 98 :param f_path:
99 99 :param line_no:
100 100 :param status_change: Label for status change
101 101 :param status_change_type: type of status change
102 102 :param closing_pr:
103 103 :param send_email:
104 104 """
105 105 if not text:
106 106 log.warning('Missing text for comment, skipping...')
107 107 return
108 108
109 109 if not renderer:
110 110 renderer = self._get_renderer()
111 111
112 112 repo = self._get_repo(repo)
113 113 user = self._get_user(user)
114 114 comment = ChangesetComment()
115 115 comment.renderer = renderer
116 116 comment.repo = repo
117 117 comment.author = user
118 118 comment.text = text
119 119 comment.f_path = f_path
120 120 comment.line_no = line_no
121 121
122 122 #TODO (marcink): fix this and remove revision as param
123 123 commit_id = revision
124 124 pull_request_id = pull_request
125 125
126 126 commit_obj = None
127 127 pull_request_obj = None
128 128
129 129 if commit_id:
130 130 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
131 131 # do a lookup, so we don't pass something bad here
132 132 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
133 133 comment.revision = commit_obj.raw_id
134 134
135 135 elif pull_request_id:
136 136 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
137 137 pull_request_obj = self.__get_pull_request(pull_request_id)
138 138 comment.pull_request = pull_request_obj
139 139 else:
140 140 raise Exception('Please specify commit or pull_request_id')
141 141
142 142 Session().add(comment)
143 143 Session().flush()
144 144 kwargs = {
145 145 'user': user,
146 146 'renderer_type': renderer,
147 147 'repo_name': repo.repo_name,
148 148 'status_change': status_change,
149 149 'status_change_type': status_change_type,
150 150 'comment_body': text,
151 151 'comment_file': f_path,
152 152 'comment_line': line_no,
153 153 }
154 154
155 155 if commit_obj:
156 156 recipients = ChangesetComment.get_users(
157 157 revision=commit_obj.raw_id)
158 158 # add commit author if it's in RhodeCode system
159 159 cs_author = User.get_from_cs_author(commit_obj.author)
160 160 if not cs_author:
161 161 # use repo owner if we cannot extract the author correctly
162 162 cs_author = repo.user
163 163 recipients += [cs_author]
164 164
165 165 commit_comment_url = self.get_url(comment)
166 166
167 167 target_repo_url = h.link_to(
168 168 repo.repo_name,
169 169 h.url('summary_home',
170 170 repo_name=repo.repo_name, qualified=True))
171 171
172 172 # commit specifics
173 173 kwargs.update({
174 174 'commit': commit_obj,
175 175 'commit_message': commit_obj.message,
176 176 'commit_target_repo': target_repo_url,
177 177 'commit_comment_url': commit_comment_url,
178 178 })
179 179
180 180 elif pull_request_obj:
181 181 # get the current participants of this pull request
182 182 recipients = ChangesetComment.get_users(
183 183 pull_request_id=pull_request_obj.pull_request_id)
184 184 # add pull request author
185 185 recipients += [pull_request_obj.author]
186 186
187 187 # add the reviewers to notification
188 188 recipients += [x.user for x in pull_request_obj.reviewers]
189 189
190 190 pr_target_repo = pull_request_obj.target_repo
191 191 pr_source_repo = pull_request_obj.source_repo
192 192
193 193 pr_comment_url = h.url(
194 194 'pullrequest_show',
195 195 repo_name=pr_target_repo.repo_name,
196 196 pull_request_id=pull_request_obj.pull_request_id,
197 197 anchor='comment-%s' % comment.comment_id,
198 198 qualified=True,)
199 199
200 200 # set some variables for email notification
201 201 pr_target_repo_url = h.url(
202 202 'summary_home', repo_name=pr_target_repo.repo_name,
203 203 qualified=True)
204 204
205 205 pr_source_repo_url = h.url(
206 206 'summary_home', repo_name=pr_source_repo.repo_name,
207 207 qualified=True)
208 208
209 209 # pull request specifics
210 210 kwargs.update({
211 211 'pull_request': pull_request_obj,
212 212 'pr_id': pull_request_obj.pull_request_id,
213 213 'pr_target_repo': pr_target_repo,
214 214 'pr_target_repo_url': pr_target_repo_url,
215 215 'pr_source_repo': pr_source_repo,
216 216 'pr_source_repo_url': pr_source_repo_url,
217 217 'pr_comment_url': pr_comment_url,
218 218 'pr_closing': closing_pr,
219 219 })
220 220 if send_email:
221 221 # pre-generate the subject for notification itself
222 222 (subject,
223 223 _h, _e, # we don't care about those
224 224 body_plaintext) = EmailNotificationModel().render_email(
225 225 notification_type, **kwargs)
226 226
227 227 mention_recipients = set(
228 228 self._extract_mentions(text)).difference(recipients)
229 229
230 230 # create notification objects, and emails
231 231 NotificationModel().create(
232 232 created_by=user,
233 233 notification_subject=subject,
234 234 notification_body=body_plaintext,
235 235 notification_type=notification_type,
236 236 recipients=recipients,
237 237 mention_recipients=mention_recipients,
238 238 email_kwargs=kwargs,
239 239 )
240 240
241 241 action = (
242 242 'user_commented_pull_request:{}'.format(
243 243 comment.pull_request.pull_request_id)
244 244 if comment.pull_request
245 245 else 'user_commented_revision:{}'.format(comment.revision)
246 246 )
247 247 action_logger(user, action, comment.repo)
248 248
249 249 registry = get_current_registry()
250 250 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
251 251 channelstream_config = rhodecode_plugins.get('channelstream', {})
252 252 msg_url = ''
253 253 if commit_obj:
254 254 msg_url = commit_comment_url
255 255 repo_name = repo.repo_name
256 256 elif pull_request_obj:
257 257 msg_url = pr_comment_url
258 258 repo_name = pr_target_repo.repo_name
259 259
260 260 if channelstream_config.get('enabled'):
261 261 message = '<strong>{}</strong> {} - ' \
262 262 '<a onclick="window.location=\'{}\';' \
263 263 'window.location.reload()">' \
264 264 '<strong>{}</strong></a>'
265 265 message = message.format(
266 266 user.username, _('made a comment'), msg_url,
267 267 _('Show it now'))
268 268 channel = '/repo${}$/pr/{}'.format(
269 269 repo_name,
270 270 pull_request_id
271 271 )
272 272 payload = {
273 273 'type': 'message',
274 274 'timestamp': datetime.utcnow(),
275 275 'user': 'system',
276 276 'exclude_users': [user.username],
277 277 'channel': channel,
278 278 'message': {
279 279 'message': message,
280 280 'level': 'info',
281 281 'topic': '/notifications'
282 282 }
283 283 }
284 284 channelstream_request(channelstream_config, [payload],
285 285 '/message', raise_exc=False)
286 286
287 287 return comment
288 288
289 289 def delete(self, comment):
290 290 """
291 291 Deletes given comment
292 292
293 293 :param comment_id:
294 294 """
295 295 comment = self.__get_commit_comment(comment)
296 296 Session().delete(comment)
297 297
298 298 return comment
299 299
300 300 def get_all_comments(self, repo_id, revision=None, pull_request=None):
301 301 q = ChangesetComment.query()\
302 302 .filter(ChangesetComment.repo_id == repo_id)
303 303 if revision:
304 304 q = q.filter(ChangesetComment.revision == revision)
305 305 elif pull_request:
306 306 pull_request = self.__get_pull_request(pull_request)
307 307 q = q.filter(ChangesetComment.pull_request == pull_request)
308 308 else:
309 309 raise Exception('Please specify commit or pull_request')
310 310 q = q.order_by(ChangesetComment.created_on)
311 311 return q.all()
312 312
313 313 def get_url(self, comment):
314 314 comment = self.__get_commit_comment(comment)
315 315 if comment.pull_request:
316 316 return h.url(
317 317 'pullrequest_show',
318 318 repo_name=comment.pull_request.target_repo.repo_name,
319 319 pull_request_id=comment.pull_request.pull_request_id,
320 320 anchor='comment-%s' % comment.comment_id,
321 321 qualified=True,)
322 322 else:
323 323 return h.url(
324 324 'changeset_home',
325 325 repo_name=comment.repo.repo_name,
326 326 revision=comment.revision,
327 327 anchor='comment-%s' % comment.comment_id,
328 328 qualified=True,)
329 329
330 330 def get_comments(self, repo_id, revision=None, pull_request=None):
331 331 """
332 332 Gets main comments based on revision or pull_request_id
333 333
334 334 :param repo_id:
335 335 :param revision:
336 336 :param pull_request:
337 337 """
338 338
339 339 q = ChangesetComment.query()\
340 340 .filter(ChangesetComment.repo_id == repo_id)\
341 341 .filter(ChangesetComment.line_no == None)\
342 342 .filter(ChangesetComment.f_path == None)
343 343 if revision:
344 344 q = q.filter(ChangesetComment.revision == revision)
345 345 elif pull_request:
346 346 pull_request = self.__get_pull_request(pull_request)
347 347 q = q.filter(ChangesetComment.pull_request == pull_request)
348 348 else:
349 349 raise Exception('Please specify commit or pull_request')
350 350 q = q.order_by(ChangesetComment.created_on)
351 351 return q.all()
352 352
353 353 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
354 354 q = self._get_inline_comments_query(repo_id, revision, pull_request)
355 355 return self._group_comments_by_path_and_line_number(q)
356 356
357 357 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
358 version=None):
358 version=None, include_aggregates=False):
359 version_aggregates = collections.defaultdict(list)
359 360 inline_cnt = 0
360 361 for fname, per_line_comments in inline_comments.iteritems():
361 362 for lno, comments in per_line_comments.iteritems():
362 inline_cnt += len(
363 [comm for comm in comments
364 if (not comm.outdated_at_version(version) and skip_outdated)])
363 for comm in comments:
364 version_aggregates[comm.pull_request_version_id].append(comm)
365 if not comm.outdated_at_version(version) and skip_outdated:
366 inline_cnt += 1
367
368 if include_aggregates:
369 return inline_cnt, version_aggregates
365 370 return inline_cnt
366 371
367 372 def get_outdated_comments(self, repo_id, pull_request):
368 373 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
369 374 # of a pull request.
370 375 q = self._all_inline_comments_of_pull_request(pull_request)
371 376 q = q.filter(
372 377 ChangesetComment.display_state ==
373 378 ChangesetComment.COMMENT_OUTDATED
374 379 ).order_by(ChangesetComment.comment_id.asc())
375 380
376 381 return self._group_comments_by_path_and_line_number(q)
377 382
378 383 def _get_inline_comments_query(self, repo_id, revision, pull_request):
379 384 # TODO: johbo: Split this into two methods: One for PR and one for
380 385 # commit.
381 386 if revision:
382 387 q = Session().query(ChangesetComment).filter(
383 388 ChangesetComment.repo_id == repo_id,
384 389 ChangesetComment.line_no != null(),
385 390 ChangesetComment.f_path != null(),
386 391 ChangesetComment.revision == revision)
387 392
388 393 elif pull_request:
389 394 pull_request = self.__get_pull_request(pull_request)
390 395 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
391 396 q = self._visible_inline_comments_of_pull_request(pull_request)
392 397 else:
393 398 q = self._all_inline_comments_of_pull_request(pull_request)
394 399
395 400 else:
396 401 raise Exception('Please specify commit or pull_request_id')
397 402 q = q.order_by(ChangesetComment.comment_id.asc())
398 403 return q
399 404
400 405 def _group_comments_by_path_and_line_number(self, q):
401 406 comments = q.all()
402 407 paths = collections.defaultdict(lambda: collections.defaultdict(list))
403 408 for co in comments:
404 409 paths[co.f_path][co.line_no].append(co)
405 410 return paths
406 411
407 412 @classmethod
408 413 def needed_extra_diff_context(cls):
409 414 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
410 415
411 416 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
412 417 if not ChangesetCommentsModel.use_outdated_comments(pull_request):
413 418 return
414 419
415 420 comments = self._visible_inline_comments_of_pull_request(pull_request)
416 421 comments_to_outdate = comments.all()
417 422
418 423 for comment in comments_to_outdate:
419 424 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
420 425
421 426 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
422 427 diff_line = _parse_comment_line_number(comment.line_no)
423 428
424 429 try:
425 430 old_context = old_diff_proc.get_context_of_line(
426 431 path=comment.f_path, diff_line=diff_line)
427 432 new_context = new_diff_proc.get_context_of_line(
428 433 path=comment.f_path, diff_line=diff_line)
429 434 except (diffs.LineNotInDiffException,
430 435 diffs.FileNotInDiffException):
431 436 comment.display_state = ChangesetComment.COMMENT_OUTDATED
432 437 return
433 438
434 439 if old_context == new_context:
435 440 return
436 441
437 442 if self._should_relocate_diff_line(diff_line):
438 443 new_diff_lines = new_diff_proc.find_context(
439 444 path=comment.f_path, context=old_context,
440 445 offset=self.DIFF_CONTEXT_BEFORE)
441 446 if not new_diff_lines:
442 447 comment.display_state = ChangesetComment.COMMENT_OUTDATED
443 448 else:
444 449 new_diff_line = self._choose_closest_diff_line(
445 450 diff_line, new_diff_lines)
446 451 comment.line_no = _diff_to_comment_line_number(new_diff_line)
447 452 else:
448 453 comment.display_state = ChangesetComment.COMMENT_OUTDATED
449 454
450 455 def _should_relocate_diff_line(self, diff_line):
451 456 """
452 457 Checks if relocation shall be tried for the given `diff_line`.
453 458
454 459 If a comment points into the first lines, then we can have a situation
455 460 that after an update another line has been added on top. In this case
456 461 we would find the context still and move the comment around. This
457 462 would be wrong.
458 463 """
459 464 should_relocate = (
460 465 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
461 466 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
462 467 return should_relocate
463 468
464 469 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
465 470 candidate = new_diff_lines[0]
466 471 best_delta = _diff_line_delta(diff_line, candidate)
467 472 for new_diff_line in new_diff_lines[1:]:
468 473 delta = _diff_line_delta(diff_line, new_diff_line)
469 474 if delta < best_delta:
470 475 candidate = new_diff_line
471 476 best_delta = delta
472 477 return candidate
473 478
474 479 def _visible_inline_comments_of_pull_request(self, pull_request):
475 480 comments = self._all_inline_comments_of_pull_request(pull_request)
476 481 comments = comments.filter(
477 482 coalesce(ChangesetComment.display_state, '') !=
478 483 ChangesetComment.COMMENT_OUTDATED)
479 484 return comments
480 485
481 486 def _all_inline_comments_of_pull_request(self, pull_request):
482 487 comments = Session().query(ChangesetComment)\
483 488 .filter(ChangesetComment.line_no != None)\
484 489 .filter(ChangesetComment.f_path != None)\
485 490 .filter(ChangesetComment.pull_request == pull_request)
486 491 return comments
487 492
488 493 @staticmethod
489 494 def use_outdated_comments(pull_request):
490 495 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
491 496 settings = settings_model.get_general_settings()
492 497 return settings.get('rhodecode_use_outdated_comments', False)
493 498
494 499
495 500 def _parse_comment_line_number(line_no):
496 501 """
497 502 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
498 503 """
499 504 old_line = None
500 505 new_line = None
501 506 if line_no.startswith('o'):
502 507 old_line = int(line_no[1:])
503 508 elif line_no.startswith('n'):
504 509 new_line = int(line_no[1:])
505 510 else:
506 511 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
507 512 return diffs.DiffLineNumber(old_line, new_line)
508 513
509 514
510 515 def _diff_to_comment_line_number(diff_line):
511 516 if diff_line.new is not None:
512 517 return u'n{}'.format(diff_line.new)
513 518 elif diff_line.old is not None:
514 519 return u'o{}'.format(diff_line.old)
515 520 return u''
516 521
517 522
518 523 def _diff_line_delta(a, b):
519 524 if None not in (a.new, b.new):
520 525 return abs(a.new - b.new)
521 526 elif None not in (a.old, b.old):
522 527 return abs(a.old - b.old)
523 528 else:
524 529 raise ValueError(
525 530 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1187 +1,1199 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px !important;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color !important;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 overflow-y: hidden;
395 395 padding: 0;
396 396 }
397 397 div.diffblock {
398 398 border: none;
399 399 }
400 400
401 401 &.inline-form {
402 402 width: 99%
403 403 }
404 404 }
405 405
406 406
407 407 table.code-difftable {
408 408 width: 100%;
409 409 }
410 410
411 411 /** PYGMENTS COLORING **/
412 412 div.codeblock {
413 413
414 414 // TODO: johbo: Added interim to get rid of the margin around
415 415 // Select2 widgets. This needs further cleanup.
416 416 margin-top: @padding;
417 417
418 418 overflow: auto;
419 419 padding: 0px;
420 420 border: @border-thickness solid @grey5;
421 421 background: @grey6;
422 422 .border-radius(@border-radius);
423 423
424 424 #remove_gist {
425 425 float: right;
426 426 }
427 427
428 428 .author {
429 429 clear: both;
430 430 vertical-align: middle;
431 431 font-family: @text-bold;
432 432 }
433 433
434 434 .btn-mini {
435 435 float: left;
436 436 margin: 0 5px 0 0;
437 437 }
438 438
439 439 .code-header {
440 440 padding: @padding;
441 441 border-bottom: @border-thickness solid @grey5;
442 442
443 443 .rc-user {
444 444 min-width: 0;
445 445 margin-right: .5em;
446 446 }
447 447
448 448 .stats {
449 449 clear: both;
450 450 margin: 0 0 @padding 0;
451 451 padding: 0;
452 452 .left {
453 453 float: left;
454 454 clear: left;
455 455 max-width: 75%;
456 456 margin: 0 0 @padding 0;
457 457
458 458 &.item {
459 459 margin-right: @padding;
460 460 &.last { border-right: none; }
461 461 }
462 462 }
463 463 .buttons { float: right; }
464 464 .author {
465 465 height: 25px; margin-left: 15px; font-weight: bold;
466 466 }
467 467 }
468 468
469 469 .commit {
470 470 margin: 5px 0 0 26px;
471 471 font-weight: normal;
472 472 white-space: pre-wrap;
473 473 }
474 474 }
475 475
476 476 .message {
477 477 position: relative;
478 478 margin: @padding;
479 479
480 480 .codeblock-label {
481 481 margin: 0 0 1em 0;
482 482 }
483 483 }
484 484
485 485 .code-body {
486 486 padding: @padding;
487 487 background-color: #ffffff;
488 488 min-width: 100%;
489 489 box-sizing: border-box;
490 490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 491 // to have the intended size and to scroll. Should be simplified.
492 492 width: 100%;
493 493 overflow-x: auto;
494 494 }
495 495 }
496 496
497 497 .code-highlighttable,
498 498 div.codeblock {
499 499
500 500 &.readme {
501 501 background-color: white;
502 502 }
503 503
504 504 .markdown-block table {
505 505 border-collapse: collapse;
506 506
507 507 th,
508 508 td {
509 509 padding: .5em;
510 510 border: @border-thickness solid @border-default-color;
511 511 }
512 512 }
513 513
514 514 table {
515 515 border: 0px;
516 516 margin: 0;
517 517 letter-spacing: normal;
518 518
519 519
520 520 td {
521 521 border: 0px;
522 522 vertical-align: top;
523 523 }
524 524 }
525 525 }
526 526
527 527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 528 div.search-code-body {
529 529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 530 pre {
531 531 .match { background-color: #faffa6;}
532 532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 533 }
534 534 .code-highlighttable {
535 535 border-collapse: collapse;
536 536
537 537 tr:hover {
538 538 background: #fafafa;
539 539 }
540 540 td.code {
541 541 padding-left: 10px;
542 542 }
543 543 td.line {
544 544 border-right: 1px solid #ccc !important;
545 545 padding-right: 10px;
546 546 text-align: right;
547 547 font-family: "Lucida Console",Monaco,monospace;
548 548 span {
549 549 white-space: pre-wrap;
550 550 color: #666666;
551 551 }
552 552 }
553 553 }
554 554 }
555 555
556 556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 557 .code-highlight {
558 558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 560 pre div:target {background-color: @comment-highlight-color !important;}
561 561 }
562 562
563 563 .linenos a { text-decoration: none; }
564 564
565 565 .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 567 .CodeMirror ::selection { background: @rchighlightblue; }
568 568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569 569
570 570 .code { display: block; border:0px !important; }
571 571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 572 .codehilite {
573 573 .hll { background-color: #ffffcc }
574 574 .c { color: #408080; font-style: italic } /* Comment */
575 575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 576 .k { color: #008000; font-weight: bold } /* Keyword */
577 577 .o { color: #666666 } /* Operator */
578 578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 582 .gd { color: #A00000 } /* Generic.Deleted */
583 583 .ge { font-style: italic } /* Generic.Emph */
584 584 .gr { color: #FF0000 } /* Generic.Error */
585 585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 586 .gi { color: #00A000 } /* Generic.Inserted */
587 587 .go { color: #808080 } /* Generic.Output */
588 588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 589 .gs { font-weight: bold } /* Generic.Strong */
590 590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 591 .gt { color: #0040D0 } /* Generic.Traceback */
592 592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 595 .kp { color: #008000 } /* Keyword.Pseudo */
596 596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 597 .kt { color: #B00040 } /* Keyword.Type */
598 598 .m { color: #666666 } /* Literal.Number */
599 599 .s { color: #BA2121 } /* Literal.String */
600 600 .na { color: #7D9029 } /* Name.Attribute */
601 601 .nb { color: #008000 } /* Name.Builtin */
602 602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 603 .no { color: #880000 } /* Name.Constant */
604 604 .nd { color: #AA22FF } /* Name.Decorator */
605 605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 607 .nf { color: #0000FF } /* Name.Function */
608 608 .nl { color: #A0A000 } /* Name.Label */
609 609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 611 .nv { color: #19177C } /* Name.Variable */
612 612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 613 .w { color: #bbbbbb } /* Text.Whitespace */
614 614 .mf { color: #666666 } /* Literal.Number.Float */
615 615 .mh { color: #666666 } /* Literal.Number.Hex */
616 616 .mi { color: #666666 } /* Literal.Number.Integer */
617 617 .mo { color: #666666 } /* Literal.Number.Oct */
618 618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 619 .sc { color: #BA2121 } /* Literal.String.Char */
620 620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 625 .sx { color: #008000 } /* Literal.String.Other */
626 626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 628 .ss { color: #19177C } /* Literal.String.Symbol */
629 629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 630 .vc { color: #19177C } /* Name.Variable.Class */
631 631 .vg { color: #19177C } /* Name.Variable.Global */
632 632 .vi { color: #19177C } /* Name.Variable.Instance */
633 633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 634 }
635 635
636 636 /* customized pre blocks for markdown/rst */
637 637 pre.literal-block, .codehilite pre{
638 638 padding: @padding;
639 639 border: 1px solid @grey6;
640 640 .border-radius(@border-radius);
641 641 background-color: @grey7;
642 642 }
643 643
644 644
645 645 /* START NEW CODE BLOCK CSS */
646 646
647 647 @cb-line-height: 18px;
648 648 @cb-line-code-padding: 10px;
649 649 @cb-text-padding: 5px;
650 650
651 651 @pill-padding: 2px 7px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 border-width: 9px 0 9px 15.6px;
661 width: 0;
662 height: 0;
663 border-style: solid;
664 border-width: 6.5px 0 6.5px 11.3px;
662 665 border-color: transparent transparent transparent #ccc;
663 666 }
664 667 .filediff-menu {
665 668 display: none;
666 669 }
667 margin: -1px 0 0 0;
670 margin: 10px 0 0 0;
668 671 }
669 672
670 673 &+ .filediff { /* file diff is expanded */
671 674 .filediff-collapse-indicator {
672 border-width: 15.6px 9px 0 9px;
675 width: 0;
676 height: 0;
677 border-style: solid;
678 border-width: 11.3px 6.5px 0 6.5px;
673 679 border-color: #ccc transparent transparent transparent;
674 680 }
675 681 .filediff-menu {
676 682 display: block;
677 683 }
678 margin: 20px 0;
684 margin: 10px 0;
679 685 &:nth-child(2) {
680 686 margin: 0;
681 687 }
682 688 }
683 689 }
684 690 .cs_files {
685 691 clear: both;
686 692 }
687 693
688 694 .diffset-menu {
689 695 margin-bottom: 20px;
690 696 }
691 697 .diffset {
692 698 margin: 20px auto;
693 699 .diffset-heading {
694 700 border: 1px solid @grey5;
695 701 margin-bottom: -1px;
696 702 // margin-top: 20px;
697 703 h2 {
698 704 margin: 0;
699 705 line-height: 38px;
700 706 padding-left: 10px;
701 707 }
702 708 .btn {
703 709 margin: 0;
704 710 }
705 711 background: @grey6;
706 712 display: block;
707 713 padding: 5px;
708 714 }
709 715 .diffset-heading-warning {
710 716 background: @alert3-inner;
711 717 border: 1px solid @alert3;
712 718 }
713 719 &.diffset-comments-disabled {
714 720 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
715 721 display: none !important;
716 722 }
717 723 }
718 724 }
719 725
720 726 .pill {
721 727 display: block;
722 728 float: left;
723 729 padding: @pill-padding;
724 730 }
725 731 .pill-group {
726 732 .pill {
727 733 opacity: .8;
728 734 &:first-child {
729 735 border-radius: @border-radius 0 0 @border-radius;
730 736 }
731 737 &:last-child {
732 738 border-radius: 0 @border-radius @border-radius 0;
733 739 }
734 740 &:only-child {
735 741 border-radius: @border-radius;
736 742 }
737 743 }
738 744 }
739 745
740 746 /* Main comments*/
741 747 #comments {
742 748 .comment-selected {
743 749 border-left: 6px solid @comment-highlight-color;
744 750 padding-left: 3px;
745 751 margin-left: -9px;
746 752 }
747 753 }
748 754
749 755 .filediff {
750 756 border: 1px solid @grey5;
751 757
752 758 /* START OVERRIDES */
753 759 .code-highlight {
754 760 border: none; // TODO: remove this border from the global
755 761 // .code-highlight, it doesn't belong there
756 762 }
757 763 label {
758 764 margin: 0; // TODO: remove this margin definition from global label
759 765 // it doesn't belong there - if margin on labels
760 766 // are needed for a form they should be defined
761 767 // in the form's class
762 768 }
763 769 /* END OVERRIDES */
764 770
765 771 * {
766 772 box-sizing: border-box;
767 773 }
768 774 .filediff-anchor {
769 775 visibility: hidden;
770 776 }
771 777 &:hover {
772 778 .filediff-anchor {
773 779 visibility: visible;
774 780 }
775 781 }
776 782
777 783 .filediff-collapse-indicator {
778 width: 0;
779 height: 0;
780 784 border-style: solid;
781 785 float: left;
782 margin: 2px 2px 0 0;
786 margin: 4px 0px 0 0;
783 787 cursor: pointer;
784 788 }
785 789
786 790 .filediff-heading {
787 791 background: @grey7;
788 792 cursor: pointer;
789 793 display: block;
790 794 padding: 5px 10px;
791 795 }
792 796 .filediff-heading:after {
793 797 content: "";
794 798 display: table;
795 799 clear: both;
796 800 }
797 801 .filediff-heading:hover {
798 802 background: #e1e9f4 !important;
799 803 }
800 804
801 805 .filediff-menu {
802 806 float: right;
807 text-align: right;
808 padding: 5px 5px 5px 0px;
803 809
804 &> a, &> span {
805 padding: 5px;
806 display: block;
807 float: left
810 &> a,
811 &> span {
812 padding: 1px;
808 813 }
809 814 }
810 815
811 816 .pill {
812 817 &[op="name"] {
813 818 background: none;
814 819 color: @grey2;
815 820 opacity: 1;
816 821 color: white;
817 822 }
818 823 &[op="limited"] {
819 824 background: @grey2;
820 825 color: white;
821 826 }
822 827 &[op="binary"] {
823 828 background: @color7;
824 829 color: white;
825 830 }
826 831 &[op="modified"] {
827 832 background: @alert1;
828 833 color: white;
829 834 }
830 835 &[op="renamed"] {
831 836 background: @color4;
832 837 color: white;
833 838 }
834 839 &[op="mode"] {
835 840 background: @grey3;
836 841 color: white;
837 842 }
838 843 &[op="symlink"] {
839 844 background: @color8;
840 845 color: white;
841 846 }
842 847
843 848 &[op="added"] { /* added lines */
844 849 background: @alert1;
845 850 color: white;
846 851 }
847 852 &[op="deleted"] { /* deleted lines */
848 853 background: @alert2;
849 854 color: white;
850 855 }
851 856
852 857 &[op="created"] { /* created file */
853 858 background: @alert1;
854 859 color: white;
855 860 }
856 861 &[op="removed"] { /* deleted file */
857 862 background: @color5;
858 863 color: white;
859 864 }
860 865 }
861 866
862 867 .filediff-collapse-button, .filediff-expand-button {
863 868 cursor: pointer;
864 869 }
865 870 .filediff-collapse-button {
866 871 display: inline;
867 872 }
868 873 .filediff-expand-button {
869 874 display: none;
870 875 }
871 876 .filediff-collapsed .filediff-collapse-button {
872 877 display: none;
873 878 }
874 879 .filediff-collapsed .filediff-expand-button {
875 880 display: inline;
876 881 }
877 882
878 883 @comment-padding: 5px;
879 884
880 885 /**** COMMENTS ****/
881 886
882 887 .filediff-menu {
883 888 .show-comment-button {
884 889 display: none;
885 890 }
886 891 }
887 892 &.hide-comments {
888 893 .inline-comments {
889 894 display: none;
890 895 }
891 896 .filediff-menu {
892 897 .show-comment-button {
893 898 display: inline;
894 899 }
895 900 .hide-comment-button {
896 901 display: none;
897 902 }
898 903 }
899 904 }
900 905
901 906 .hide-line-comments {
902 907 .inline-comments {
903 908 display: none;
904 909 }
905 910 }
906 911
907 912 .inline-comments {
908 913 border-radius: @border-radius;
909 914 background: @grey6;
910 915 .comment {
911 916 margin: 0;
912 917 border-radius: @border-radius;
913 918 }
914 919 .comment-outdated {
915 920 opacity: 0.5;
916 921 }
917 922
918 923 .comment-inline {
919 924 background: white;
920 925 padding: (@comment-padding + 3px) @comment-padding;
921 926 border: @comment-padding solid @grey6;
922 927
923 928 .text {
924 929 border: none;
925 930 }
926 931 .meta {
927 932 border-bottom: 1px solid @grey6;
928 933 padding-bottom: 10px;
929 934 }
930 935 }
931 936 .comment-selected {
932 937 border-left: 6px solid @comment-highlight-color;
933 938 }
934 939 .comment-inline-form {
935 940 padding: @comment-padding;
936 941 display: none;
937 942 }
938 943 .cb-comment-add-button {
939 944 margin: @comment-padding;
940 945 }
941 946 /* hide add comment button when form is open */
942 947 .comment-inline-form-open ~ .cb-comment-add-button {
943 948 display: none;
944 949 }
945 950 .comment-inline-form-open {
946 951 display: block;
947 952 }
948 953 /* hide add comment button when form but no comments */
949 954 .comment-inline-form:first-child + .cb-comment-add-button {
950 955 display: none;
951 956 }
952 957 /* hide add comment button when no comments or form */
953 958 .cb-comment-add-button:first-child {
954 959 display: none;
955 960 }
956 961 /* hide add comment button when only comment is being deleted */
957 962 .comment-deleting:first-child + .cb-comment-add-button {
958 963 display: none;
959 964 }
960 965 }
961 966 /**** END COMMENTS ****/
962 967
963 968 }
964 969
970 .filediff-outdated {
971 padding: 8px 0;
972
973 .filediff-heading {
974 opacity: .5;
975 }
976 }
965 977
966 978 table.cb {
967 979 width: 100%;
968 980 border-collapse: collapse;
969 981
970 982 .cb-text {
971 983 padding: @cb-text-padding;
972 984 }
973 985 .cb-hunk {
974 986 padding: @cb-text-padding;
975 987 }
976 988 .cb-expand {
977 989 display: none;
978 990 }
979 991 .cb-collapse {
980 992 display: inline;
981 993 }
982 994 &.cb-collapsed {
983 995 .cb-line {
984 996 display: none;
985 997 }
986 998 .cb-expand {
987 999 display: inline;
988 1000 }
989 1001 .cb-collapse {
990 1002 display: none;
991 1003 }
992 1004 }
993 1005
994 1006 /* intentionally general selector since .cb-line-selected must override it
995 1007 and they both use !important since the td itself may have a random color
996 1008 generated by annotation blocks. TLDR: if you change it, make sure
997 1009 annotated block selection and line selection in file view still work */
998 1010 .cb-line-fresh .cb-content {
999 1011 background: white !important;
1000 1012 }
1001 1013 .cb-warning {
1002 1014 background: #fff4dd;
1003 1015 }
1004 1016
1005 1017 &.cb-diff-sideside {
1006 1018 td {
1007 1019 &.cb-content {
1008 1020 width: 50%;
1009 1021 }
1010 1022 }
1011 1023 }
1012 1024
1013 1025 tr {
1014 1026 &.cb-annotate {
1015 1027 border-top: 1px solid #eee;
1016 1028
1017 1029 &+ .cb-line {
1018 1030 border-top: 1px solid #eee;
1019 1031 }
1020 1032
1021 1033 &:first-child {
1022 1034 border-top: none;
1023 1035 &+ .cb-line {
1024 1036 border-top: none;
1025 1037 }
1026 1038 }
1027 1039 }
1028 1040
1029 1041 &.cb-hunk {
1030 1042 font-family: @font-family-monospace;
1031 1043 color: rgba(0, 0, 0, 0.3);
1032 1044
1033 1045 td {
1034 1046 &:first-child {
1035 1047 background: #edf2f9;
1036 1048 }
1037 1049 &:last-child {
1038 1050 background: #f4f7fb;
1039 1051 }
1040 1052 }
1041 1053 }
1042 1054 }
1043 1055
1044 1056
1045 1057 td {
1046 1058 vertical-align: top;
1047 1059 padding: 0;
1048 1060
1049 1061 &.cb-content {
1050 1062 font-size: 12.35px;
1051 1063
1052 1064 &.cb-line-selected .cb-code {
1053 1065 background: @comment-highlight-color !important;
1054 1066 }
1055 1067
1056 1068 span.cb-code {
1057 1069 line-height: @cb-line-height;
1058 1070 padding-left: @cb-line-code-padding;
1059 1071 padding-right: @cb-line-code-padding;
1060 1072 display: block;
1061 1073 white-space: pre-wrap;
1062 1074 font-family: @font-family-monospace;
1063 1075 word-break: break-word;
1064 1076 .nonl {
1065 1077 color: @color5;
1066 1078 }
1067 1079 }
1068 1080
1069 1081 &> button.cb-comment-box-opener {
1070 1082
1071 padding: 2px 5px 1px 5px;
1072 margin-left: 0px;
1083 padding: 2px 2px 1px 3px;
1084 margin-left: -6px;
1073 1085 margin-top: -1px;
1074 1086
1075 1087 border-radius: @border-radius;
1076 1088 position: absolute;
1077 1089 display: none;
1078 1090 }
1079 1091 .cb-comment {
1080 1092 margin-top: 10px;
1081 1093 white-space: normal;
1082 1094 }
1083 1095 }
1084 1096 &:hover {
1085 1097 button.cb-comment-box-opener {
1086 1098 display: block;
1087 1099 }
1088 1100 &+ td button.cb-comment-box-opener {
1089 1101 display: block
1090 1102 }
1091 1103 }
1092 1104
1093 1105 &.cb-data {
1094 1106 text-align: right;
1095 1107 width: 30px;
1096 1108 font-family: @font-family-monospace;
1097 1109
1098 1110 .icon-comment {
1099 1111 cursor: pointer;
1100 1112 }
1101 1113 &.cb-line-selected > div {
1102 1114 display: block;
1103 1115 background: @comment-highlight-color !important;
1104 1116 line-height: @cb-line-height;
1105 1117 color: rgba(0, 0, 0, 0.3);
1106 1118 }
1107 1119 }
1108 1120
1109 1121 &.cb-lineno {
1110 1122 padding: 0;
1111 1123 width: 50px;
1112 1124 color: rgba(0, 0, 0, 0.3);
1113 1125 text-align: right;
1114 1126 border-right: 1px solid #eee;
1115 1127 font-family: @font-family-monospace;
1116 1128
1117 1129 a::before {
1118 1130 content: attr(data-line-no);
1119 1131 }
1120 1132 &.cb-line-selected a {
1121 1133 background: @comment-highlight-color !important;
1122 1134 }
1123 1135
1124 1136 a {
1125 1137 display: block;
1126 1138 padding-right: @cb-line-code-padding;
1127 1139 padding-left: @cb-line-code-padding;
1128 1140 line-height: @cb-line-height;
1129 1141 color: rgba(0, 0, 0, 0.3);
1130 1142 }
1131 1143 }
1132 1144
1133 1145 &.cb-empty {
1134 1146 background: @grey7;
1135 1147 }
1136 1148
1137 1149 ins {
1138 1150 color: black;
1139 1151 background: #a6f3a6;
1140 1152 text-decoration: none;
1141 1153 }
1142 1154 del {
1143 1155 color: black;
1144 1156 background: #f8cbcb;
1145 1157 text-decoration: none;
1146 1158 }
1147 1159 &.cb-addition {
1148 1160 background: #ecffec;
1149 1161
1150 1162 &.blob-lineno {
1151 1163 background: #ddffdd;
1152 1164 }
1153 1165 }
1154 1166 &.cb-deletion {
1155 1167 background: #ffecec;
1156 1168
1157 1169 &.blob-lineno {
1158 1170 background: #ffdddd;
1159 1171 }
1160 1172 }
1161 1173
1162 1174 &.cb-annotate-info {
1163 1175 width: 320px;
1164 1176 min-width: 320px;
1165 1177 max-width: 320px;
1166 1178 padding: 5px 2px;
1167 1179 font-size: 13px;
1168 1180
1169 1181 strong.cb-annotate-message {
1170 1182 padding: 5px 0;
1171 1183 white-space: pre-line;
1172 1184 display: inline-block;
1173 1185 }
1174 1186 .rc-user {
1175 1187 float: none;
1176 1188 padding: 0 6px 0 17px;
1177 1189 min-width: auto;
1178 1190 min-height: auto;
1179 1191 }
1180 1192 }
1181 1193
1182 1194 &.cb-annotate-revision {
1183 1195 cursor: pointer;
1184 1196 text-align: right;
1185 1197 }
1186 1198 }
1187 1199 }
@@ -1,2217 +1,2223 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wide-mode-wrapper {
113 max-width:2400px !important;
113 max-width:4000px !important;
114 114 }
115 115
116 116 .wrapper {
117 117 position: relative;
118 118 max-width: @wrapper-maxwidth;
119 119 margin: 0 auto;
120 120 }
121 121
122 122 #content {
123 123 clear: both;
124 124 padding: 0 @contentpadding;
125 125 }
126 126
127 127 .advanced-settings-fields{
128 128 input{
129 129 margin-left: @textmargin;
130 130 margin-right: @padding/2;
131 131 }
132 132 }
133 133
134 134 .cs_files_title {
135 135 margin: @pagepadding 0 0;
136 136 }
137 137
138 138 input.inline[type="file"] {
139 139 display: inline;
140 140 }
141 141
142 142 .error_page {
143 143 margin: 10% auto;
144 144
145 145 h1 {
146 146 color: @grey2;
147 147 }
148 148
149 149 .alert {
150 150 margin: @padding 0;
151 151 }
152 152
153 153 .error-branding {
154 154 font-family: @text-semibold;
155 155 color: @grey4;
156 156 }
157 157
158 158 .error_message {
159 159 font-family: @text-regular;
160 160 }
161 161
162 162 .sidebar {
163 163 min-height: 275px;
164 164 margin: 0;
165 165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 166 border: none;
167 167 }
168 168
169 169 .main-content {
170 170 position: relative;
171 171 margin: 0 @sidebarpadding @sidebarpadding;
172 172 padding: 0 0 0 @sidebarpadding;
173 173 border-left: @border-thickness solid @grey5;
174 174
175 175 @media (max-width:767px) {
176 176 clear: both;
177 177 width: 100%;
178 178 margin: 0;
179 179 border: none;
180 180 }
181 181 }
182 182
183 183 .inner-column {
184 184 float: left;
185 185 width: 29.75%;
186 186 min-height: 150px;
187 187 margin: @sidebarpadding 2% 0 0;
188 188 padding: 0 2% 0 0;
189 189 border-right: @border-thickness solid @grey5;
190 190
191 191 @media (max-width:767px) {
192 192 clear: both;
193 193 width: 100%;
194 194 border: none;
195 195 }
196 196
197 197 ul {
198 198 padding-left: 1.25em;
199 199 }
200 200
201 201 &:last-child {
202 202 margin: @sidebarpadding 0 0;
203 203 border: none;
204 204 }
205 205
206 206 h4 {
207 207 margin: 0 0 @padding;
208 208 font-family: @text-semibold;
209 209 }
210 210 }
211 211 }
212 212 .error-page-logo {
213 213 width: 130px;
214 214 height: 160px;
215 215 }
216 216
217 217 // HEADER
218 218 .header {
219 219
220 220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 221 // for the header and then remove the min-height. I chose a smaller value
222 222 // intentionally here to avoid rendering issues in the main navigation.
223 223 min-height: 49px;
224 224
225 225 position: relative;
226 226 vertical-align: bottom;
227 227 padding: 0 @header-padding;
228 228 background-color: @grey2;
229 229 color: @grey5;
230 230
231 231 .title {
232 232 overflow: visible;
233 233 }
234 234
235 235 &:before,
236 236 &:after {
237 237 content: "";
238 238 clear: both;
239 239 width: 100%;
240 240 }
241 241
242 242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 243 .select2-container .select2-choice .select2-arrow {
244 244 display: none;
245 245 }
246 246 }
247 247
248 248 #header-inner {
249 249 &.title {
250 250 margin: 0;
251 251 }
252 252 &:before,
253 253 &:after {
254 254 content: "";
255 255 clear: both;
256 256 }
257 257 }
258 258
259 259 // Gists
260 260 #files_data {
261 261 clear: both; //for firefox
262 262 }
263 263 #gistid {
264 264 margin-right: @padding;
265 265 }
266 266
267 267 // Global Settings Editor
268 268 .textarea.editor {
269 269 float: left;
270 270 position: relative;
271 271 max-width: @texteditor-width;
272 272
273 273 select {
274 274 position: absolute;
275 275 top:10px;
276 276 right:0;
277 277 }
278 278
279 279 .CodeMirror {
280 280 margin: 0;
281 281 }
282 282
283 283 .help-block {
284 284 margin: 0 0 @padding;
285 285 padding:.5em;
286 286 background-color: @grey6;
287 287 }
288 288 }
289 289
290 290 ul.auth_plugins {
291 291 margin: @padding 0 @padding @legend-width;
292 292 padding: 0;
293 293
294 294 li {
295 295 margin-bottom: @padding;
296 296 line-height: 1em;
297 297 list-style-type: none;
298 298
299 299 .auth_buttons .btn {
300 300 margin-right: @padding;
301 301 }
302 302
303 303 &:before { content: none; }
304 304 }
305 305 }
306 306
307 307
308 308 // My Account PR list
309 309
310 310 #show_closed {
311 311 margin: 0 1em 0 0;
312 312 }
313 313
314 314 .pullrequestlist {
315 315 .closed {
316 316 background-color: @grey6;
317 317 }
318 318 .td-status {
319 319 padding-left: .5em;
320 320 }
321 321 .log-container .truncate {
322 322 height: 2.75em;
323 323 white-space: pre-line;
324 324 }
325 325 table.rctable .user {
326 326 padding-left: 0;
327 327 }
328 328 table.rctable {
329 329 td.td-description,
330 330 .rc-user {
331 331 min-width: auto;
332 332 }
333 333 }
334 334 }
335 335
336 336 // Pull Requests
337 337
338 338 .pullrequests_section_head {
339 339 display: block;
340 340 clear: both;
341 341 margin: @padding 0;
342 342 font-family: @text-bold;
343 343 }
344 344
345 345 .pr-origininfo, .pr-targetinfo {
346 346 position: relative;
347 347
348 348 .tag {
349 349 display: inline-block;
350 350 margin: 0 1em .5em 0;
351 351 }
352 352
353 353 .clone-url {
354 354 display: inline-block;
355 355 margin: 0 0 .5em 0;
356 356 padding: 0;
357 357 line-height: 1.2em;
358 358 }
359 359 }
360 360
361 361 .pr-pullinfo {
362 362 clear: both;
363 363 margin: .5em 0;
364 364 }
365 365
366 366 #pr-title-input {
367 367 width: 72%;
368 368 font-size: 1em;
369 369 font-family: @text-bold;
370 370 margin: 0;
371 371 padding: 0 0 0 @padding/4;
372 372 line-height: 1.7em;
373 373 color: @text-color;
374 374 letter-spacing: .02em;
375 375 }
376 376
377 377 #pullrequest_title {
378 378 width: 100%;
379 379 box-sizing: border-box;
380 380 }
381 381
382 382 #pr_open_message {
383 383 border: @border-thickness solid #fff;
384 384 border-radius: @border-radius;
385 385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 386 text-align: right;
387 387 overflow: hidden;
388 388 }
389 389
390 390 .pr-submit-button {
391 391 float: right;
392 392 margin: 0 0 0 5px;
393 393 }
394 394
395 395 .pr-spacing-container {
396 396 padding: 20px;
397 397 clear: both
398 398 }
399 399
400 400 #pr-description-input {
401 401 margin-bottom: 0;
402 402 }
403 403
404 404 .pr-description-label {
405 405 vertical-align: top;
406 406 }
407 407
408 408 .perms_section_head {
409 409 min-width: 625px;
410 410
411 411 h2 {
412 412 margin-bottom: 0;
413 413 }
414 414
415 415 .label-checkbox {
416 416 float: left;
417 417 }
418 418
419 419 &.field {
420 420 margin: @space 0 @padding;
421 421 }
422 422
423 423 &:first-child.field {
424 424 margin-top: 0;
425 425
426 426 .label {
427 427 margin-top: 0;
428 428 padding-top: 0;
429 429 }
430 430
431 431 .radios {
432 432 padding-top: 0;
433 433 }
434 434 }
435 435
436 436 .radios {
437 437 float: right;
438 438 position: relative;
439 439 width: 405px;
440 440 }
441 441 }
442 442
443 443 //--- MODULES ------------------//
444 444
445 445
446 446 // Server Announcement
447 447 #server-announcement {
448 448 width: 95%;
449 449 margin: @padding auto;
450 450 padding: @padding;
451 451 border-width: 2px;
452 452 border-style: solid;
453 453 .border-radius(2px);
454 454 font-family: @text-bold;
455 455
456 456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 461 }
462 462
463 463 // Fixed Sidebar Column
464 464 .sidebar-col-wrapper {
465 465 padding-left: @sidebar-all-width;
466 466
467 467 .sidebar {
468 468 width: @sidebar-width;
469 469 margin-left: -@sidebar-all-width;
470 470 }
471 471 }
472 472
473 473 .sidebar-col-wrapper.scw-small {
474 474 padding-left: @sidebar-small-all-width;
475 475
476 476 .sidebar {
477 477 width: @sidebar-small-width;
478 478 margin-left: -@sidebar-small-all-width;
479 479 }
480 480 }
481 481
482 482
483 483 // FOOTER
484 484 #footer {
485 485 padding: 0;
486 486 text-align: center;
487 487 vertical-align: middle;
488 488 color: @grey2;
489 489 background-color: @grey6;
490 490
491 491 p {
492 492 margin: 0;
493 493 padding: 1em;
494 494 line-height: 1em;
495 495 }
496 496
497 497 .server-instance { //server instance
498 498 display: none;
499 499 }
500 500
501 501 .title {
502 502 float: none;
503 503 margin: 0 auto;
504 504 }
505 505 }
506 506
507 507 button.close {
508 508 padding: 0;
509 509 cursor: pointer;
510 510 background: transparent;
511 511 border: 0;
512 512 .box-shadow(none);
513 513 -webkit-appearance: none;
514 514 }
515 515
516 516 .close {
517 517 float: right;
518 518 font-size: 21px;
519 519 font-family: @text-bootstrap;
520 520 line-height: 1em;
521 521 font-weight: bold;
522 522 color: @grey2;
523 523
524 524 &:hover,
525 525 &:focus {
526 526 color: @grey1;
527 527 text-decoration: none;
528 528 cursor: pointer;
529 529 }
530 530 }
531 531
532 532 // GRID
533 533 .sorting,
534 534 .sorting_desc,
535 535 .sorting_asc {
536 536 cursor: pointer;
537 537 }
538 538 .sorting_desc:after {
539 539 content: "\00A0\25B2";
540 540 font-size: .75em;
541 541 }
542 542 .sorting_asc:after {
543 543 content: "\00A0\25BC";
544 544 font-size: .68em;
545 545 }
546 546
547 547
548 548 .user_auth_tokens {
549 549
550 550 &.truncate {
551 551 white-space: nowrap;
552 552 overflow: hidden;
553 553 text-overflow: ellipsis;
554 554 }
555 555
556 556 .fields .field .input {
557 557 margin: 0;
558 558 }
559 559
560 560 input#description {
561 561 width: 100px;
562 562 margin: 0;
563 563 }
564 564
565 565 .drop-menu {
566 566 // TODO: johbo: Remove this, should work out of the box when
567 567 // having multiple inputs inline
568 568 margin: 0 0 0 5px;
569 569 }
570 570 }
571 571 #user_list_table {
572 572 .closed {
573 573 background-color: @grey6;
574 574 }
575 575 }
576 576
577 577
578 578 input {
579 579 &.disabled {
580 580 opacity: .5;
581 581 }
582 582 }
583 583
584 584 // remove extra padding in firefox
585 585 input::-moz-focus-inner { border:0; padding:0 }
586 586
587 587 .adjacent input {
588 588 margin-bottom: @padding;
589 589 }
590 590
591 591 .permissions_boxes {
592 592 display: block;
593 593 }
594 594
595 595 //TODO: lisa: this should be in tables
596 596 .show_more_col {
597 597 width: 20px;
598 598 }
599 599
600 600 //FORMS
601 601
602 602 .medium-inline,
603 603 input#description.medium-inline {
604 604 display: inline;
605 605 width: @medium-inline-input-width;
606 606 min-width: 100px;
607 607 }
608 608
609 609 select {
610 610 //reset
611 611 -webkit-appearance: none;
612 612 -moz-appearance: none;
613 613
614 614 display: inline-block;
615 615 height: 28px;
616 616 width: auto;
617 617 margin: 0 @padding @padding 0;
618 618 padding: 0 18px 0 8px;
619 619 line-height:1em;
620 620 font-size: @basefontsize;
621 621 border: @border-thickness solid @rcblue;
622 622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 623 color: @rcblue;
624 624
625 625 &:after {
626 626 content: "\00A0\25BE";
627 627 }
628 628
629 629 &:focus {
630 630 outline: none;
631 631 }
632 632 }
633 633
634 634 option {
635 635 &:focus {
636 636 outline: none;
637 637 }
638 638 }
639 639
640 640 input,
641 641 textarea {
642 642 padding: @input-padding;
643 643 border: @input-border-thickness solid @border-highlight-color;
644 644 .border-radius (@border-radius);
645 645 font-family: @text-light;
646 646 font-size: @basefontsize;
647 647
648 648 &.input-sm {
649 649 padding: 5px;
650 650 }
651 651
652 652 &#description {
653 653 min-width: @input-description-minwidth;
654 654 min-height: 1em;
655 655 padding: 10px;
656 656 }
657 657 }
658 658
659 659 .field-sm {
660 660 input,
661 661 textarea {
662 662 padding: 5px;
663 663 }
664 664 }
665 665
666 666 textarea {
667 667 display: block;
668 668 clear: both;
669 669 width: 100%;
670 670 min-height: 100px;
671 671 margin-bottom: @padding;
672 672 .box-sizing(border-box);
673 673 overflow: auto;
674 674 }
675 675
676 676 label {
677 677 font-family: @text-light;
678 678 }
679 679
680 680 // GRAVATARS
681 681 // centers gravatar on username to the right
682 682
683 683 .gravatar {
684 684 display: inline;
685 685 min-width: 16px;
686 686 min-height: 16px;
687 687 margin: -5px 0;
688 688 padding: 0;
689 689 line-height: 1em;
690 690 border: 1px solid @grey4;
691 691
692 692 &.gravatar-large {
693 693 margin: -0.5em .25em -0.5em 0;
694 694 }
695 695
696 696 & + .user {
697 697 display: inline;
698 698 margin: 0;
699 699 padding: 0 0 0 .17em;
700 700 line-height: 1em;
701 701 }
702 702 }
703 703
704 704 .user-inline-data {
705 705 display: inline-block;
706 706 float: left;
707 707 padding-left: .5em;
708 708 line-height: 1.3em;
709 709 }
710 710
711 711 .rc-user { // gravatar + user wrapper
712 712 float: left;
713 713 position: relative;
714 714 min-width: 100px;
715 715 max-width: 200px;
716 716 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 717 display: block;
718 718 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719 719
720 720
721 721 .gravatar {
722 722 display: block;
723 723 position: absolute;
724 724 top: 0;
725 725 left: 0;
726 726 min-width: @gravatar-size;
727 727 min-height: @gravatar-size;
728 728 margin: 0;
729 729 }
730 730
731 731 .user {
732 732 display: block;
733 733 max-width: 175px;
734 734 padding-top: 2px;
735 735 overflow: hidden;
736 736 text-overflow: ellipsis;
737 737 }
738 738 }
739 739
740 740 .gist-gravatar,
741 741 .journal_container {
742 742 .gravatar-large {
743 743 margin: 0 .5em -10px 0;
744 744 }
745 745 }
746 746
747 747
748 748 // ADMIN SETTINGS
749 749
750 750 // Tag Patterns
751 751 .tag_patterns {
752 752 .tag_input {
753 753 margin-bottom: @padding;
754 754 }
755 755 }
756 756
757 757 .locked_input {
758 758 position: relative;
759 759
760 760 input {
761 761 display: inline;
762 762 margin-top: 3px;
763 763 }
764 764
765 765 br {
766 766 display: none;
767 767 }
768 768
769 769 .error-message {
770 770 float: left;
771 771 width: 100%;
772 772 }
773 773
774 774 .lock_input_button {
775 775 display: inline;
776 776 }
777 777
778 778 .help-block {
779 779 clear: both;
780 780 }
781 781 }
782 782
783 783 // Notifications
784 784
785 785 .notifications_buttons {
786 786 margin: 0 0 @space 0;
787 787 padding: 0;
788 788
789 789 .btn {
790 790 display: inline-block;
791 791 }
792 792 }
793 793
794 794 .notification-list {
795 795
796 796 div {
797 797 display: inline-block;
798 798 vertical-align: middle;
799 799 }
800 800
801 801 .container {
802 802 display: block;
803 803 margin: 0 0 @padding 0;
804 804 }
805 805
806 806 .delete-notifications {
807 807 margin-left: @padding;
808 808 text-align: right;
809 809 cursor: pointer;
810 810 }
811 811
812 812 .read-notifications {
813 813 margin-left: @padding/2;
814 814 text-align: right;
815 815 width: 35px;
816 816 cursor: pointer;
817 817 }
818 818
819 819 .icon-minus-sign {
820 820 color: @alert2;
821 821 }
822 822
823 823 .icon-ok-sign {
824 824 color: @alert1;
825 825 }
826 826 }
827 827
828 828 .user_settings {
829 829 float: left;
830 830 clear: both;
831 831 display: block;
832 832 width: 100%;
833 833
834 834 .gravatar_box {
835 835 margin-bottom: @padding;
836 836
837 837 &:after {
838 838 content: " ";
839 839 clear: both;
840 840 width: 100%;
841 841 }
842 842 }
843 843
844 844 .fields .field {
845 845 clear: both;
846 846 }
847 847 }
848 848
849 849 .advanced_settings {
850 850 margin-bottom: @space;
851 851
852 852 .help-block {
853 853 margin-left: 0;
854 854 }
855 855
856 856 button + .help-block {
857 857 margin-top: @padding;
858 858 }
859 859 }
860 860
861 861 // admin settings radio buttons and labels
862 862 .label-2 {
863 863 float: left;
864 864 width: @label2-width;
865 865
866 866 label {
867 867 color: @grey1;
868 868 }
869 869 }
870 870 .checkboxes {
871 871 float: left;
872 872 width: @checkboxes-width;
873 873 margin-bottom: @padding;
874 874
875 875 .checkbox {
876 876 width: 100%;
877 877
878 878 label {
879 879 margin: 0;
880 880 padding: 0;
881 881 }
882 882 }
883 883
884 884 .checkbox + .checkbox {
885 885 display: inline-block;
886 886 }
887 887
888 888 label {
889 889 margin-right: 1em;
890 890 }
891 891 }
892 892
893 893 // CHANGELOG
894 894 .container_header {
895 895 float: left;
896 896 display: block;
897 897 width: 100%;
898 898 margin: @padding 0 @padding;
899 899
900 900 #filter_changelog {
901 901 float: left;
902 902 margin-right: @padding;
903 903 }
904 904
905 905 .breadcrumbs_light {
906 906 display: inline-block;
907 907 }
908 908 }
909 909
910 910 .info_box {
911 911 float: right;
912 912 }
913 913
914 914
915 915 #graph_nodes {
916 916 padding-top: 43px;
917 917 }
918 918
919 919 #graph_content{
920 920
921 921 // adjust for table headers so that graph renders properly
922 922 // #graph_nodes padding - table cell padding
923 923 padding-top: (@space - (@basefontsize * 2.4));
924 924
925 925 &.graph_full_width {
926 926 width: 100%;
927 927 max-width: 100%;
928 928 }
929 929 }
930 930
931 931 #graph {
932 932 .flag_status {
933 933 margin: 0;
934 934 }
935 935
936 936 .pagination-left {
937 937 float: left;
938 938 clear: both;
939 939 }
940 940
941 941 .log-container {
942 942 max-width: 345px;
943 943
944 944 .message{
945 945 max-width: 340px;
946 946 }
947 947 }
948 948
949 949 .graph-col-wrapper {
950 950 padding-left: 110px;
951 951
952 952 #graph_nodes {
953 953 width: 100px;
954 954 margin-left: -110px;
955 955 float: left;
956 956 clear: left;
957 957 }
958 958 }
959 959 }
960 960
961 961 #filter_changelog {
962 962 float: left;
963 963 }
964 964
965 965
966 966 //--- THEME ------------------//
967 967
968 968 #logo {
969 969 float: left;
970 970 margin: 9px 0 0 0;
971 971
972 972 .header {
973 973 background-color: transparent;
974 974 }
975 975
976 976 a {
977 977 display: inline-block;
978 978 }
979 979
980 980 img {
981 981 height:30px;
982 982 }
983 983 }
984 984
985 985 .logo-wrapper {
986 986 float:left;
987 987 }
988 988
989 989 .branding{
990 990 float: left;
991 991 padding: 9px 2px;
992 992 line-height: 1em;
993 993 font-size: @navigation-fontsize;
994 994 }
995 995
996 996 img {
997 997 border: none;
998 998 outline: none;
999 999 }
1000 1000 user-profile-header
1001 1001 label {
1002 1002
1003 1003 input[type="checkbox"] {
1004 1004 margin-right: 1em;
1005 1005 }
1006 1006 input[type="radio"] {
1007 1007 margin-right: 1em;
1008 1008 }
1009 1009 }
1010 1010
1011 1011 .flag_status {
1012 1012 margin: 2px 8px 6px 2px;
1013 1013 &.under_review {
1014 1014 .circle(5px, @alert3);
1015 1015 }
1016 1016 &.approved {
1017 1017 .circle(5px, @alert1);
1018 1018 }
1019 1019 &.rejected,
1020 1020 &.forced_closed{
1021 1021 .circle(5px, @alert2);
1022 1022 }
1023 1023 &.not_reviewed {
1024 1024 .circle(5px, @grey5);
1025 1025 }
1026 1026 }
1027 1027
1028 1028 .flag_status_comment_box {
1029 1029 margin: 5px 6px 0px 2px;
1030 1030 }
1031 1031 .test_pattern_preview {
1032 1032 margin: @space 0;
1033 1033
1034 1034 p {
1035 1035 margin-bottom: 0;
1036 1036 border-bottom: @border-thickness solid @border-default-color;
1037 1037 color: @grey3;
1038 1038 }
1039 1039
1040 1040 .btn {
1041 1041 margin-bottom: @padding;
1042 1042 }
1043 1043 }
1044 1044 #test_pattern_result {
1045 1045 display: none;
1046 1046 &:extend(pre);
1047 1047 padding: .9em;
1048 1048 color: @grey3;
1049 1049 background-color: @grey7;
1050 1050 border-right: @border-thickness solid @border-default-color;
1051 1051 border-bottom: @border-thickness solid @border-default-color;
1052 1052 border-left: @border-thickness solid @border-default-color;
1053 1053 }
1054 1054
1055 1055 #repo_vcs_settings {
1056 1056 #inherit_overlay_vcs_default {
1057 1057 display: none;
1058 1058 }
1059 1059 #inherit_overlay_vcs_custom {
1060 1060 display: custom;
1061 1061 }
1062 1062 &.inherited {
1063 1063 #inherit_overlay_vcs_default {
1064 1064 display: block;
1065 1065 }
1066 1066 #inherit_overlay_vcs_custom {
1067 1067 display: none;
1068 1068 }
1069 1069 }
1070 1070 }
1071 1071
1072 1072 .issue-tracker-link {
1073 1073 color: @rcblue;
1074 1074 }
1075 1075
1076 1076 // Issue Tracker Table Show/Hide
1077 1077 #repo_issue_tracker {
1078 1078 #inherit_overlay {
1079 1079 display: none;
1080 1080 }
1081 1081 #custom_overlay {
1082 1082 display: custom;
1083 1083 }
1084 1084 &.inherited {
1085 1085 #inherit_overlay {
1086 1086 display: block;
1087 1087 }
1088 1088 #custom_overlay {
1089 1089 display: none;
1090 1090 }
1091 1091 }
1092 1092 }
1093 1093 table.issuetracker {
1094 1094 &.readonly {
1095 1095 tr, td {
1096 1096 color: @grey3;
1097 1097 }
1098 1098 }
1099 1099 .edit {
1100 1100 display: none;
1101 1101 }
1102 1102 .editopen {
1103 1103 .edit {
1104 1104 display: inline;
1105 1105 }
1106 1106 .entry {
1107 1107 display: none;
1108 1108 }
1109 1109 }
1110 1110 tr td.td-action {
1111 1111 min-width: 117px;
1112 1112 }
1113 1113 td input {
1114 1114 max-width: none;
1115 1115 min-width: 30px;
1116 1116 width: 80%;
1117 1117 }
1118 1118 .issuetracker_pref input {
1119 1119 width: 40%;
1120 1120 }
1121 1121 input.edit_issuetracker_update {
1122 1122 margin-right: 0;
1123 1123 width: auto;
1124 1124 }
1125 1125 }
1126 1126
1127 1127 table.integrations {
1128 1128 .td-icon {
1129 1129 width: 20px;
1130 1130 .integration-icon {
1131 1131 height: 20px;
1132 1132 width: 20px;
1133 1133 }
1134 1134 }
1135 1135 }
1136 1136
1137 1137 .integrations {
1138 1138 a.integration-box {
1139 1139 color: @text-color;
1140 1140 &:hover {
1141 1141 .panel {
1142 1142 background: #fbfbfb;
1143 1143 }
1144 1144 }
1145 1145 .integration-icon {
1146 1146 width: 30px;
1147 1147 height: 30px;
1148 1148 margin-right: 20px;
1149 1149 float: left;
1150 1150 }
1151 1151
1152 1152 .panel-body {
1153 1153 padding: 10px;
1154 1154 }
1155 1155 .panel {
1156 1156 margin-bottom: 10px;
1157 1157 }
1158 1158 h2 {
1159 1159 display: inline-block;
1160 1160 margin: 0;
1161 1161 min-width: 140px;
1162 1162 }
1163 1163 }
1164 1164 }
1165 1165
1166 1166 //Permissions Settings
1167 1167 #add_perm {
1168 1168 margin: 0 0 @padding;
1169 1169 cursor: pointer;
1170 1170 }
1171 1171
1172 1172 .perm_ac {
1173 1173 input {
1174 1174 width: 95%;
1175 1175 }
1176 1176 }
1177 1177
1178 1178 .autocomplete-suggestions {
1179 1179 width: auto !important; // overrides autocomplete.js
1180 1180 margin: 0;
1181 1181 border: @border-thickness solid @rcblue;
1182 1182 border-radius: @border-radius;
1183 1183 color: @rcblue;
1184 1184 background-color: white;
1185 1185 }
1186 1186 .autocomplete-selected {
1187 1187 background: #F0F0F0;
1188 1188 }
1189 1189 .ac-container-wrap {
1190 1190 margin: 0;
1191 1191 padding: 8px;
1192 1192 border-bottom: @border-thickness solid @rclightblue;
1193 1193 list-style-type: none;
1194 1194 cursor: pointer;
1195 1195
1196 1196 &:hover {
1197 1197 background-color: @rclightblue;
1198 1198 }
1199 1199
1200 1200 img {
1201 1201 height: @gravatar-size;
1202 1202 width: @gravatar-size;
1203 1203 margin-right: 1em;
1204 1204 }
1205 1205
1206 1206 strong {
1207 1207 font-weight: normal;
1208 1208 }
1209 1209 }
1210 1210
1211 1211 // Settings Dropdown
1212 1212 .user-menu .container {
1213 1213 padding: 0 4px;
1214 1214 margin: 0;
1215 1215 }
1216 1216
1217 1217 .user-menu .gravatar {
1218 1218 cursor: pointer;
1219 1219 }
1220 1220
1221 1221 .codeblock {
1222 1222 margin-bottom: @padding;
1223 1223 clear: both;
1224 1224
1225 1225 .stats{
1226 1226 overflow: hidden;
1227 1227 }
1228 1228
1229 1229 .message{
1230 1230 textarea{
1231 1231 margin: 0;
1232 1232 }
1233 1233 }
1234 1234
1235 1235 .code-header {
1236 1236 .stats {
1237 1237 line-height: 2em;
1238 1238
1239 1239 .revision_id {
1240 1240 margin-left: 0;
1241 1241 }
1242 1242 .buttons {
1243 1243 padding-right: 0;
1244 1244 }
1245 1245 }
1246 1246
1247 1247 .item{
1248 1248 margin-right: 0.5em;
1249 1249 }
1250 1250 }
1251 1251
1252 1252 #editor_container{
1253 1253 position: relative;
1254 1254 margin: @padding;
1255 1255 }
1256 1256 }
1257 1257
1258 1258 #file_history_container {
1259 1259 display: none;
1260 1260 }
1261 1261
1262 1262 .file-history-inner {
1263 1263 margin-bottom: 10px;
1264 1264 }
1265 1265
1266 1266 // Pull Requests
1267 1267 .summary-details {
1268 1268 width: 72%;
1269 1269 }
1270 1270 .pr-summary {
1271 1271 border-bottom: @border-thickness solid @grey5;
1272 1272 margin-bottom: @space;
1273 1273 }
1274 1274 .reviewers-title {
1275 1275 width: 25%;
1276 1276 min-width: 200px;
1277 1277 }
1278 1278 .reviewers {
1279 1279 width: 25%;
1280 1280 min-width: 200px;
1281 1281 }
1282 1282 .reviewers ul li {
1283 1283 position: relative;
1284 1284 width: 100%;
1285 1285 margin-bottom: 8px;
1286 1286 }
1287 1287 .reviewers_member {
1288 1288 width: 100%;
1289 1289 overflow: auto;
1290 1290 }
1291 1291 .reviewer_reason {
1292 1292 padding-left: 20px;
1293 1293 }
1294 1294 .reviewer_status {
1295 1295 display: inline-block;
1296 1296 vertical-align: top;
1297 1297 width: 7%;
1298 1298 min-width: 20px;
1299 1299 height: 1.2em;
1300 1300 margin-top: 3px;
1301 1301 line-height: 1em;
1302 1302 }
1303 1303
1304 1304 .reviewer_name {
1305 1305 display: inline-block;
1306 1306 max-width: 83%;
1307 1307 padding-right: 20px;
1308 1308 vertical-align: middle;
1309 1309 line-height: 1;
1310 1310
1311 1311 .rc-user {
1312 1312 min-width: 0;
1313 1313 margin: -2px 1em 0 0;
1314 1314 }
1315 1315
1316 1316 .reviewer {
1317 1317 float: left;
1318 1318 }
1319 1319
1320 1320 &.to-delete {
1321 1321 .user,
1322 1322 .reviewer {
1323 1323 text-decoration: line-through;
1324 1324 }
1325 1325 }
1326 1326 }
1327 1327
1328 1328 .reviewer_member_remove {
1329 1329 position: absolute;
1330 1330 right: 0;
1331 1331 top: 0;
1332 1332 width: 16px;
1333 1333 margin-bottom: 10px;
1334 1334 padding: 0;
1335 1335 color: black;
1336 1336 }
1337 1337 .reviewer_member_status {
1338 1338 margin-top: 5px;
1339 1339 }
1340 1340 .pr-summary #summary{
1341 1341 width: 100%;
1342 1342 }
1343 1343 .pr-summary .action_button:hover {
1344 1344 border: 0;
1345 1345 cursor: pointer;
1346 1346 }
1347 1347 .pr-details-title {
1348 1348 padding-bottom: 8px;
1349 1349 border-bottom: @border-thickness solid @grey5;
1350 1350
1351 1351 .action_button.disabled {
1352 1352 color: @grey4;
1353 1353 cursor: inherit;
1354 1354 }
1355 1355 .action_button {
1356 1356 color: @rcblue;
1357 1357 }
1358 1358 }
1359 1359 .pr-details-content {
1360 1360 margin-top: @textmargin;
1361 1361 margin-bottom: @textmargin;
1362 1362 }
1363 1363 .pr-description {
1364 1364 white-space:pre-wrap;
1365 1365 }
1366 1366 .group_members {
1367 1367 margin-top: 0;
1368 1368 padding: 0;
1369 1369 list-style: outside none none;
1370 1370
1371 1371 img {
1372 1372 height: @gravatar-size;
1373 1373 width: @gravatar-size;
1374 1374 margin-right: .5em;
1375 1375 margin-left: 3px;
1376 1376 }
1377 1377
1378 1378 .to-delete {
1379 1379 .user {
1380 1380 text-decoration: line-through;
1381 1381 }
1382 1382 }
1383 1383 }
1384 1384
1385 1385 .compare_view_commits_title {
1386 1386 .disabled {
1387 1387 cursor: inherit;
1388 1388 &:hover{
1389 1389 background-color: inherit;
1390 1390 color: inherit;
1391 1391 }
1392 1392 }
1393 1393 }
1394 1394
1395 1395 // new entry in group_members
1396 1396 .td-author-new-entry {
1397 1397 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 1398 }
1399 1399
1400 1400 .usergroup_member_remove {
1401 1401 width: 16px;
1402 1402 margin-bottom: 10px;
1403 1403 padding: 0;
1404 1404 color: black !important;
1405 1405 cursor: pointer;
1406 1406 }
1407 1407
1408 1408 .reviewer_ac .ac-input {
1409 1409 width: 92%;
1410 1410 margin-bottom: 1em;
1411 1411 }
1412 1412
1413 1413 .compare_view_commits tr{
1414 1414 height: 20px;
1415 1415 }
1416 1416 .compare_view_commits td {
1417 1417 vertical-align: top;
1418 1418 padding-top: 10px;
1419 1419 }
1420 1420 .compare_view_commits .author {
1421 1421 margin-left: 5px;
1422 1422 }
1423 1423
1424 1424 .compare_view_files {
1425 1425 width: 100%;
1426 1426
1427 1427 td {
1428 1428 vertical-align: middle;
1429 1429 }
1430 1430 }
1431 1431
1432 1432 .compare_view_filepath {
1433 1433 color: @grey1;
1434 1434 }
1435 1435
1436 1436 .show_more {
1437 1437 display: inline-block;
1438 1438 position: relative;
1439 1439 vertical-align: middle;
1440 1440 width: 4px;
1441 1441 height: @basefontsize;
1442 1442
1443 1443 &:after {
1444 1444 content: "\00A0\25BE";
1445 1445 display: inline-block;
1446 1446 width:10px;
1447 1447 line-height: 5px;
1448 1448 font-size: 12px;
1449 1449 cursor: pointer;
1450 1450 }
1451 1451 }
1452 1452
1453 1453 .journal_more .show_more {
1454 1454 display: inline;
1455 1455
1456 1456 &:after {
1457 1457 content: none;
1458 1458 }
1459 1459 }
1460 1460
1461 1461 .open .show_more:after,
1462 1462 .select2-dropdown-open .show_more:after {
1463 1463 .rotate(180deg);
1464 1464 margin-left: 4px;
1465 1465 }
1466 1466
1467 1467
1468 1468 .compare_view_commits .collapse_commit:after {
1469 1469 cursor: pointer;
1470 1470 content: "\00A0\25B4";
1471 1471 margin-left: -3px;
1472 1472 font-size: 17px;
1473 1473 color: @grey4;
1474 1474 }
1475 1475
1476 1476 .diff_links {
1477 1477 margin-left: 8px;
1478 1478 }
1479 1479
1480 1480 div.ancestor {
1481 1481 margin: @padding 0;
1482 1482 line-height: 3.0em;
1483 1483 }
1484 1484
1485 1485 .cs_icon_td input[type="checkbox"] {
1486 1486 display: none;
1487 1487 }
1488 1488
1489 1489 .cs_icon_td .expand_file_icon:after {
1490 1490 cursor: pointer;
1491 1491 content: "\00A0\25B6";
1492 1492 font-size: 12px;
1493 1493 color: @grey4;
1494 1494 }
1495 1495
1496 1496 .cs_icon_td .collapse_file_icon:after {
1497 1497 cursor: pointer;
1498 1498 content: "\00A0\25BC";
1499 1499 font-size: 12px;
1500 1500 color: @grey4;
1501 1501 }
1502 1502
1503 1503 /*new binary
1504 1504 NEW_FILENODE = 1
1505 1505 DEL_FILENODE = 2
1506 1506 MOD_FILENODE = 3
1507 1507 RENAMED_FILENODE = 4
1508 1508 COPIED_FILENODE = 5
1509 1509 CHMOD_FILENODE = 6
1510 1510 BIN_FILENODE = 7
1511 1511 */
1512 1512 .cs_files_expand {
1513 1513 font-size: @basefontsize + 5px;
1514 1514 line-height: 1.8em;
1515 1515 float: right;
1516 1516 }
1517 1517
1518 1518 .cs_files_expand span{
1519 1519 color: @rcblue;
1520 1520 cursor: pointer;
1521 1521 }
1522 1522 .cs_files {
1523 1523 clear: both;
1524 1524 padding-bottom: @padding;
1525 1525
1526 1526 .cur_cs {
1527 1527 margin: 10px 2px;
1528 1528 font-weight: bold;
1529 1529 }
1530 1530
1531 1531 .node {
1532 1532 float: left;
1533 1533 }
1534 1534
1535 1535 .changes {
1536 1536 float: right;
1537 1537 color: white;
1538 1538 font-size: @basefontsize - 4px;
1539 1539 margin-top: 4px;
1540 1540 opacity: 0.6;
1541 1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1542 1542
1543 1543 .added {
1544 1544 background-color: @alert1;
1545 1545 float: left;
1546 1546 text-align: center;
1547 1547 }
1548 1548
1549 1549 .deleted {
1550 1550 background-color: @alert2;
1551 1551 float: left;
1552 1552 text-align: center;
1553 1553 }
1554 1554
1555 1555 .bin {
1556 1556 background-color: @alert1;
1557 1557 text-align: center;
1558 1558 }
1559 1559
1560 1560 /*new binary*/
1561 1561 .bin.bin1 {
1562 1562 background-color: @alert1;
1563 1563 text-align: center;
1564 1564 }
1565 1565
1566 1566 /*deleted binary*/
1567 1567 .bin.bin2 {
1568 1568 background-color: @alert2;
1569 1569 text-align: center;
1570 1570 }
1571 1571
1572 1572 /*mod binary*/
1573 1573 .bin.bin3 {
1574 1574 background-color: @grey2;
1575 1575 text-align: center;
1576 1576 }
1577 1577
1578 1578 /*rename file*/
1579 1579 .bin.bin4 {
1580 1580 background-color: @alert4;
1581 1581 text-align: center;
1582 1582 }
1583 1583
1584 1584 /*copied file*/
1585 1585 .bin.bin5 {
1586 1586 background-color: @alert4;
1587 1587 text-align: center;
1588 1588 }
1589 1589
1590 1590 /*chmod file*/
1591 1591 .bin.bin6 {
1592 1592 background-color: @grey2;
1593 1593 text-align: center;
1594 1594 }
1595 1595 }
1596 1596 }
1597 1597
1598 1598 .cs_files .cs_added, .cs_files .cs_A,
1599 1599 .cs_files .cs_added, .cs_files .cs_M,
1600 1600 .cs_files .cs_added, .cs_files .cs_D {
1601 1601 height: 16px;
1602 1602 padding-right: 10px;
1603 1603 margin-top: 7px;
1604 1604 text-align: left;
1605 1605 }
1606 1606
1607 1607 .cs_icon_td {
1608 1608 min-width: 16px;
1609 1609 width: 16px;
1610 1610 }
1611 1611
1612 1612 .pull-request-merge {
1613 1613 padding: 10px 0;
1614 1614 margin-top: 10px;
1615 1615 margin-bottom: 20px;
1616 1616 }
1617 1617
1618 1618 .pull-request-merge .pull-request-wrap {
1619 1619 height: 25px;
1620 1620 padding: 5px 0;
1621 1621 }
1622 1622
1623 1623 .pull-request-merge span {
1624 1624 margin-right: 10px;
1625 1625 }
1626
1627 .pr-versions {
1628 position: relative;
1629 top: 6px;
1630 }
1631
1626 1632 #close_pull_request {
1627 1633 margin-right: 0px;
1628 1634 }
1629 1635
1630 1636 .empty_data {
1631 1637 color: @grey4;
1632 1638 }
1633 1639
1634 1640 #changeset_compare_view_content {
1635 1641 margin-bottom: @space;
1636 1642 clear: both;
1637 1643 width: 100%;
1638 1644 box-sizing: border-box;
1639 1645 .border-radius(@border-radius);
1640 1646
1641 1647 .help-block {
1642 1648 margin: @padding 0;
1643 1649 color: @text-color;
1644 1650 }
1645 1651
1646 1652 .empty_data {
1647 1653 margin: @padding 0;
1648 1654 }
1649 1655
1650 1656 .alert {
1651 1657 margin-bottom: @space;
1652 1658 }
1653 1659 }
1654 1660
1655 1661 .table_disp {
1656 1662 .status {
1657 1663 width: auto;
1658 1664
1659 1665 .flag_status {
1660 1666 float: left;
1661 1667 }
1662 1668 }
1663 1669 }
1664 1670
1665 1671 .status_box_menu {
1666 1672 margin: 0;
1667 1673 }
1668 1674
1669 1675 .notification-table{
1670 1676 margin-bottom: @space;
1671 1677 display: table;
1672 1678 width: 100%;
1673 1679
1674 1680 .container{
1675 1681 display: table-row;
1676 1682
1677 1683 .notification-header{
1678 1684 border-bottom: @border-thickness solid @border-default-color;
1679 1685 }
1680 1686
1681 1687 .notification-subject{
1682 1688 display: table-cell;
1683 1689 }
1684 1690 }
1685 1691 }
1686 1692
1687 1693 // Notifications
1688 1694 .notification-header{
1689 1695 display: table;
1690 1696 width: 100%;
1691 1697 padding: floor(@basefontsize/2) 0;
1692 1698 line-height: 1em;
1693 1699
1694 1700 .desc, .delete-notifications, .read-notifications{
1695 1701 display: table-cell;
1696 1702 text-align: left;
1697 1703 }
1698 1704
1699 1705 .desc{
1700 1706 width: 1163px;
1701 1707 }
1702 1708
1703 1709 .delete-notifications, .read-notifications{
1704 1710 width: 35px;
1705 1711 min-width: 35px; //fixes when only one button is displayed
1706 1712 }
1707 1713 }
1708 1714
1709 1715 .notification-body {
1710 1716 .markdown-block,
1711 1717 .rst-block {
1712 1718 padding: @padding 0;
1713 1719 }
1714 1720
1715 1721 .notification-subject {
1716 1722 padding: @textmargin 0;
1717 1723 border-bottom: @border-thickness solid @border-default-color;
1718 1724 }
1719 1725 }
1720 1726
1721 1727
1722 1728 .notifications_buttons{
1723 1729 float: right;
1724 1730 }
1725 1731
1726 1732 #notification-status{
1727 1733 display: inline;
1728 1734 }
1729 1735
1730 1736 // Repositories
1731 1737
1732 1738 #summary.fields{
1733 1739 display: table;
1734 1740
1735 1741 .field{
1736 1742 display: table-row;
1737 1743
1738 1744 .label-summary{
1739 1745 display: table-cell;
1740 1746 min-width: @label-summary-minwidth;
1741 1747 padding-top: @padding/2;
1742 1748 padding-bottom: @padding/2;
1743 1749 padding-right: @padding/2;
1744 1750 }
1745 1751
1746 1752 .input{
1747 1753 display: table-cell;
1748 1754 padding: @padding/2;
1749 1755
1750 1756 input{
1751 1757 min-width: 29em;
1752 1758 padding: @padding/4;
1753 1759 }
1754 1760 }
1755 1761 .statistics, .downloads{
1756 1762 .disabled{
1757 1763 color: @grey4;
1758 1764 }
1759 1765 }
1760 1766 }
1761 1767 }
1762 1768
1763 1769 #summary{
1764 1770 width: 70%;
1765 1771 }
1766 1772
1767 1773
1768 1774 // Journal
1769 1775 .journal.title {
1770 1776 h5 {
1771 1777 float: left;
1772 1778 margin: 0;
1773 1779 width: 70%;
1774 1780 }
1775 1781
1776 1782 ul {
1777 1783 float: right;
1778 1784 display: inline-block;
1779 1785 margin: 0;
1780 1786 width: 30%;
1781 1787 text-align: right;
1782 1788
1783 1789 li {
1784 1790 display: inline;
1785 1791 font-size: @journal-fontsize;
1786 1792 line-height: 1em;
1787 1793
1788 1794 &:before { content: none; }
1789 1795 }
1790 1796 }
1791 1797 }
1792 1798
1793 1799 .filterexample {
1794 1800 position: absolute;
1795 1801 top: 95px;
1796 1802 left: @contentpadding;
1797 1803 color: @rcblue;
1798 1804 font-size: 11px;
1799 1805 font-family: @text-regular;
1800 1806 cursor: help;
1801 1807
1802 1808 &:hover {
1803 1809 color: @rcdarkblue;
1804 1810 }
1805 1811
1806 1812 @media (max-width:768px) {
1807 1813 position: relative;
1808 1814 top: auto;
1809 1815 left: auto;
1810 1816 display: block;
1811 1817 }
1812 1818 }
1813 1819
1814 1820
1815 1821 #journal{
1816 1822 margin-bottom: @space;
1817 1823
1818 1824 .journal_day{
1819 1825 margin-bottom: @textmargin/2;
1820 1826 padding-bottom: @textmargin/2;
1821 1827 font-size: @journal-fontsize;
1822 1828 border-bottom: @border-thickness solid @border-default-color;
1823 1829 }
1824 1830
1825 1831 .journal_container{
1826 1832 margin-bottom: @space;
1827 1833
1828 1834 .journal_user{
1829 1835 display: inline-block;
1830 1836 }
1831 1837 .journal_action_container{
1832 1838 display: block;
1833 1839 margin-top: @textmargin;
1834 1840
1835 1841 div{
1836 1842 display: inline;
1837 1843 }
1838 1844
1839 1845 div.journal_action_params{
1840 1846 display: block;
1841 1847 }
1842 1848
1843 1849 div.journal_repo:after{
1844 1850 content: "\A";
1845 1851 white-space: pre;
1846 1852 }
1847 1853
1848 1854 div.date{
1849 1855 display: block;
1850 1856 margin-bottom: @textmargin;
1851 1857 }
1852 1858 }
1853 1859 }
1854 1860 }
1855 1861
1856 1862 // Files
1857 1863 .edit-file-title {
1858 1864 border-bottom: @border-thickness solid @border-default-color;
1859 1865
1860 1866 .breadcrumbs {
1861 1867 margin-bottom: 0;
1862 1868 }
1863 1869 }
1864 1870
1865 1871 .edit-file-fieldset {
1866 1872 margin-top: @sidebarpadding;
1867 1873
1868 1874 .fieldset {
1869 1875 .left-label {
1870 1876 width: 13%;
1871 1877 }
1872 1878 .right-content {
1873 1879 width: 87%;
1874 1880 max-width: 100%;
1875 1881 }
1876 1882 .filename-label {
1877 1883 margin-top: 13px;
1878 1884 }
1879 1885 .commit-message-label {
1880 1886 margin-top: 4px;
1881 1887 }
1882 1888 .file-upload-input {
1883 1889 input {
1884 1890 display: none;
1885 1891 }
1886 1892 }
1887 1893 p {
1888 1894 margin-top: 5px;
1889 1895 }
1890 1896
1891 1897 }
1892 1898 .custom-path-link {
1893 1899 margin-left: 5px;
1894 1900 }
1895 1901 #commit {
1896 1902 resize: vertical;
1897 1903 }
1898 1904 }
1899 1905
1900 1906 .delete-file-preview {
1901 1907 max-height: 250px;
1902 1908 }
1903 1909
1904 1910 .new-file,
1905 1911 #filter_activate,
1906 1912 #filter_deactivate {
1907 1913 float: left;
1908 1914 margin: 0 0 0 15px;
1909 1915 }
1910 1916
1911 1917 h3.files_location{
1912 1918 line-height: 2.4em;
1913 1919 }
1914 1920
1915 1921 .browser-nav {
1916 1922 display: table;
1917 1923 margin-bottom: @space;
1918 1924
1919 1925
1920 1926 .info_box {
1921 1927 display: inline-table;
1922 1928 height: 2.5em;
1923 1929
1924 1930 .browser-cur-rev, .info_box_elem {
1925 1931 display: table-cell;
1926 1932 vertical-align: middle;
1927 1933 }
1928 1934
1929 1935 .info_box_elem {
1930 1936 border-top: @border-thickness solid @rcblue;
1931 1937 border-bottom: @border-thickness solid @rcblue;
1932 1938
1933 1939 #at_rev, a {
1934 1940 padding: 0.6em 0.9em;
1935 1941 margin: 0;
1936 1942 .box-shadow(none);
1937 1943 border: 0;
1938 1944 height: 12px;
1939 1945 }
1940 1946
1941 1947 input#at_rev {
1942 1948 max-width: 50px;
1943 1949 text-align: right;
1944 1950 }
1945 1951
1946 1952 &.previous {
1947 1953 border: @border-thickness solid @rcblue;
1948 1954 .disabled {
1949 1955 color: @grey4;
1950 1956 cursor: not-allowed;
1951 1957 }
1952 1958 }
1953 1959
1954 1960 &.next {
1955 1961 border: @border-thickness solid @rcblue;
1956 1962 .disabled {
1957 1963 color: @grey4;
1958 1964 cursor: not-allowed;
1959 1965 }
1960 1966 }
1961 1967 }
1962 1968
1963 1969 .browser-cur-rev {
1964 1970
1965 1971 span{
1966 1972 margin: 0;
1967 1973 color: @rcblue;
1968 1974 height: 12px;
1969 1975 display: inline-block;
1970 1976 padding: 0.7em 1em ;
1971 1977 border: @border-thickness solid @rcblue;
1972 1978 margin-right: @padding;
1973 1979 }
1974 1980 }
1975 1981 }
1976 1982
1977 1983 .search_activate {
1978 1984 display: table-cell;
1979 1985 vertical-align: middle;
1980 1986
1981 1987 input, label{
1982 1988 margin: 0;
1983 1989 padding: 0;
1984 1990 }
1985 1991
1986 1992 input{
1987 1993 margin-left: @textmargin;
1988 1994 }
1989 1995
1990 1996 }
1991 1997 }
1992 1998
1993 1999 .browser-cur-rev{
1994 2000 margin-bottom: @textmargin;
1995 2001 }
1996 2002
1997 2003 #node_filter_box_loading{
1998 2004 .info_text;
1999 2005 }
2000 2006
2001 2007 .browser-search {
2002 2008 margin: -25px 0px 5px 0px;
2003 2009 }
2004 2010
2005 2011 .node-filter {
2006 2012 font-size: @repo-title-fontsize;
2007 2013 padding: 4px 0px 0px 0px;
2008 2014
2009 2015 .node-filter-path {
2010 2016 float: left;
2011 2017 color: @grey4;
2012 2018 }
2013 2019 .node-filter-input {
2014 2020 float: left;
2015 2021 margin: -2px 0px 0px 2px;
2016 2022 input {
2017 2023 padding: 2px;
2018 2024 border: none;
2019 2025 font-size: @repo-title-fontsize;
2020 2026 }
2021 2027 }
2022 2028 }
2023 2029
2024 2030
2025 2031 .browser-result{
2026 2032 td a{
2027 2033 margin-left: 0.5em;
2028 2034 display: inline-block;
2029 2035
2030 2036 em{
2031 2037 font-family: @text-bold;
2032 2038 }
2033 2039 }
2034 2040 }
2035 2041
2036 2042 .browser-highlight{
2037 2043 background-color: @grey5-alpha;
2038 2044 }
2039 2045
2040 2046
2041 2047 // Search
2042 2048
2043 2049 .search-form{
2044 2050 #q {
2045 2051 width: @search-form-width;
2046 2052 }
2047 2053 .fields{
2048 2054 margin: 0 0 @space;
2049 2055 }
2050 2056
2051 2057 label{
2052 2058 display: inline-block;
2053 2059 margin-right: @textmargin;
2054 2060 padding-top: 0.25em;
2055 2061 }
2056 2062
2057 2063
2058 2064 .results{
2059 2065 clear: both;
2060 2066 margin: 0 0 @padding;
2061 2067 }
2062 2068 }
2063 2069
2064 2070 div.search-feedback-items {
2065 2071 display: inline-block;
2066 2072 padding:0px 0px 0px 96px;
2067 2073 }
2068 2074
2069 2075 div.search-code-body {
2070 2076 background-color: #ffffff; padding: 5px 0 5px 10px;
2071 2077 pre {
2072 2078 .match { background-color: #faffa6;}
2073 2079 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2074 2080 }
2075 2081 }
2076 2082
2077 2083 .expand_commit.search {
2078 2084 .show_more.open {
2079 2085 height: auto;
2080 2086 max-height: none;
2081 2087 }
2082 2088 }
2083 2089
2084 2090 .search-results {
2085 2091
2086 2092 h2 {
2087 2093 margin-bottom: 0;
2088 2094 }
2089 2095 .codeblock {
2090 2096 border: none;
2091 2097 background: transparent;
2092 2098 }
2093 2099
2094 2100 .codeblock-header {
2095 2101 border: none;
2096 2102 background: transparent;
2097 2103 }
2098 2104
2099 2105 .code-body {
2100 2106 border: @border-thickness solid @border-default-color;
2101 2107 .border-radius(@border-radius);
2102 2108 }
2103 2109
2104 2110 .td-commit {
2105 2111 &:extend(pre);
2106 2112 border-bottom: @border-thickness solid @border-default-color;
2107 2113 }
2108 2114
2109 2115 .message {
2110 2116 height: auto;
2111 2117 max-width: 350px;
2112 2118 white-space: normal;
2113 2119 text-overflow: initial;
2114 2120 overflow: visible;
2115 2121
2116 2122 .match { background-color: #faffa6;}
2117 2123 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2118 2124 }
2119 2125
2120 2126 }
2121 2127
2122 2128 table.rctable td.td-search-results div {
2123 2129 max-width: 100%;
2124 2130 }
2125 2131
2126 2132 #tip-box, .tip-box{
2127 2133 padding: @menupadding/2;
2128 2134 display: block;
2129 2135 border: @border-thickness solid @border-highlight-color;
2130 2136 .border-radius(@border-radius);
2131 2137 background-color: white;
2132 2138 z-index: 99;
2133 2139 white-space: pre-wrap;
2134 2140 }
2135 2141
2136 2142 #linktt {
2137 2143 width: 79px;
2138 2144 }
2139 2145
2140 2146 #help_kb .modal-content{
2141 2147 max-width: 750px;
2142 2148 margin: 10% auto;
2143 2149
2144 2150 table{
2145 2151 td,th{
2146 2152 border-bottom: none;
2147 2153 line-height: 2.5em;
2148 2154 }
2149 2155 th{
2150 2156 padding-bottom: @textmargin/2;
2151 2157 }
2152 2158 td.keys{
2153 2159 text-align: center;
2154 2160 }
2155 2161 }
2156 2162
2157 2163 .block-left{
2158 2164 width: 45%;
2159 2165 margin-right: 5%;
2160 2166 }
2161 2167 .modal-footer{
2162 2168 clear: both;
2163 2169 }
2164 2170 .key.tag{
2165 2171 padding: 0.5em;
2166 2172 background-color: @rcblue;
2167 2173 color: white;
2168 2174 border-color: @rcblue;
2169 2175 .box-shadow(none);
2170 2176 }
2171 2177 }
2172 2178
2173 2179
2174 2180
2175 2181 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2176 2182
2177 2183 @import 'statistics-graph';
2178 2184 @import 'tables';
2179 2185 @import 'forms';
2180 2186 @import 'diff';
2181 2187 @import 'summary';
2182 2188 @import 'navigation';
2183 2189
2184 2190 //--- SHOW/HIDE SECTIONS --//
2185 2191
2186 2192 .btn-collapse {
2187 2193 float: right;
2188 2194 text-align: right;
2189 2195 font-family: @text-light;
2190 2196 font-size: @basefontsize;
2191 2197 cursor: pointer;
2192 2198 border: none;
2193 2199 color: @rcblue;
2194 2200 }
2195 2201
2196 2202 table.rctable,
2197 2203 table.dataTable {
2198 2204 .btn-collapse {
2199 2205 float: right;
2200 2206 text-align: right;
2201 2207 }
2202 2208 }
2203 2209
2204 2210
2205 2211 // TODO: johbo: Fix for IE10, this avoids that we see a border
2206 2212 // and padding around checkboxes and radio boxes. Move to the right place,
2207 2213 // or better: Remove this once we did the form refactoring.
2208 2214 input[type=checkbox],
2209 2215 input[type=radio] {
2210 2216 padding: 0;
2211 2217 border: none;
2212 2218 }
2213 2219
2214 2220 .toggle-ajax-spinner{
2215 2221 height: 16px;
2216 2222 width: 16px;
2217 2223 }
@@ -1,530 +1,541 b''
1 1
2 2 // tables.less
3 3 // For use in RhodeCode application tables;
4 4 // see style guide documentation for guidelines.
5 5
6 6 // TABLES
7 7
8 8 .rctable,
9 9 table.rctable,
10 10 table.dataTable {
11 11 clear:both;
12 12 width: 100%;
13 13 margin: 0 auto @padding;
14 14 padding: 0;
15 15 vertical-align: baseline;
16 16 line-height:1.5em;
17 17 border: none;
18 18 outline: none;
19 19 border-collapse: collapse;
20 20 border-spacing: 0;
21 21 color: @grey2;
22 22
23 23 b {
24 24 font-weight: normal;
25 25 }
26 26
27 27 em {
28 28 font-weight: bold;
29 29 font-style: normal;
30 30 }
31 31
32 32 th,
33 33 td {
34 34 height: auto;
35 35 max-width: 20%;
36 36 padding: .65em 1em .65em 0;
37 37 vertical-align: middle;
38 38 border-bottom: @border-thickness solid @grey5;
39 39 white-space: normal;
40 40
41 41 &.td-radio,
42 42 &.td-checkbox {
43 43 padding-right: 0;
44 44 text-align: center;
45 45
46 46 input {
47 47 margin: 0 1em;
48 48 }
49 49 }
50 50
51 51 &.truncate-wrap {
52 52 white-space: nowrap !important;
53 53 }
54 54
55 55 pre {
56 56 margin: 0;
57 57 }
58 58
59 59 .show_more {
60 60 height: inherit;
61 61 }
62 62 }
63 63
64 64 .expired td {
65 65 background-color: @grey7;
66 66 }
67 67
68 68 .td-radio + .td-owner {
69 69 padding-left: 1em;
70 70 }
71 71
72 72
73 73 th {
74 74 text-align: left;
75 75 font-family: @text-semibold;
76 76 }
77 77
78 78 .hl {
79 79 td {
80 80 background-color: lighten(@alert4,25%);
81 81 }
82 82 }
83 83
84 84 // Special Data Cell Types
85 85 // See style guide for desciptions and examples.
86 86
87 87 td {
88 88
89 89 &.user {
90 90 padding-left: 1em;
91 91 }
92 92
93 93 &.td-rss {
94 94 width: 20px;
95 95 min-width: 0;
96 96 margin: 0;
97 97 }
98 98
99 99 &.quick_repo_menu {
100 100 width: 15px;
101 101 text-align: center;
102 102
103 103 &:hover {
104 104 background-color: @grey5;
105 105 }
106 106 }
107 107
108 108 &.td-hash {
109 109 min-width: 80px;
110 110 width: 200px;
111 111 }
112 112
113 113 &.td-time {
114 114 width: 160px;
115 115 white-space: nowrap;
116 116 }
117 117
118 118 &.annotate{
119 119 padding-right: 0;
120 120
121 121 div.annotatediv{
122 122 margin: 0 0.7em;
123 123 }
124 124 }
125 125
126 126 &.tags-col {
127 127 padding-right: 0;
128 128 }
129 129
130 130 &.td-description {
131 131 min-width: 350px;
132 132
133 133 &.truncate, .truncate-wrap {
134 134 white-space: nowrap;
135 135 overflow: hidden;
136 136 text-overflow: ellipsis;
137 137 max-width: 450px;
138 138 }
139 139 }
140 140
141 141 &.td-componentname {
142 142 white-space: nowrap;
143 143 }
144 144
145 145 &.td-journalaction {
146 146 min-width: 300px;
147 147
148 148 .journal_action_params {
149 149 // waiting for feedback
150 150 }
151 151 }
152 152
153 153 &.td-active {
154 154 padding-left: .65em;
155 155 }
156 156
157 157 &.td-url {
158 158 white-space: nowrap;
159 159 }
160 160
161 161 &.td-comments {
162 162 min-width: 3em;
163 163 }
164 164
165 165 &.td-buttons {
166 166 padding: .3em 0;
167 167 }
168 168
169 169 &.td-action {
170 170 // this is for the remove/delete/edit buttons
171 171 padding-right: 0;
172 172 min-width: 95px;
173 173 text-transform: capitalize;
174 174
175 175 i {
176 176 display: none;
177 177 }
178 178 }
179 179
180 180 // TODO: lisa: this needs to be cleaned up with the buttons
181 181 .grid_edit,
182 182 .grid_delete {
183 183 display: inline-block;
184 184 margin: 0 @padding/3 0 0;
185 185 font-family: @text-light;
186 186
187 187 i {
188 188 display: none;
189 189 }
190 190 }
191 191
192 192 .grid_edit + .grid_delete {
193 193 border-left: @border-thickness solid @grey5;
194 194 padding-left: @padding/2;
195 195 }
196 196
197 197 &.td-compare {
198 198
199 199 input {
200 200 margin-right: 1em;
201 201 }
202 202
203 203 .compare-radio-button {
204 204 margin: 0 1em 0 0;
205 205 }
206 206
207 207
208 208 }
209 209
210 210 &.td-tags {
211 211 padding: .5em 1em .5em 0;
212 212 width: 140px;
213 213
214 214 .tag {
215 215 margin: 1px;
216 216 float: left;
217 217 }
218 218 }
219 219
220 220 .icon-svn, .icon-hg, .icon-git {
221 221 font-size: 1.4em;
222 222 }
223 223
224 224 &.collapse_commit,
225 225 &.expand_commit {
226 226 padding-right: 0;
227 227 padding-left: 1em;
228 228 }
229 229 }
230 230
231 231 .perm_admin_row {
232 232 color: @grey4;
233 233 background-color: @grey6;
234 234 }
235 235
236 236 .noborder {
237 237 border: none;
238 238
239 239 td {
240 240 border: none;
241 241 }
242 242 }
243 243 }
244 244
245 245 // TRUNCATING
246 246 // TODO: lisaq: should this possibly be moved out of tables.less?
247 247 // for truncated text
248 248 // used inside of table cells and in code block headers
249 249 .truncate-wrap {
250 250 white-space: nowrap !important;
251 251
252 252 //truncated text
253 253 .truncate {
254 254 max-width: 450px;
255 255 width: 300px;
256 256 overflow: hidden;
257 257 text-overflow: ellipsis;
258 258 -o-text-overflow: ellipsis;
259 259 -ms-text-overflow: ellipsis;
260 260
261 261 &.autoexpand {
262 262 width: 120px;
263 263 margin-right: 200px;
264 264 }
265 265 }
266 266 &:hover .truncate.autoexpand {
267 267 overflow: visible;
268 268 }
269 269
270 270 .tags-truncate {
271 271 width: 150px;
272 272 height: 22px;
273 273 overflow: hidden;
274 274
275 275 .tag {
276 276 display: inline-block;
277 277 }
278 278
279 279 &.truncate {
280 280 height: 22px;
281 281 max-height:2em;
282 282 width: 140px;
283 283 }
284 284 }
285 285 }
286 286
287 287 .apikeys_wrap {
288 288 margin-bottom: @padding;
289 289
290 290 table.rctable td:first-child {
291 291 width: 340px;
292 292 }
293 293 }
294 294
295 295
296 296
297 297 // SPECIAL CASES
298 298
299 299 // Repository Followers
300 300 table.rctable.followers_data {
301 301 width: 75%;
302 302 margin: 0;
303 303 }
304 304
305 305 // Repository List
306 306 // Group Members List
307 307 table.rctable.group_members,
308 308 table#repo_list_table {
309 309 min-width: 600px;
310 310 }
311 311
312 312 // Keyboard mappings
313 313 table.keyboard-mappings {
314 314 th {
315 315 text-align: left;
316 316 font-family: @text-semibold;
317 317 }
318 318 }
319 319
320 320 // Branches, Tags, and Bookmarks
321 321 #obj_list_table.dataTable {
322 322 td.td-time {
323 323 padding-right: 1em;
324 324 }
325 325 }
326 326
327 327 // User Admin
328 328 .rctable.useremails,
329 329 .rctable.account_emails {
330 330 .tag,
331 331 .btn {
332 332 float: right;
333 333 }
334 334 .btn { //to line up with tags
335 335 margin-right: 1.65em;
336 336 }
337 337 }
338 338
339 339 // User List
340 340 #user_list_table {
341 341
342 342 td.td-user {
343 343 min-width: 100px;
344 344 }
345 345 }
346 346
347 347 // Pull Request List Table
348 348 #pull_request_list_table.dataTable {
349 349
350 350 //TODO: lisa: This needs to be removed once the description is adjusted
351 351 // for using an expand_commit button (see issue 765)
352 352 td {
353 353 vertical-align: middle;
354 354 }
355 355 }
356 356
357 357 // Settings (no border)
358 358 table.rctable.dl-settings {
359 359 td {
360 360 border: none;
361 361 }
362 362 }
363 363
364 364
365 365 // Statistics
366 366 table.trending_language_tbl {
367 367 width: 100%;
368 368 line-height: 1em;
369 369
370 370 td div {
371 371 overflow: visible;
372 372 }
373 373 }
374 374
375 375 .trending_language_tbl, .trending_language_tbl td {
376 376 border: 0;
377 377 margin: 0;
378 378 padding: 0;
379 379 background: transparent;
380 380 }
381 381
382 382 .trending_language_tbl, .trending_language_tbl tr {
383 383 border-spacing: 0 3px;
384 384 }
385 385
386 386 .trending_language {
387 387 position: relative;
388 388 width: 100%;
389 389 height: 19px;
390 390 overflow: hidden;
391 391 background-color: @grey6;
392 392
393 393 span, b{
394 394 position: absolute;
395 395 display: block;
396 396 height: 12px;
397 397 margin-bottom: 0px;
398 398 white-space: pre;
399 399 padding: floor(@basefontsize/4);
400 400 top: 0;
401 401 left: 0;
402 402 }
403 403
404 404 span{
405 405 color: @text-color;
406 406 z-index: 0;
407 407 min-width: 20px;
408 408 }
409 409
410 410 b {
411 411 z-index: 1;
412 412 overflow: hidden;
413 413 background-color: @rcblue;
414 414 color: #FFF;
415 415 text-decoration: none;
416 416 }
417 417
418 418 }
419 419
420 420 // Changesets
421 421 #changesets.rctable {
422 422
423 423 // td must be fixed height for graph
424 424 td {
425 425 height: 32px;
426 426 padding: 0 1em 0 0;
427 427 vertical-align: middle;
428 428 white-space: nowrap;
429 429
430 430 &.td-description {
431 431 white-space: normal;
432 432 }
433 433
434 434 &.expand_commit {
435 435 padding-right: 0;
436 436 }
437 437 }
438 438 }
439 439
440 440 // Compare
441 441 table.compare_view_commits {
442 442 margin-top: @space;
443 443
444 444 td.td-time {
445 445 padding-left: .5em;
446 446 }
447 447
448 // special case to not show hover actions on hidden indicator
449 tr.compare_select_hidden:hover {
450 cursor: inherit;
451
452 td {
453 background-color: inherit;
454 }
455 }
456
448 457 tr:hover {
449 458 cursor: pointer;
450 459
451 460 td {
452 461 background-color: lighten(@alert4,25%);
453 462 }
454 463 }
464
465
455 466 }
456 467
457 468 .file_history {
458 469 td.td-actions {
459 470 text-align: right;
460 471 }
461 472 }
462 473
463 474 .compare_view_files {
464 475
465 476 td.td-actions {
466 477 text-align: right;
467 478 }
468 479
469 480 .flag_status {
470 481 margin: 0 0 0 5px;
471 482 }
472 483
473 484 td.injected_diff {
474 485
475 486 .code-difftable {
476 487 border:none;
477 488 }
478 489
479 490 .diff-container {
480 491 border: @border-thickness solid @border-default-color;
481 492 .border-radius(@border-radius);
482 493 }
483 494
484 495 div.diffblock {
485 496 border:none;
486 497 }
487 498
488 499 div.code-body {
489 500 max-width: 1152px;
490 501 }
491 502 }
492 503
493 504 .rctable {
494 505
495 506 td {
496 507 padding-top: @space;
497 508 }
498 509
499 510 &:first-child td {
500 511 padding-top: 0;
501 512 }
502 513 }
503 514
504 515 .comment-bubble,
505 516 .show_comments {
506 517 float: right;
507 518 visibility: hidden;
508 519 padding: 0 1em 0 0;
509 520 }
510 521
511 522 .injected_diff {
512 523 padding-bottom: @padding;
513 524 }
514 525 }
515 526
516 527 // Gist List
517 528 #gist_list_table {
518 529 td {
519 530 vertical-align: middle;
520 531
521 532 div{
522 533 display: inline-block;
523 534 vertical-align: middle;
524 535 }
525 536
526 537 img{
527 538 vertical-align: middle;
528 539 }
529 540 }
530 541 }
@@ -1,640 +1,651 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 // returns a node from given html;
29 29 var fromHTML = function(html){
30 30 var _html = document.createElement('element');
31 31 _html.innerHTML = html;
32 32 return _html;
33 33 };
34 34
35 35 var tableTr = function(cls, body){
36 36 var _el = document.createElement('div');
37 37 var _body = $(body).attr('id');
38 38 var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
39 39 var id = 'comment-tr-{0}'.format(comment_id);
40 40 var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
41 41 '<td class="add-comment-line tooltip tooltip" title="Add Comment"><span class="add-comment-content"></span></td>'+
42 42 '<td></td>'+
43 43 '<td></td>'+
44 44 '<td></td>'+
45 45 '<td>{2}</td>'+
46 46 '</tr></tbody></table>').format(id, cls, body);
47 47 $(_el).html(_html);
48 48 return _el.children[0].children[0].children[0];
49 49 };
50 50
51 51 function bindDeleteCommentButtons() {
52 52 $('.delete-comment').one('click', function() {
53 53 var comment_id = $(this).data("comment-id");
54 54
55 55 if (comment_id){
56 56 deleteComment(comment_id);
57 57 }
58 58 });
59 59 }
60 60
61 61 var deleteComment = function(comment_id) {
62 62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
63 63 var postData = {
64 64 '_method': 'delete',
65 65 'csrf_token': CSRF_TOKEN
66 66 };
67 67
68 68 var success = function(o) {
69 69 window.location.reload();
70 70 };
71 71 ajaxPOST(url, postData, success);
72 72 };
73 73
74 74
75 75 var bindToggleButtons = function() {
76 76 $('.comment-toggle').on('click', function() {
77 77 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
78 78 });
79 79 };
80 80
81 81 var linkifyComments = function(comments) {
82 82 /* TODO: dan: remove this - it should no longer needed */
83 83 for (var i = 0; i < comments.length; i++) {
84 84 var comment_id = $(comments[i]).data('comment-id');
85 85 var prev_comment_id = $(comments[i - 1]).data('comment-id');
86 86 var next_comment_id = $(comments[i + 1]).data('comment-id');
87 87
88 88 // place next/prev links
89 89 if (prev_comment_id) {
90 90 $('#prev_c_' + comment_id).show();
91 91 $('#prev_c_' + comment_id + " a.arrow_comment_link").attr(
92 92 'href', '#comment-' + prev_comment_id).removeClass('disabled');
93 93 }
94 94 if (next_comment_id) {
95 95 $('#next_c_' + comment_id).show();
96 96 $('#next_c_' + comment_id + " a.arrow_comment_link").attr(
97 97 'href', '#comment-' + next_comment_id).removeClass('disabled');
98 98 }
99 99 // place a first link to the total counter
100 100 if (i === 0) {
101 101 $('#inline-comments-counter').attr('href', '#comment-' + comment_id);
102 102 }
103 103 }
104 104
105 105 };
106 106
107 107
108 108 /* Comment form for main and inline comments */
109 109 var CommentForm = (function() {
110 110 "use strict";
111 111
112 112 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions) {
113 113
114 114 this.withLineNo = function(selector) {
115 115 var lineNo = this.lineNo;
116 116 if (lineNo === undefined) {
117 117 return selector
118 118 } else {
119 119 return selector + '_' + lineNo;
120 120 }
121 121 };
122 122
123 123 this.commitId = commitId;
124 124 this.pullRequestId = pullRequestId;
125 125 this.lineNo = lineNo;
126 126 this.initAutocompleteActions = initAutocompleteActions;
127 127
128 128 this.previewButton = this.withLineNo('#preview-btn');
129 129 this.previewContainer = this.withLineNo('#preview-container');
130 130
131 131 this.previewBoxSelector = this.withLineNo('#preview-box');
132 132
133 133 this.editButton = this.withLineNo('#edit-btn');
134 134 this.editContainer = this.withLineNo('#edit-container');
135 135
136 136 this.cancelButton = this.withLineNo('#cancel-btn');
137 137
138 138 this.statusChange = '#change_status';
139 139 this.cmBox = this.withLineNo('#text');
140 140 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
141 141
142 142 this.submitForm = formElement;
143 143 this.submitButton = $(this.submitForm).find('input[type="submit"]');
144 144 this.submitButtonText = this.submitButton.val();
145 145
146 146 this.previewUrl = pyroutes.url('changeset_comment_preview',
147 147 {'repo_name': templateContext.repo_name});
148 148
149 149 // based on commitId, or pullReuqestId decide where do we submit
150 150 // out data
151 151 if (this.commitId){
152 152 this.submitUrl = pyroutes.url('changeset_comment',
153 153 {'repo_name': templateContext.repo_name,
154 154 'revision': this.commitId});
155 155
156 156 } else if (this.pullRequestId) {
157 157 this.submitUrl = pyroutes.url('pullrequest_comment',
158 158 {'repo_name': templateContext.repo_name,
159 159 'pull_request_id': this.pullRequestId});
160 160
161 161 } else {
162 162 throw new Error(
163 163 'CommentForm requires pullRequestId, or commitId to be specified.')
164 164 }
165 165
166 166 this.getCmInstance = function(){
167 167 return this.cm
168 168 };
169 169
170 170 var self = this;
171 171
172 172 this.getCommentStatus = function() {
173 173 return $(this.submitForm).find(this.statusChange).val();
174 174 };
175 175
176 176 this.isAllowedToSubmit = function() {
177 177 return !$(this.submitButton).prop('disabled');
178 178 };
179 179
180 180 this.initStatusChangeSelector = function(){
181 181 var formatChangeStatus = function(state, escapeMarkup) {
182 182 var originalOption = state.element;
183 183 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
184 184 '<span>' + escapeMarkup(state.text) + '</span>';
185 185 };
186 186 var formatResult = function(result, container, query, escapeMarkup) {
187 187 return formatChangeStatus(result, escapeMarkup);
188 188 };
189 189
190 190 var formatSelection = function(data, container, escapeMarkup) {
191 191 return formatChangeStatus(data, escapeMarkup);
192 192 };
193 193
194 194 $(this.submitForm).find(this.statusChange).select2({
195 195 placeholder: _gettext('Status Review'),
196 196 formatResult: formatResult,
197 197 formatSelection: formatSelection,
198 198 containerCssClass: "drop-menu status_box_menu",
199 199 dropdownCssClass: "drop-menu-dropdown",
200 200 dropdownAutoWidth: true,
201 201 minimumResultsForSearch: -1
202 202 });
203 203 $(this.submitForm).find(this.statusChange).on('change', function() {
204 204 var status = self.getCommentStatus();
205 205 if (status && !self.lineNo) {
206 206 $(self.submitButton).prop('disabled', false);
207 207 }
208 208 //todo, fix this name
209 209 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
210 210 self.cm.setOption('placeholder', placeholderText);
211 211 })
212 212 };
213 213
214 214 // reset the comment form into it's original state
215 215 this.resetCommentFormState = function(content) {
216 216 content = content || '';
217 217
218 218 $(this.editContainer).show();
219 219 $(this.editButton).hide();
220 220
221 221 $(this.previewContainer).hide();
222 222 $(this.previewButton).show();
223 223
224 224 this.setActionButtonsDisabled(true);
225 225 self.cm.setValue(content);
226 226 self.cm.setOption("readOnly", false);
227 227 };
228 228
229 229 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
230 230 failHandler = failHandler || function() {};
231 231 var postData = toQueryString(postData);
232 232 var request = $.ajax({
233 233 url: url,
234 234 type: 'POST',
235 235 data: postData,
236 236 headers: {'X-PARTIAL-XHR': true}
237 237 })
238 238 .done(function(data) {
239 239 successHandler(data);
240 240 })
241 241 .fail(function(data, textStatus, errorThrown){
242 242 alert(
243 243 "Error while submitting comment.\n" +
244 244 "Error code {0} ({1}).".format(data.status, data.statusText));
245 245 failHandler()
246 246 });
247 247 return request;
248 248 };
249 249
250 250 // overwrite a submitHandler, we need to do it for inline comments
251 251 this.setHandleFormSubmit = function(callback) {
252 252 this.handleFormSubmit = callback;
253 253 };
254 254
255 255 // default handler for for submit for main comments
256 256 this.handleFormSubmit = function() {
257 257 var text = self.cm.getValue();
258 258 var status = self.getCommentStatus();
259 259
260 260 if (text === "" && !status) {
261 261 return;
262 262 }
263 263
264 264 var excludeCancelBtn = false;
265 265 var submitEvent = true;
266 266 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
267 267 self.cm.setOption("readOnly", true);
268 268 var postData = {
269 269 'text': text,
270 270 'changeset_status': status,
271 271 'csrf_token': CSRF_TOKEN
272 272 };
273 273
274 274 var submitSuccessCallback = function(o) {
275 275 if (status) {
276 276 location.reload(true);
277 277 } else {
278 278 $('#injected_page_comments').append(o.rendered_text);
279 279 self.resetCommentFormState();
280 280 bindDeleteCommentButtons();
281 281 timeagoActivate();
282 282 }
283 283 };
284 284 var submitFailCallback = function(){
285 285 self.resetCommentFormState(text)
286 286 };
287 287 self.submitAjaxPOST(
288 288 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
289 289 };
290 290
291 291 this.previewSuccessCallback = function(o) {
292 292 $(self.previewBoxSelector).html(o);
293 293 $(self.previewBoxSelector).removeClass('unloaded');
294 294
295 295 // swap buttons
296 296 $(self.previewButton).hide();
297 297 $(self.editButton).show();
298 298
299 299 // unlock buttons
300 300 self.setActionButtonsDisabled(false);
301 301 };
302 302
303 303 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
304 304 excludeCancelBtn = excludeCancelBtn || false;
305 305 submitEvent = submitEvent || false;
306 306
307 307 $(this.editButton).prop('disabled', state);
308 308 $(this.previewButton).prop('disabled', state);
309 309
310 310 if (!excludeCancelBtn) {
311 311 $(this.cancelButton).prop('disabled', state);
312 312 }
313 313
314 314 var submitState = state;
315 315 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
316 316 // if the value of commit review status is set, we allow
317 317 // submit button, but only on Main form, lineNo means inline
318 318 submitState = false
319 319 }
320 320 $(this.submitButton).prop('disabled', submitState);
321 321 if (submitEvent) {
322 322 $(this.submitButton).val(_gettext('Submitting...'));
323 323 } else {
324 324 $(this.submitButton).val(this.submitButtonText);
325 325 }
326 326
327 327 };
328 328
329 329 // lock preview/edit/submit buttons on load, but exclude cancel button
330 330 var excludeCancelBtn = true;
331 331 this.setActionButtonsDisabled(true, excludeCancelBtn);
332 332
333 333 // anonymous users don't have access to initialized CM instance
334 334 if (this.cm !== undefined){
335 335 this.cm.on('change', function(cMirror) {
336 336 if (cMirror.getValue() === "") {
337 337 self.setActionButtonsDisabled(true, excludeCancelBtn)
338 338 } else {
339 339 self.setActionButtonsDisabled(false, excludeCancelBtn)
340 340 }
341 341 });
342 342 }
343 343
344 344 $(this.editButton).on('click', function(e) {
345 345 e.preventDefault();
346 346
347 347 $(self.previewButton).show();
348 348 $(self.previewContainer).hide();
349 349 $(self.editButton).hide();
350 350 $(self.editContainer).show();
351 351
352 352 });
353 353
354 354 $(this.previewButton).on('click', function(e) {
355 355 e.preventDefault();
356 356 var text = self.cm.getValue();
357 357
358 358 if (text === "") {
359 359 return;
360 360 }
361 361
362 362 var postData = {
363 363 'text': text,
364 364 'renderer': DEFAULT_RENDERER,
365 365 'csrf_token': CSRF_TOKEN
366 366 };
367 367
368 368 // lock ALL buttons on preview
369 369 self.setActionButtonsDisabled(true);
370 370
371 371 $(self.previewBoxSelector).addClass('unloaded');
372 372 $(self.previewBoxSelector).html(_gettext('Loading ...'));
373 373 $(self.editContainer).hide();
374 374 $(self.previewContainer).show();
375 375
376 376 // by default we reset state of comment preserving the text
377 377 var previewFailCallback = function(){
378 378 self.resetCommentFormState(text)
379 379 };
380 380 self.submitAjaxPOST(
381 381 self.previewUrl, postData, self.previewSuccessCallback, previewFailCallback);
382 382
383 383 });
384 384
385 385 $(this.submitForm).submit(function(e) {
386 386 e.preventDefault();
387 387 var allowedToSubmit = self.isAllowedToSubmit();
388 388 if (!allowedToSubmit){
389 389 return false;
390 390 }
391 391 self.handleFormSubmit();
392 392 });
393 393
394 394 }
395 395
396 396 return CommentForm;
397 397 })();
398 398
399 399 var CommentsController = function() { /* comments controller */
400 400 var self = this;
401 401
402 402 this.cancelComment = function(node) {
403 403 var $node = $(node);
404 404 var $td = $node.closest('td');
405 405 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
406 406 return false;
407 407 };
408 408
409 409 this.getLineNumber = function(node) {
410 410 var $node = $(node);
411 411 return $node.closest('td').attr('data-line-number');
412 412 };
413 413
414 this.scrollToComment = function(node, offset) {
414 this.scrollToComment = function(node, offset, outdated) {
415 var outdated = outdated || false;
416 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
417
415 418 if (!node) {
416 419 node = $('.comment-selected');
417 420 if (!node.length) {
418 421 node = $('comment-current')
419 422 }
420 423 }
421 $comment = $(node).closest('.comment-current');
422 $comments = $('.comment-current');
424 $comment = $(node).closest(klass);
425 $comments = $(klass);
423 426
424 427 $('.comment-selected').removeClass('comment-selected');
425 428
426 var nextIdx = $('.comment-current').index($comment) + offset;
429 var nextIdx = $(klass).index($comment) + offset;
427 430 if (nextIdx >= $comments.length) {
428 431 nextIdx = 0;
429 432 }
430 var $next = $('.comment-current').eq(nextIdx);
433 var $next = $(klass).eq(nextIdx);
431 434 var $cb = $next.closest('.cb');
432 435 $cb.removeClass('cb-collapsed');
433 436
434 437 var $filediffCollapseState = $cb.closest('.filediff').prev();
435 438 $filediffCollapseState.prop('checked', false);
436 439 $next.addClass('comment-selected');
437 440 scrollToElement($next);
438 441 return false;
439 442 };
440 443
441 444 this.nextComment = function(node) {
442 445 return self.scrollToComment(node, 1);
443 446 };
444 447
445 448 this.prevComment = function(node) {
446 449 return self.scrollToComment(node, -1);
447 450 };
448 451
452 this.nextOutdatedComment = function(node) {
453 return self.scrollToComment(node, 1, true);
454 };
455
456 this.prevOutdatedComment = function(node) {
457 return self.scrollToComment(node, -1, true);
458 };
459
449 460 this.deleteComment = function(node) {
450 461 if (!confirm(_gettext('Delete this comment?'))) {
451 462 return false;
452 463 }
453 464 var $node = $(node);
454 465 var $td = $node.closest('td');
455 466 var $comment = $node.closest('.comment');
456 467 var comment_id = $comment.attr('data-comment-id');
457 468 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
458 469 var postData = {
459 470 '_method': 'delete',
460 471 'csrf_token': CSRF_TOKEN
461 472 };
462 473
463 474 $comment.addClass('comment-deleting');
464 475 $comment.hide('fast');
465 476
466 477 var success = function(response) {
467 478 $comment.remove();
468 479 return false;
469 480 };
470 481 var failure = function(data, textStatus, xhr) {
471 482 alert("error processing request: " + textStatus);
472 483 $comment.show('fast');
473 484 $comment.removeClass('comment-deleting');
474 485 return false;
475 486 };
476 487 ajaxPOST(url, postData, success, failure);
477 488 };
478 489
479 490 this.toggleWideMode = function (node) {
480 491 if ($('#content').hasClass('wrapper')) {
481 492 $('#content').removeClass("wrapper");
482 493 $('#content').addClass("wide-mode-wrapper");
483 494 $(node).addClass('btn-success');
484 495 } else {
485 496 $('#content').removeClass("wide-mode-wrapper");
486 497 $('#content').addClass("wrapper");
487 498 $(node).removeClass('btn-success');
488 499 }
489 500 return false;
490 501 };
491 502
492 503 this.toggleComments = function(node, show) {
493 504 var $filediff = $(node).closest('.filediff');
494 505 if (show === true) {
495 506 $filediff.removeClass('hide-comments');
496 507 } else if (show === false) {
497 508 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
498 509 $filediff.addClass('hide-comments');
499 510 } else {
500 511 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
501 512 $filediff.toggleClass('hide-comments');
502 513 }
503 514 return false;
504 515 };
505 516
506 517 this.toggleLineComments = function(node) {
507 518 self.toggleComments(node, true);
508 519 var $node = $(node);
509 520 $node.closest('tr').toggleClass('hide-line-comments');
510 521 };
511 522
512 523 this.createComment = function(node) {
513 524 var $node = $(node);
514 525 var $td = $node.closest('td');
515 526 var $form = $td.find('.comment-inline-form');
516 527
517 528 if (!$form.length) {
518 529 var tmpl = $('#cb-comment-inline-form-template').html();
519 530 var $filediff = $node.closest('.filediff');
520 531 $filediff.removeClass('hide-comments');
521 532 var f_path = $filediff.attr('data-f-path');
522 533 var lineno = self.getLineNumber(node);
523 534 tmpl = tmpl.format(f_path, lineno);
524 535 $form = $(tmpl);
525 536
526 537 var $comments = $td.find('.inline-comments');
527 538 if (!$comments.length) {
528 539 $comments = $(
529 540 $('#cb-comments-inline-container-template').html());
530 541 $td.append($comments);
531 542 }
532 543
533 544 $td.find('.cb-comment-add-button').before($form);
534 545
535 546 var pullRequestId = templateContext.pull_request_data.pull_request_id;
536 547 var commitId = templateContext.commit_data.commit_id;
537 548 var _form = $form[0];
538 549 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
539 550 var cm = commentForm.getCmInstance();
540 551
541 552 // set a CUSTOM submit handler for inline comments.
542 553 commentForm.setHandleFormSubmit(function(o) {
543 554 var text = commentForm.cm.getValue();
544 555
545 556 if (text === "") {
546 557 return;
547 558 }
548 559
549 560 if (lineno === undefined) {
550 561 alert('missing line !');
551 562 return;
552 563 }
553 564 if (f_path === undefined) {
554 565 alert('missing file path !');
555 566 return;
556 567 }
557 568
558 569 var excludeCancelBtn = false;
559 570 var submitEvent = true;
560 571 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
561 572 commentForm.cm.setOption("readOnly", true);
562 573 var postData = {
563 574 'text': text,
564 575 'f_path': f_path,
565 576 'line': lineno,
566 577 'csrf_token': CSRF_TOKEN
567 578 };
568 579 var submitSuccessCallback = function(json_data) {
569 580 $form.remove();
570 581 try {
571 582 var html = json_data.rendered_text;
572 583 var lineno = json_data.line_no;
573 584 var target_id = json_data.target_id;
574 585
575 586 $comments.find('.cb-comment-add-button').before(html);
576 587
577 588 } catch (e) {
578 589 console.error(e);
579 590 }
580 591
581 592 // re trigger the linkification of next/prev navigation
582 593 linkifyComments($('.inline-comment-injected'));
583 594 timeagoActivate();
584 595 bindDeleteCommentButtons();
585 596 commentForm.setActionButtonsDisabled(false);
586 597
587 598 };
588 599 var submitFailCallback = function(){
589 600 commentForm.resetCommentFormState(text)
590 601 };
591 602 commentForm.submitAjaxPOST(
592 603 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
593 604 });
594 605
595 606 setTimeout(function() {
596 607 // callbacks
597 608 if (cm !== undefined) {
598 609 cm.focus();
599 610 }
600 611 }, 10);
601 612
602 613 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
603 614 form: _form,
604 615 parent: $td[0],
605 616 lineno: lineno,
606 617 f_path: f_path}
607 618 );
608 619 }
609 620
610 621 $form.addClass('comment-inline-form-open');
611 622 };
612 623
613 624 this.renderInlineComments = function(file_comments) {
614 625 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
615 626
616 627 for (var i = 0; i < file_comments.length; i++) {
617 628 var box = file_comments[i];
618 629
619 630 var target_id = $(box).attr('target_id');
620 631
621 632 // actually comments with line numbers
622 633 var comments = box.children;
623 634
624 635 for (var j = 0; j < comments.length; j++) {
625 636 var data = {
626 637 'rendered_text': comments[j].outerHTML,
627 638 'line_no': $(comments[j]).attr('line'),
628 639 'target_id': target_id
629 640 };
630 641 }
631 642 }
632 643
633 644 // since order of injection is random, we're now re-iterating
634 645 // from correct order and filling in links
635 646 linkifyComments($('.inline-comment-injected'));
636 647 bindDeleteCommentButtons();
637 648 firefoxAnchorFix();
638 649 };
639 650
640 651 };
@@ -1,418 +1,418 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 ${ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='changelog')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31
32 32 <div class="box">
33 33 <div class="title">
34 34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 35 <ul class="links">
36 36 <li>
37 37 <a href="#" class="btn btn-small" id="rev_range_container" style="display:none;"></a>
38 38 %if c.rhodecode_db_repo.fork:
39 39 <span>
40 40 <a id="compare_fork_button"
41 41 title="${_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name)}"
42 42 class="btn btn-small"
43 43 href="${h.url('compare_url',
44 44 repo_name=c.rhodecode_db_repo.fork.repo_name,
45 45 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
46 46 source_ref=c.rhodecode_db_repo.landing_rev[1],
47 47 target_repo=c.repo_name,
48 48 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
49 49 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
50 50 merge=1)
51 51 }">
52 52 <i class="icon-loop"></i>
53 53 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
54 54 </a>
55 55 </span>
56 56 %endif
57 57
58 58 ## pr open link
59 59 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
60 60 <span>
61 61 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.url('pullrequest_home',repo_name=c.repo_name)}">
62 62 ${_('Open new pull request')}
63 63 </a>
64 64 </span>
65 65 %endif
66 66
67 67 ## clear selection
68 68 <div title="${_('Clear selection')}" class="btn" id="rev_range_clear" style="display:none">
69 69 ${_('Clear selection')}
70 70 </div>
71 71
72 72 </li>
73 73 </ul>
74 74 </div>
75 75
76 76 % if c.pagination:
77 77
78 78 <div class="graph-header">
79 79 <div id="filter_changelog">
80 80 ${h.hidden('branch_filter')}
81 81 %if c.selected_name:
82 82 <div class="btn btn-default" id="clear_filter" >
83 83 ${_('Clear filter')}
84 84 </div>
85 85 %endif
86 86 </div>
87 87 ${self.breadcrumbs('breadcrumbs_light')}
88 88 </div>
89 89
90 90 <div id="graph">
91 91 <div class="graph-col-wrapper">
92 92 <div id="graph_nodes">
93 93 <div id="graph_canvas" data-graph='${c.jsdata|n}'></div>
94 94 </div>
95 95 <div id="graph_content" class="main-content graph_full_width">
96 96
97 97 <div class="table">
98 98 <table id="changesets" class="rctable">
99 99 <tr>
100 100 ## checkbox
101 101 <th></th>
102 102 <th colspan="2"></th>
103 103
104 104 <th>${_('Commit')}</th>
105 105 ## commit message expand arrow
106 106 <th></th>
107 107 <th>${_('Commit Message')}</th>
108 108
109 109 <th>${_('Age')}</th>
110 110 <th>${_('Author')}</th>
111 111
112 112 <th>${_('Refs')}</th>
113 113 </tr>
114 114 <tbody>
115 115 %for cnt,commit in enumerate(c.pagination):
116 116 <tr id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
117 117
118 118 <td class="td-checkbox">
119 119 ${h.checkbox(commit.raw_id,class_="commit-range")}
120 120 </td>
121 121 <td class="td-status">
122 122
123 123 %if c.statuses.get(commit.raw_id):
124 124 <div class="changeset-status-ico">
125 125 %if c.statuses.get(commit.raw_id)[2]:
126 126 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (h.commit_status_lbl(c.statuses.get(commit.raw_id)[0]), c.statuses.get(commit.raw_id)[2])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(commit.raw_id)[3],pull_request_id=c.statuses.get(commit.raw_id)[2])}">
127 127 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
128 128 </a>
129 129 %else:
130 130 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(commit.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
131 131 <div class="${'flag_status %s' % c.statuses.get(commit.raw_id)[0]}"></div>
132 132 </a>
133 133 %endif
134 134 </div>
135 135 %else:
136 136 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
137 137 %endif
138 138 </td>
139 139 <td class="td-comments comments-col">
140 140 %if c.comments.get(commit.raw_id):
141 141 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id,anchor='comment-%s' % c.comments[commit.raw_id][0].comment_id)}">
142 <i class="icon-comment icon-comment-colored"></i> ${len(c.comments[commit.raw_id])}
142 <i class="icon-comment"></i> ${len(c.comments[commit.raw_id])}
143 143 </a>
144 144 %endif
145 145 </td>
146 146 <td class="td-hash">
147 147 <code>
148 148 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">
149 149 <span class="commit_hash">${h.show_id(commit)}</span>
150 150 </a>
151 151 </code>
152 152 </td>
153 153 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}">
154 154 <div class="show_more_col">
155 155 <i class="show_more"></i>&nbsp;
156 156 </div>
157 157 </td>
158 158 <td class="td-description mid">
159 159 <div class="log-container truncate-wrap">
160 160 <div class="message truncate" id="c-${commit.raw_id}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
161 161 </div>
162 162 </td>
163 163
164 164 <td class="td-time">
165 165 ${h.age_component(commit.date)}
166 166 </td>
167 167 <td class="td-user">
168 168 ${self.gravatar_with_user(commit.author)}
169 169 </td>
170 170
171 171 <td class="td-tags tags-col">
172 172 <div id="t-${commit.raw_id}">
173 173 ## branch
174 174 %if commit.branch:
175 175 <span class="branchtag tag" title="${_('Branch %s') % commit.branch}">
176 176 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=commit.branch)}"><i class="icon-code-fork"></i>${h.shorter(commit.branch)}</a>
177 177 </span>
178 178 %endif
179 179
180 180 ## bookmarks
181 181 %if h.is_hg(c.rhodecode_repo):
182 182 %for book in commit.bookmarks:
183 183 <span class="tag booktag" title="${_('Bookmark %s') % book}">
184 184 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
185 185 </span>
186 186 %endfor
187 187 %endif
188 188
189 189 ## tags
190 190 %for tag in commit.tags:
191 191 <span class="tagtag tag" title="${_('Tag %s') % tag}">
192 192 <a href="${h.url('files_home',repo_name=c.repo_name,revision=commit.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
193 193 </span>
194 194 %endfor
195 195
196 196 </div>
197 197 </td>
198 198 </tr>
199 199 %endfor
200 200 </tbody>
201 201 </table>
202 202 </div>
203 203 </div>
204 204 </div>
205 205 <div class="pagination-wh pagination-left">
206 206 ${c.pagination.pager('$link_previous ~2~ $link_next')}
207 207 </div>
208 208
209 209 <script type="text/javascript" src="${h.asset('js/jquery.commits-graph.js')}"></script>
210 210 <script type="text/javascript">
211 211 var cache = {};
212 212 $(function(){
213 213
214 214 // Create links to commit ranges when range checkboxes are selected
215 215 var $commitCheckboxes = $('.commit-range');
216 216 // cache elements
217 217 var $commitRangeContainer = $('#rev_range_container');
218 218 var $commitRangeClear = $('#rev_range_clear');
219 219
220 220 var checkboxRangeSelector = function(e){
221 221 var selectedCheckboxes = [];
222 222 for (pos in $commitCheckboxes){
223 223 if($commitCheckboxes[pos].checked){
224 224 selectedCheckboxes.push($commitCheckboxes[pos]);
225 225 }
226 226 }
227 227 var open_new_pull_request = $('#open_new_pull_request');
228 228 if(open_new_pull_request){
229 229 var selected_changes = selectedCheckboxes.length;
230 230 if (selected_changes > 1 || selected_changes == 1 && templateContext.repo_type != 'hg') {
231 231 open_new_pull_request.hide();
232 232 } else {
233 233 if (selected_changes == 1) {
234 234 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
235 235 } else if (selected_changes == 0) {
236 236 open_new_pull_request.html(_gettext('Open new pull request'));
237 237 }
238 238 open_new_pull_request.show();
239 239 }
240 240 }
241 241
242 242 if (selectedCheckboxes.length>0){
243 243 var revEnd = selectedCheckboxes[0].name;
244 244 var revStart = selectedCheckboxes[selectedCheckboxes.length-1].name;
245 245 var url = pyroutes.url('changeset_home',
246 246 {'repo_name': '${c.repo_name}',
247 247 'revision': revStart+'...'+revEnd});
248 248
249 249 var link = (revStart == revEnd)
250 250 ? _gettext('Show selected commit __S')
251 251 : _gettext('Show selected commits __S ... __E');
252 252
253 253 link = link.replace('__S', revStart.substr(0,6));
254 254 link = link.replace('__E', revEnd.substr(0,6));
255 255
256 256 $commitRangeContainer
257 257 .attr('href',url)
258 258 .html(link)
259 259 .show();
260 260
261 261 $commitRangeClear.show();
262 262 var _url = pyroutes.url('pullrequest_home',
263 263 {'repo_name': '${c.repo_name}',
264 264 'commit': revEnd});
265 265 open_new_pull_request.attr('href', _url);
266 266 $('#compare_fork_button').hide();
267 267 } else {
268 268 $commitRangeContainer.hide();
269 269 $commitRangeClear.hide();
270 270
271 271 %if c.branch_name:
272 272 var _url = pyroutes.url('pullrequest_home',
273 273 {'repo_name': '${c.repo_name}',
274 274 'branch':'${c.branch_name}'});
275 275 open_new_pull_request.attr('href', _url);
276 276 %else:
277 277 var _url = pyroutes.url('pullrequest_home',
278 278 {'repo_name': '${c.repo_name}'});
279 279 open_new_pull_request.attr('href', _url);
280 280 %endif
281 281 $('#compare_fork_button').show();
282 282 }
283 283 };
284 284
285 285 $commitCheckboxes.on('click', checkboxRangeSelector);
286 286
287 287 $commitRangeClear.on('click',function(e) {
288 288 $commitCheckboxes.attr('checked', false)
289 289 checkboxRangeSelector();
290 290 e.preventDefault();
291 291 });
292 292
293 293 // make sure the buttons are consistent when navigate back and forth
294 294 checkboxRangeSelector();
295 295
296 296
297 297 var msgs = $('.message');
298 298 // get first element height
299 299 var el = $('#graph_content .container')[0];
300 300 var row_h = el.clientHeight;
301 301 for (var i=0; i < msgs.length; i++) {
302 302 var m = msgs[i];
303 303
304 304 var h = m.clientHeight;
305 305 var pad = $(m).css('padding');
306 306 if (h > row_h) {
307 307 var offset = row_h - (h+12);
308 308 $(m.nextElementSibling).css('display','block');
309 309 $(m.nextElementSibling).css('margin-top',offset+'px');
310 310 }
311 311 }
312 312
313 313 $('.expand_commit').on('click',function(e){
314 314 var target_expand = $(this);
315 315 var cid = target_expand.data('commitId');
316 316
317 317 if (target_expand.hasClass('open')){
318 318 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
319 319 $('#t-'+cid).css({'height': 'auto', 'line-height': '.9em', 'text-overflow': 'ellipsis', 'overflow':'hidden', 'white-space':'nowrap'});
320 320 target_expand.removeClass('open');
321 321 }
322 322 else {
323 323 $('#c-'+cid).css({'height': 'auto', 'white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'});
324 324 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'});
325 325 target_expand.addClass('open');
326 326 }
327 327 // redraw the graph
328 328 graph_options.height = $("#changesets").height();
329 329 $("canvas").remove();
330 330 $("[data-graph]").commits(graph_options);
331 331 });
332 332
333 333 $("#clear_filter").on("click", function() {
334 334 var filter = {'repo_name': '${c.repo_name}'};
335 335 window.location = pyroutes.url('changelog_home', filter);
336 336 });
337 337
338 338 $("#branch_filter").select2({
339 339 'dropdownAutoWidth': true,
340 340 'width': 'resolve',
341 341 'placeholder': "${c.selected_name or _('Filter changelog')}",
342 342 containerCssClass: "drop-menu",
343 343 dropdownCssClass: "drop-menu-dropdown",
344 344 query: function(query){
345 345 var key = 'cache';
346 346 var cached = cache[key] ;
347 347 if(cached) {
348 348 var data = {results: []};
349 349 //filter results
350 350 $.each(cached.results, function(){
351 351 var section = this.text;
352 352 var children = [];
353 353 $.each(this.children, function(){
354 354 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
355 355 children.push({'id': this.id, 'text': this.text, 'type': this.type})
356 356 }
357 357 });
358 358 data.results.push({'text': section, 'children': children});
359 359 query.callback({results: data.results});
360 360 });
361 361 }else{
362 362 $.ajax({
363 363 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
364 364 data: {},
365 365 dataType: 'json',
366 366 type: 'GET',
367 367 success: function(data) {
368 368 cache[key] = data;
369 369 query.callback({results: data.results});
370 370 }
371 371 })
372 372 }
373 373 }
374 374 });
375 375
376 376 $('#branch_filter').on('change', function(e){
377 377 var data = $('#branch_filter').select2('data');
378 378 var selected = data.text;
379 379 var filter = {'repo_name': '${c.repo_name}'};
380 380 if(data.type == 'branch' || data.type == 'branch_closed'){
381 381 filter["branch"] = selected;
382 382 }
383 383 else if (data.type == 'book'){
384 384 filter["bookmark"] = selected;
385 385 }
386 386 window.location = pyroutes.url('changelog_home', filter);
387 387 });
388 388
389 389 // Determine max number of edges per row in graph
390 390 var jsdata = $.parseJSON($("[data-graph]").attr('data-graph'));
391 391 var edgeCount = 1;
392 392 $.each(jsdata, function(i, item){
393 393 $.each(item[2], function(key, value) {
394 394 if (value[1] > edgeCount){
395 395 edgeCount = value[1];
396 396 }
397 397 });
398 398 });
399 399 var x_step = Math.min(18, Math.floor(86 / edgeCount));
400 400 var graph_options = {
401 401 width: 100,
402 402 height: $("#changesets").height(),
403 403 x_step: x_step,
404 404 y_step: 42,
405 405 dotRadius: 3.5,
406 406 lineWidth: 2.5
407 407 };
408 408 $("[data-graph]").commits(graph_options);
409 409
410 410 });
411 411
412 412 </script>
413 413 %else:
414 414 ${_('There are no changes yet')}
415 415 %endif
416 416 </div>
417 417 </div>
418 418 </%def>
@@ -1,136 +1,136 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.html"/>
3 3 %if c.repo_commits:
4 4 <table class="rctable repo_summary table_disp">
5 5 <tr>
6 6
7 7 <th class="status" colspan="2"></th>
8 8 <th>${_('Commit')}</th>
9 9 <th>${_('Commit message')}</th>
10 10 <th>${_('Age')}</th>
11 11 <th>${_('Author')}</th>
12 12 <th>${_('Refs')}</th>
13 13 </tr>
14 14 %for cnt,cs in enumerate(c.repo_commits):
15 15 <tr class="parity${cnt%2}">
16 16
17 17 <td class="td-status">
18 18 %if c.statuses.get(cs.raw_id):
19 19 <div class="changeset-status-ico shortlog">
20 20 %if c.statuses.get(cs.raw_id)[2]:
21 21 <a class="tooltip" title="${_('Commit status: %s\nClick to open associated pull request #%s') % (c.statuses.get(cs.raw_id)[0], c.statuses.get(cs.raw_id)[2])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
22 22 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
23 23 </a>
24 24 %else:
25 25 <a class="tooltip" title="${_('Commit status: %s') % h.commit_status_lbl(c.statuses.get(cs.raw_id)[0])}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
26 26 <div class="${'flag_status %s' % c.statuses.get(cs.raw_id)[0]}"></div>
27 27 </a>
28 28 %endif
29 29 </div>
30 30 %else:
31 31 <div class="tooltip flag_status not_reviewed" title="${_('Commit status: Not Reviewed')}"></div>
32 32 %endif
33 33 </td>
34 34 <td class="td-comments">
35 35 %if c.comments.get(cs.raw_id,[]):
36 36 <a title="${_('Commit has comments')}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
37 <i class="icon-comment icon-comment-colored"></i> ${len(c.comments[cs.raw_id])}
37 <i class="icon-comment"></i> ${len(c.comments[cs.raw_id])}
38 38 </a>
39 39 %endif
40 40 </td>
41 41 <td class="td-commit">
42 42 <pre><a href="${h.url('changeset_home', repo_name=c.repo_name, revision=cs.raw_id)}">${h.show_id(cs)}</a></pre>
43 43 </td>
44 44
45 45 <td class="td-description mid">
46 46 <div class="log-container truncate-wrap">
47 47 <div class="message truncate" id="c-${cs.raw_id}">${h.urlify_commit_message(cs.message, c.repo_name)}</div>
48 48 </div>
49 49 </td>
50 50
51 51 <td class="td-time">
52 52 ${h.age_component(cs.date)}
53 53 </td>
54 54 <td class="td-user author">
55 55 ${base.gravatar_with_user(cs.author)}
56 56 </td>
57 57
58 58 <td class="td-tags">
59 59 <div class="autoexpand">
60 60 %if h.is_hg(c.rhodecode_repo):
61 61 %for book in cs.bookmarks:
62 62 <span class="booktag tag" title="${_('Bookmark %s') % book}">
63 63 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-bookmark"></i>${h.shorter(book)}</a>
64 64 </span>
65 65 %endfor
66 66 %endif
67 67 ## tags
68 68 %for tag in cs.tags:
69 69 <span class="tagtag tag" title="${_('Tag %s') % tag}">
70 70 <a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}"><i class="icon-tag"></i>${h.shorter(tag)}</a>
71 71 </span>
72 72 %endfor
73 73
74 74 ## branch
75 75 %if cs.branch:
76 76 <span class="branchtag tag" title="${_('Branch %s') % cs.branch}">
77 77 <a href="${h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch)}"><i class="icon-code-fork"></i>${h.shorter(cs.branch)}</a>
78 78 </span>
79 79 %endif
80 80 </div>
81 81 </td>
82 82 </tr>
83 83 %endfor
84 84
85 85 </table>
86 86
87 87 <script type="text/javascript">
88 88 $(document).pjax('#shortlog_data .pager_link','#shortlog_data', {timeout: 2000, scrollTo: false });
89 89 $(document).on('pjax:success', function(){ timeagoActivate(); });
90 90 </script>
91 91
92 92 <div class="pagination-wh pagination-left">
93 93 ${c.repo_commits.pager('$link_previous ~2~ $link_next')}
94 94 </div>
95 95 %else:
96 96
97 97 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
98 98 <div class="quick_start">
99 99 <div class="fieldset">
100 100 <div class="left-label">${_('Add or upload files directly via RhodeCode:')}</div>
101 101 <div class="right-content">
102 102 <div id="add_node_id" class="add_node">
103 103 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}" class="btn btn-default">${_('Add New File')}</a>
104 104 </div>
105 105 </div>
106 106 %endif
107 107 </div>
108 108
109 109 %if not h.is_svn(c.rhodecode_repo):
110 110 <div class="fieldset">
111 111 <div class="left-label">${_('Push new repo:')}</div>
112 112 <div class="right-content">
113 113 <pre>
114 114 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
115 115 ${c.rhodecode_repo.alias} add README # add first file
116 116 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
117 117 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
118 118 </pre>
119 119 </div>
120 120 </div>
121 121 <div class="fieldset">
122 122 <div class="left-label">${_('Existing repository?')}</div>
123 123 <div class="right-content">
124 124 <pre>
125 125 %if h.is_git(c.rhodecode_repo):
126 126 git remote add origin ${c.clone_repo_url}
127 127 git push -u origin master
128 128 %else:
129 129 hg push ${c.clone_repo_url}
130 130 %endif
131 131 </pre>
132 132 </div>
133 133 </div>
134 134 %endif
135 135 </div>
136 136 %endif
@@ -1,208 +1,209 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.html"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10
9 11 <div class="comment
10 12 ${'comment-inline' if inline else ''}
11 ${'comment-outdated' if comment.outdated_at_version(getattr(c, 'at_version', None)) else 'comment-current'}"
13 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
12 14 id="comment-${comment.comment_id}"
13 15 line="${comment.line_no}"
14 16 data-comment-id="${comment.comment_id}"
15 style="${'display: none;' if comment.outdated_at_version(getattr(c, 'at_version', None)) else ''}">
17 style="${'display: none;' if outdated_at_ver else ''}">
16 18
17 19 <div class="meta">
18 20 <div class="author">
19 21 ${base.gravatar_with_user(comment.author.email, 16)}
20 22 </div>
21 23 <div class="date">
22 24 ${h.age_component(comment.modified_at, time_is_local=True)}
23 25 </div>
24 26 <div class="status-change">
25 27 % if comment.pull_request:
26 28 % if comment.outdated:
27 29 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
28 30 ${_('Outdated comment from pull request version {}').format(comment.pull_request_version_id)}
29 31 </a>
30 32 % else:
31 33 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
32 34 %if comment.status_change:
33 35 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
34 36 %else:
35 37 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
36 38 %endif
37 39 </a>
38 40 % endif
39 41 % else:
40 42 % if comment.status_change:
41 43 ${_('Status change on commit')}:
42 44 % else:
43 45 ${_('Comment on commit')}
44 46 % endif
45 47 % endif
46 48 </div>
47 49 %if comment.status_change:
48 50 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
49 51 <div title="${_('Commit status')}" class="changeset-status-lbl">
50 52 ${comment.status_change[0].status_lbl}
51 53 </div>
52 54 %endif
53 55 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
54 56
55
56 57 <div class="comment-links-block">
57
58 58 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
59 ## only super-admin, repo admin OR comment owner can delete
60 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
59 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
60 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
61 61 ## permissions to delete
62 62 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
63 63 ## TODO: dan: add edit comment here
64 64 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
65 65 %else:
66 66 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
67 67 %endif
68 68 %else:
69 69 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
70 70 %endif
71 %if not comment.outdated_at_version(getattr(c, 'at_version', None)):
71
72 %if not outdated_at_ver:
72 73 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
73 74 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
74 75 %endif
75 76
76 77 </div>
77 78 </div>
78 79 <div class="text">
79 80 ${comment.render(mentions=True)|n}
80 81 </div>
81 82
82 83 </div>
83 84 </%def>
84 85 ## generate main comments
85 86 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
86 87 <div id="comments">
87 88 %for comment in c.comments:
88 89 <div id="comment-tr-${comment.comment_id}">
89 90 ## only render comments that are not from pull request, or from
90 91 ## pull request and a status change
91 92 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
92 93 ${comment_block(comment)}
93 94 %endif
94 95 </div>
95 96 %endfor
96 97 ## to anchor ajax comments
97 98 <div id="injected_page_comments"></div>
98 99 </div>
99 100 </%def>
100 101
101 102 ## MAIN COMMENT FORM
102 103 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
103 104
104 105 %if is_compare:
105 106 <% form_id = "comments_form_compare" %>
106 107 %else:
107 108 <% form_id = "comments_form" %>
108 109 %endif
109 110
110 111
111 112 %if is_pull_request:
112 113 <div class="pull-request-merge">
113 114 %if c.allowed_to_merge:
114 115 <div class="pull-request-wrap">
115 116 <div class="pull-right">
116 117 ${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')}
117 118 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
118 119 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
119 120 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
120 121 ${h.end_form()}
121 122 </div>
122 123 </div>
123 124 %else:
124 125 <div class="pull-request-wrap">
125 126 <div class="pull-right">
126 127 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
127 128 </div>
128 129 </div>
129 130 %endif
130 131 </div>
131 132 %endif
132 133 <div class="comments">
133 134 %if c.rhodecode_user.username != h.DEFAULT_USER:
134 135 <div class="comment-form ac">
135 136 ${h.secure_form(post_url, id_=form_id)}
136 137 <div id="edit-container" class="clearfix">
137 138 <div class="comment-title pull-left">
138 139 %if is_pull_request:
139 140 ${(_('Create a comment on this Pull Request.'))}
140 141 %elif is_compare:
141 142 ${(_('Create comments on this Commit range.'))}
142 143 %else:
143 144 ${(_('Create a comment on this Commit.'))}
144 145 %endif
145 146 </div>
146 147 <div class="comment-help pull-right">
147 148 ${(_('Comments parsed using %s syntax with %s support.') % (
148 149 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
149 150 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
150 151 )
151 152 )|n
152 153 }
153 154 </div>
154 155 <div style="clear: both"></div>
155 156 ${h.textarea('text', class_="comment-block-ta")}
156 157 </div>
157 158
158 159 <div id="preview-container" class="clearfix" style="display: none;">
159 160 <div class="comment-title">
160 161 ${_('Comment preview')}
161 162 </div>
162 163 <div id="preview-box" class="preview-box"></div>
163 164 </div>
164 165
165 166 <div id="comment_form_extras">
166 167 %if form_extras and isinstance(form_extras, (list, tuple)):
167 168 % for form_ex_el in form_extras:
168 169 ${form_ex_el|n}
169 170 % endfor
170 171 %endif
171 172 </div>
172 173 <div class="comment-footer">
173 174 %if change_status:
174 175 <div class="status_box">
175 176 <select id="change_status" name="changeset_status">
176 177 <option></option> # Placeholder
177 178 %for status,lbl in c.commit_statuses:
178 179 <option value="${status}" data-status="${status}">${lbl}</option>
179 180 %if is_pull_request and change_status and status in ('approved', 'rejected'):
180 181 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
181 182 %endif
182 183 %endfor
183 184 </select>
184 185 </div>
185 186 %endif
186 187 <div class="action-buttons">
187 188 <button id="preview-btn" class="btn btn-secondary">${_('Preview')}</button>
188 189 <button id="edit-btn" class="btn btn-secondary" style="display:none;">${_('Edit')}</button>
189 190 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
190 191 </div>
191 192 </div>
192 193 ${h.end_form()}
193 194 </div>
194 195 %endif
195 196 </div>
196 197 <script>
197 198 // init active elements of commentForm
198 199 var commitId = templateContext.commit_data.commit_id;
199 200 var pullRequestId = templateContext.pull_request_data.pull_request_id;
200 201 var lineNo;
201 202
202 203 var mainCommentForm = new CommentForm(
203 204 "#${form_id}", commitId, pullRequestId, lineNo, true);
204 205
205 206 mainCommentForm.initStatusChangeSelector();
206 207 bindToggleButtons();
207 208 </script>
208 209 </%def>
@@ -1,609 +1,691 b''
1 1 <%def name="diff_line_anchor(filename, line, type)"><%
2 2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 3 %></%def>
4 4
5 5 <%def name="action_class(action)"><%
6 6 return {
7 7 '-': 'cb-deletion',
8 8 '+': 'cb-addition',
9 9 ' ': 'cb-context',
10 10 }.get(action, 'cb-empty')
11 11 %></%def>
12 12
13 13 <%def name="op_class(op_id)"><%
14 14 return {
15 15 DEL_FILENODE: 'deletion', # file deleted
16 16 BIN_FILENODE: 'warning' # binary diff hidden
17 17 }.get(op_id, 'addition')
18 18 %></%def>
19 19
20 20 <%def name="link_for(**kw)"><%
21 21 new_args = request.GET.mixed()
22 22 new_args.update(kw)
23 23 return h.url('', **new_args)
24 24 %></%def>
25 25
26 26 <%def name="render_diffset(diffset, commit=None,
27 27
28 28 # collapse all file diff entries when there are more than this amount of files in the diff
29 29 collapse_when_files_over=20,
30 30
31 31 # collapse lines in the diff when more than this amount of lines changed in the file diff
32 32 lines_changed_limit=500,
33 33
34 34 # add a ruler at to the output
35 35 ruler_at_chars=0,
36 36
37 37 # show inline comments
38 38 use_comments=False,
39 39
40 40 # disable new comments
41 41 disable_new_comments=False,
42 42
43 # special file-comments that were deleted in previous versions
44 # it's used for showing outdated comments for deleted files in a PR
45 deleted_files_comments=None
46
43 47 )">
44 48
45 49 %if use_comments:
46 50 <div id="cb-comments-inline-container-template" class="js-template">
47 51 ${inline_comments_container([])}
48 52 </div>
49 53 <div class="js-template" id="cb-comment-inline-form-template">
50 54 <div class="comment-inline-form ac">
51 55 %if c.rhodecode_user.username != h.DEFAULT_USER:
52 56 ${h.form('#', method='get')}
53 57 <div id="edit-container_{1}" class="clearfix">
54 58 <div class="comment-title pull-left">
55 59 ${_('Create a comment on line {1}.')}
56 60 </div>
57 61 <div class="comment-help pull-right">
58 62 ${(_('Comments parsed using %s syntax with %s support.') % (
59 63 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
60 64 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
61 65 )
62 66 )|n
63 67 }
64 68 </div>
65 69 <div style="clear: both"></div>
66 70 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
67 71 </div>
68 72 <div id="preview-container_{1}" class="clearfix" style="display: none;">
69 73 <div class="comment-help">
70 74 ${_('Comment preview')}
71 75 </div>
72 76 <div id="preview-box_{1}" class="preview-box"></div>
73 77 </div>
74 78 <div class="comment-footer">
75 79 <div class="action-buttons">
76 80 <input type="hidden" name="f_path" value="{0}">
77 81 <input type="hidden" name="line" value="{1}">
78 82 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
79 83 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
80 84 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
81 85 </div>
82 86 <div class="comment-button">
83 87 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
84 88 ${_('Cancel')}
85 89 </button>
86 90 </div>
87 91 ${h.end_form()}
88 92 </div>
89 93 %else:
90 94 ${h.form('', class_='inline-form comment-form-login', method='get')}
91 95 <div class="pull-left">
92 96 <div class="comment-help pull-right">
93 97 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
94 98 </div>
95 99 </div>
96 100 <div class="comment-button pull-right">
97 101 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
98 102 ${_('Cancel')}
99 103 </button>
100 104 </div>
101 105 <div class="clearfix"></div>
102 106 ${h.end_form()}
103 107 %endif
104 108 </div>
105 109 </div>
106 110
107 111 %endif
108 112 <%
109 113 collapse_all = len(diffset.files) > collapse_when_files_over
110 114 %>
111 115
112 116 %if c.diffmode == 'sideside':
113 117 <style>
114 118 .wrapper {
115 119 max-width: 1600px !important;
116 120 }
117 121 </style>
118 122 %endif
119 123 %if ruler_at_chars:
120 124 <style>
121 125 .diff table.cb .cb-content:after {
122 126 content: "";
123 127 border-left: 1px solid blue;
124 128 position: absolute;
125 129 top: 0;
126 130 height: 18px;
127 131 opacity: .2;
128 132 z-index: 10;
129 ## +5 to account for diff action (+/-)
133 //## +5 to account for diff action (+/-)
130 134 left: ${ruler_at_chars + 5}ch;
131 135 </style>
132 136 %endif
133 137 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
134 138 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
135 139 %if commit:
136 140 <div class="pull-right">
137 141 <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='')}">
138 142 ${_('Browse Files')}
139 143 </a>
140 144 </div>
141 145 %endif
142 146 <h2 class="clearinner">
143 147 %if commit:
144 148 <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> -
145 149 ${h.age_component(commit.date)} -
146 150 %endif
147 151 %if diffset.limited_diff:
148 152 ${_('The requested commit is too big and content was truncated.')}
149 153
150 154 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
151 155 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
152 156 %else:
153 157 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
154 158 '%(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}}
155 159 %endif
160
161 <% at_ver = getattr(c, 'at_version_num', None) %>
162 % if at_ver:
163 <div class="pull-right">
164 ${_('Changes at version %d') % at_ver}
165 </div>
166 % endif
167
156 168 </h2>
157 169 </div>
158 170
159 171 %if not diffset.files:
160 172 <p class="empty_data">${_('No files')}</p>
161 173 %endif
162 174
163 175 <div class="filediffs">
164 176 %for i, filediff in enumerate(diffset.files):
165 177
166 178 <%
167 179 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
168 180 over_lines_changed_limit = lines_changed > lines_changed_limit
169 181 %>
170 182 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
171 183 <div
172 184 class="filediff"
173 185 data-f-path="${filediff['patch']['filename']}"
174 186 id="a_${h.FID('', filediff['patch']['filename'])}">
175 187 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
176 188 <div class="filediff-collapse-indicator"></div>
177 189 ${diff_ops(filediff)}
178 190 </label>
179 191 ${diff_menu(filediff, use_comments=use_comments)}
180 192 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
181 193 %if not filediff.hunks:
182 194 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
183 195 <tr>
184 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
196 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
185 197 %if op_id == DEL_FILENODE:
186 198 ${_('File was deleted')}
187 199 %elif op_id == BIN_FILENODE:
188 200 ${_('Binary file hidden')}
189 201 %else:
190 202 ${op_text}
191 203 %endif
192 204 </td>
193 205 </tr>
194 206 %endfor
195 207 %endif
208 %if filediff.patch['is_limited_diff']:
209 <tr class="cb-warning cb-collapser">
210 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
211 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
212 </td>
213 </tr>
214 %else:
196 215 %if over_lines_changed_limit:
197 216 <tr class="cb-warning cb-collapser">
198 217 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
199 218 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
200 219 <a href="#" class="cb-expand"
201 220 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
202 221 </a>
203 222 <a href="#" class="cb-collapse"
204 223 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
205 224 </a>
206 225 </td>
207 226 </tr>
208 227 %endif
209 %if filediff.patch['is_limited_diff']:
210 <tr class="cb-warning cb-collapser">
211 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
212 ${_('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>
213 </td>
214 </tr>
215 228 %endif
229
216 230 %for hunk in filediff.hunks:
217 231 <tr class="cb-hunk">
218 232 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
219 233 ## TODO: dan: add ajax loading of more context here
220 234 ## <a href="#">
221 235 <i class="icon-more"></i>
222 236 ## </a>
223 237 </td>
224 238 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
225 239 @@
226 240 -${hunk.source_start},${hunk.source_length}
227 241 +${hunk.target_start},${hunk.target_length}
228 242 ${hunk.section_header}
229 243 </td>
230 244 </tr>
231 245 %if c.diffmode == 'unified':
232 246 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
233 247 %elif c.diffmode == 'sideside':
234 248 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
235 249 %else:
236 250 <tr class="cb-line">
237 251 <td>unknown diff mode</td>
238 252 </tr>
239 253 %endif
240 254 %endfor
241 255
256 ## outdated comments that do not fit into currently displayed lines
242 257 % for lineno, comments in filediff.left_comments.items():
243 258
244 259 %if c.diffmode == 'unified':
245 260 <tr class="cb-line">
246 261 <td class="cb-data cb-context"></td>
247 262 <td class="cb-lineno cb-context"></td>
248 263 <td class="cb-lineno cb-context"></td>
249 264 <td class="cb-content cb-context">
250 265 ${inline_comments_container(comments)}
251 266 </td>
252 267 </tr>
253 268 %elif c.diffmode == 'sideside':
254 269 <tr class="cb-line">
255 270 <td class="cb-data cb-context"></td>
256 271 <td class="cb-lineno cb-context"></td>
257 272 <td class="cb-content cb-context"></td>
258 273
259 274 <td class="cb-data cb-context"></td>
260 275 <td class="cb-lineno cb-context"></td>
261 276 <td class="cb-content cb-context">
262 277 ${inline_comments_container(comments)}
263 278 </td>
264 279 </tr>
265 280 %endif
266 281
267 282 % endfor
268 283
269 284 </table>
270 285 </div>
271 286 %endfor
287
288 ## outdated comments that are made for a file that has been deleted
289 % for filename, comments_dict in (deleted_files_comments or {}).items():
290
291 <div class="filediffs filediff-outdated" style="display: none">
292 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
293 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
294 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
295 <div class="filediff-collapse-indicator"></div>
296 <span class="pill">
297 ## file was deleted
298 <strong>${filename}</strong>
299 </span>
300 <span class="pill-group" style="float: left">
301 ## file op, doesn't need translation
302 <span class="pill" op="removed">removed in this version</span>
303 </span>
304 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
305 <span class="pill-group" style="float: right">
306 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
307 </span>
308 </label>
309
310 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
311 <tr>
312 % if c.diffmode == 'unified':
313 <td></td>
314 %endif
315
316 <td></td>
317 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
318 ${_('File was deleted in this version, and outdated comments were made on it')}
319 </td>
320 </tr>
321 %if c.diffmode == 'unified':
322 <tr class="cb-line">
323 <td class="cb-data cb-context"></td>
324 <td class="cb-lineno cb-context"></td>
325 <td class="cb-lineno cb-context"></td>
326 <td class="cb-content cb-context">
327 ${inline_comments_container(comments_dict['comments'])}
328 </td>
329 </tr>
330 %elif c.diffmode == 'sideside':
331 <tr class="cb-line">
332 <td class="cb-data cb-context"></td>
333 <td class="cb-lineno cb-context"></td>
334 <td class="cb-content cb-context"></td>
335
336 <td class="cb-data cb-context"></td>
337 <td class="cb-lineno cb-context"></td>
338 <td class="cb-content cb-context">
339 ${inline_comments_container(comments_dict['comments'])}
340 </td>
341 </tr>
342 %endif
343 </table>
272 344 </div>
273 345 </div>
346 % endfor
347
348 </div>
274 349 </%def>
275 350
276 351 <%def name="diff_ops(filediff)">
277 352 <%
278 353 stats = filediff['patch']['stats']
279 354 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
280 355 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
281 356 %>
282 357 <span class="pill">
283 358 %if filediff.source_file_path and filediff.target_file_path:
284 359 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
285 360 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
286 361 %else:
287 362 ## file was modified
288 363 <strong>${filediff.source_file_path}</strong>
289 364 %endif
290 365 %else:
291 366 %if filediff.source_file_path:
292 367 ## file was deleted
293 368 <strong>${filediff.source_file_path}</strong>
294 369 %else:
295 370 ## file was added
296 371 <strong>${filediff.target_file_path}</strong>
297 372 %endif
298 373 %endif
299 374 </span>
300 375 <span class="pill-group" style="float: left">
301 376 %if filediff.patch['is_limited_diff']:
302 377 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
303 378 %endif
304 379 %if RENAMED_FILENODE in stats['ops']:
305 380 <span class="pill" op="renamed">renamed</span>
306 381 %endif
307 382
308 383 %if NEW_FILENODE in stats['ops']:
309 384 <span class="pill" op="created">created</span>
310 385 %if filediff['target_mode'].startswith('120'):
311 386 <span class="pill" op="symlink">symlink</span>
312 387 %else:
313 388 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
314 389 %endif
315 390 %endif
316 391
317 392 %if DEL_FILENODE in stats['ops']:
318 393 <span class="pill" op="removed">removed</span>
319 394 %endif
320 395
321 396 %if CHMOD_FILENODE in stats['ops']:
322 397 <span class="pill" op="mode">
323 398 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
324 399 </span>
325 400 %endif
326 401 </span>
327 402
328 403 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
329 404
330 405 <span class="pill-group" style="float: right">
331 406 %if BIN_FILENODE in stats['ops']:
332 407 <span class="pill" op="binary">binary</span>
333 408 %if MOD_FILENODE in stats['ops']:
334 409 <span class="pill" op="modified">modified</span>
335 410 %endif
336 411 %endif
337 412 %if stats['added']:
338 413 <span class="pill" op="added">+${stats['added']}</span>
339 414 %endif
340 415 %if stats['deleted']:
341 416 <span class="pill" op="deleted">-${stats['deleted']}</span>
342 417 %endif
343 418 </span>
344 419
345 420 </%def>
346 421
347 422 <%def name="nice_mode(filemode)">
348 423 ${filemode.startswith('100') and filemode[3:] or filemode}
349 424 </%def>
350 425
351 426 <%def name="diff_menu(filediff, use_comments=False)">
352 427 <div class="filediff-menu">
353 428 %if filediff.diffset.source_ref:
354 429 %if filediff.patch['operation'] in ['D', 'M']:
355 430 <a
356 431 class="tooltip"
357 432 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
358 433 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
359 434 >
360 435 ${_('Show file before')}
361 </a>
436 </a> |
362 437 %else:
363 438 <span
364 439 class="tooltip"
365 440 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
366 441 >
367 442 ${_('Show file before')}
368 </span>
443 </span> |
369 444 %endif
370 445 %if filediff.patch['operation'] in ['A', 'M']:
371 446 <a
372 447 class="tooltip"
373 448 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
374 449 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
375 450 >
376 451 ${_('Show file after')}
377 </a>
452 </a> |
378 453 %else:
379 454 <span
380 455 class="tooltip"
381 456 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
382 457 >
383 458 ${_('Show file after')}
384 </span>
459 </span> |
385 460 %endif
386 461 <a
387 462 class="tooltip"
388 463 title="${h.tooltip(_('Raw diff'))}"
389 464 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')}"
390 465 >
391 466 ${_('Raw diff')}
392 </a>
467 </a> |
393 468 <a
394 469 class="tooltip"
395 470 title="${h.tooltip(_('Download diff'))}"
396 471 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')}"
397 472 >
398 473 ${_('Download diff')}
399 474 </a>
475 % if use_comments:
476 |
477 % endif
400 478
401 479 ## 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)
402 480 %if hasattr(c, 'ignorews_url'):
403 481 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
404 482 %endif
405 483 %if hasattr(c, 'context_url'):
406 484 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
407 485 %endif
408 486
409
410 487 %if use_comments:
411 488 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
412 489 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
413 490 </a>
414 491 %endif
415 492 %endif
416 493 </div>
417 494 </%def>
418 495
419 496
420 497 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
421 498 <%def name="inline_comments_container(comments)">
422 499 <div class="inline-comments">
423 500 %for comment in comments:
424 501 ${commentblock.comment_block(comment, inline=True)}
425 502 %endfor
426 503
427 <span onclick="return Rhodecode.comments.createComment(this)"
428 class="btn btn-secondary cb-comment-add-button ${'comment-outdated' if comments and comments[-1].outdated else ''}"
429 style="${'display: none;' if comments and comments[-1].outdated else ''}">
504 % if comments and comments[-1].outdated:
505 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
506 style="display: none;}">
430 507 ${_('Add another comment')}
431 508 </span>
509 % else:
510 <span onclick="return Rhodecode.comments.createComment(this)"
511 class="btn btn-secondary cb-comment-add-button">
512 ${_('Add another comment')}
513 </span>
514 % endif
432 515
433 516 </div>
434 517 </%def>
435 518
436 519
437 520 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
438 521 %for i, line in enumerate(hunk.sideside):
439 522 <%
440 523 old_line_anchor, new_line_anchor = None, None
441 524 if line.original.lineno:
442 525 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
443 526 if line.modified.lineno:
444 527 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
445 528 %>
446 529
447 530 <tr class="cb-line">
448 531 <td class="cb-data ${action_class(line.original.action)}"
449 532 data-line-number="${line.original.lineno}"
450 533 >
451 534 <div>
452 535 %if line.original.comments:
453 536 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
454 537 %endif
455 538 </div>
456 539 </td>
457 540 <td class="cb-lineno ${action_class(line.original.action)}"
458 541 data-line-number="${line.original.lineno}"
459 542 %if old_line_anchor:
460 543 id="${old_line_anchor}"
461 544 %endif
462 545 >
463 546 %if line.original.lineno:
464 547 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
465 548 %endif
466 549 </td>
467 550 <td class="cb-content ${action_class(line.original.action)}"
468 551 data-line-number="o${line.original.lineno}"
469 552 >
470 553 %if use_comments and line.original.lineno:
471 554 ${render_add_comment_button()}
472 555 %endif
473 556 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
474 557 %if use_comments and line.original.lineno and line.original.comments:
475 558 ${inline_comments_container(line.original.comments)}
476 559 %endif
477 560 </td>
478 561 <td class="cb-data ${action_class(line.modified.action)}"
479 562 data-line-number="${line.modified.lineno}"
480 563 >
481 564 <div>
482 565 %if line.modified.comments:
483 566 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
484 567 %endif
485 568 </div>
486 569 </td>
487 570 <td class="cb-lineno ${action_class(line.modified.action)}"
488 571 data-line-number="${line.modified.lineno}"
489 572 %if new_line_anchor:
490 573 id="${new_line_anchor}"
491 574 %endif
492 575 >
493 576 %if line.modified.lineno:
494 577 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
495 578 %endif
496 579 </td>
497 580 <td class="cb-content ${action_class(line.modified.action)}"
498 581 data-line-number="n${line.modified.lineno}"
499 582 >
500 583 %if use_comments and line.modified.lineno:
501 584 ${render_add_comment_button()}
502 585 %endif
503 586 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
504 587 %if use_comments and line.modified.lineno and line.modified.comments:
505 588 ${inline_comments_container(line.modified.comments)}
506 589 %endif
507 590 </td>
508 591 </tr>
509 592 %endfor
510 593 </%def>
511 594
512 595
513 596 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
514 597 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
515 598 <%
516 599 old_line_anchor, new_line_anchor = None, None
517 600 if old_line_no:
518 601 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
519 602 if new_line_no:
520 603 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
521 604 %>
522 605 <tr class="cb-line">
523 606 <td class="cb-data ${action_class(action)}">
524 607 <div>
525 608 %if comments:
526 609 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
527 610 %endif
528 611 </div>
529 612 </td>
530 613 <td class="cb-lineno ${action_class(action)}"
531 614 data-line-number="${old_line_no}"
532 615 %if old_line_anchor:
533 616 id="${old_line_anchor}"
534 617 %endif
535 618 >
536 619 %if old_line_anchor:
537 620 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
538 621 %endif
539 622 </td>
540 623 <td class="cb-lineno ${action_class(action)}"
541 624 data-line-number="${new_line_no}"
542 625 %if new_line_anchor:
543 626 id="${new_line_anchor}"
544 627 %endif
545 628 >
546 629 %if new_line_anchor:
547 630 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
548 631 %endif
549 632 </td>
550 633 <td class="cb-content ${action_class(action)}"
551 634 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
552 635 >
553 636 %if use_comments:
554 637 ${render_add_comment_button()}
555 638 %endif
556 639 <span class="cb-code">${action} ${content or '' | n}</span>
557 640 %if use_comments and comments:
558 641 ${inline_comments_container(comments)}
559 642 %endif
560 643 </td>
561 644 </tr>
562 645 %endfor
563 646 </%def>
564 647
565 648 <%def name="render_add_comment_button()">
566 <button
567 class="btn btn-small btn-primary cb-comment-box-opener"
568 onclick="return Rhodecode.comments.createComment(this)"
569 ><span>+</span></button>
649 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
650 <span><i class="icon-comment"></i></span>
651 </button>
570 652 </%def>
571 653
572 654 <%def name="render_diffset_menu()">
573 655
574 656 <div class="diffset-menu clearinner">
575 657 <div class="pull-right">
576 658 <div class="btn-group">
577 659
578 660 <a
579 661 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
580 662 title="${_('View side by side')}"
581 663 href="${h.url_replace(diffmode='sideside')}">
582 664 <span>${_('Side by Side')}</span>
583 665 </a>
584 666 <a
585 667 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
586 668 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
587 669 <span>${_('Unified')}</span>
588 670 </a>
589 671 </div>
590 672 </div>
591 673
592 674 <div class="pull-left">
593 675 <div class="btn-group">
594 676 <a
595 677 class="btn"
596 678 href="#"
597 679 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
598 680 <a
599 681 class="btn"
600 682 href="#"
601 683 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
602 684 <a
603 685 class="btn"
604 686 href="#"
605 687 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
606 688 </div>
607 689 </div>
608 690 </div>
609 691 </%def>
@@ -1,113 +1,115 b''
1 1 ## Changesets table !
2 2 <%namespace name="base" file="/base/base.html"/>
3 3
4 4 %if c.ancestor:
5 5 <div class="ancestor">${_('Common Ancestor Commit')}:
6 6 <a href="${h.url('changeset_home',
7 7 repo_name=c.repo_name,
8 8 revision=c.ancestor)}">
9 9 ${h.short_id(c.ancestor)}
10 10 </a>
11 11 </div>
12 12 %endif
13 13
14 14 <div class="container">
15 15 <input type="hidden" name="__start__" value="revisions:sequence">
16 16 <table class="rctable compare_view_commits">
17 17 <tr>
18 18 <th>${_('Time')}</th>
19 19 <th>${_('Author')}</th>
20 20 <th>${_('Commit')}</th>
21 21 <th></th>
22 22 <th>${_('Description')}</th>
23 23 </tr>
24 24 %for commit in c.commit_ranges:
25 25 <tr id="row-${commit.raw_id}"
26 26 commit_id="${commit.raw_id}"
27 27 class="compare_select"
28 style="${'display: none' if c.collapse_all_commits else ''}"
28 29 >
29 30 <td class="td-time">
30 31 ${h.age_component(commit.date)}
31 32 </td>
32 33 <td class="td-user">
33 34 ${base.gravatar_with_user(commit.author, 16)}
34 35 </td>
35 36 <td class="td-hash">
36 37 <code>
37 38 <a href="${h.url('changeset_home',
38 39 repo_name=c.target_repo.repo_name,
39 40 revision=commit.raw_id)}">
40 41 r${commit.revision}:${h.short_id(commit.raw_id)}
41 42 </a>
42 43 ${h.hidden('revisions',commit.raw_id)}
43 44 </code>
44 45 </td>
45 46 <td class="expand_commit"
46 47 data-commit-id="${commit.raw_id}"
47 48 title="${_( 'Expand commit message')}"
48 49 >
49 50 <div class="show_more_col">
50 51 <i class="show_more"></i>
51 52 </div>
52 53 </td>
53 54 <td class="mid td-description">
54 55 <div class="log-container truncate-wrap">
55 56 <div
56 57 id="c-${commit.raw_id}"
57 58 class="message truncate"
58 59 data-message-raw="${commit.message}"
59 60 >
60 61 ${h.urlify_commit_message(commit.message, c.repo_name)}
61 62 </div>
62 63 </div>
63 64 </td>
64 65 </tr>
65 66 %endfor
66 <tr class="compare_select_hidden" style="display: none">
67 <tr class="compare_select_hidden" style="${'' if c.collapse_all_commits else 'display: none'}">
67 68 <td colspan="5">
68 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)}
69 ${ungettext('%s commit hidden','%s commits hidden', len(c.commit_ranges)) % len(c.commit_ranges)},
70 <a href="#" onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">${ungettext('show it','show them', len(c.commit_ranges))}</a>
69 71 </td>
70 72 </tr>
71 73 % if not c.commit_ranges:
72 74 <tr class="compare_select">
73 75 <td colspan="5">
74 76 ${_('No commits in this compare')}
75 77 </td>
76 78 </tr>
77 79 % endif
78 80 </table>
79 81 <input type="hidden" name="__end__" value="revisions:sequence">
80 82
81 83 </div>
82 84
83 85 <script>
84 86 $('.expand_commit').on('click',function(e){
85 87 var target_expand = $(this);
86 88 var cid = target_expand.data('commitId');
87 89
88 90 // ## TODO: dan: extract styles into css, and just toggleClass('open') here
89 91 if (target_expand.hasClass('open')){
90 92 $('#c-'+cid).css({
91 93 'height': '1.5em',
92 94 'white-space': 'nowrap',
93 95 'text-overflow': 'ellipsis',
94 96 'overflow':'hidden'
95 97 });
96 98 target_expand.removeClass('open');
97 99 }
98 100 else {
99 101 $('#c-'+cid).css({
100 102 'height': 'auto',
101 103 'white-space': 'pre-line',
102 104 'text-overflow': 'initial',
103 105 'overflow':'visible'
104 106 });
105 107 target_expand.addClass('open');
106 108 }
107 109 });
108 110
109 111 $('.compare_select').on('click',function(e){
110 112 var cid = $(this).attr('commit_id');
111 113 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
112 114 });
113 115 </script>
@@ -1,317 +1,317 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.html"/>
4 4 <%namespace name="base" file="/base/base.html"/>
5 5
6 6 ## REPOSITORY RENDERERS
7 7 <%def name="quick_menu(repo_name)">
8 8 <i class="pointer icon-more"></i>
9 9 <div class="menu_items_container hidden">
10 10 <ul class="menu_items">
11 11 <li>
12 12 <a title="${_('Summary')}" href="${h.url('summary_home',repo_name=repo_name)}">
13 13 <span>${_('Summary')}</span>
14 14 </a>
15 15 </li>
16 16 <li>
17 17 <a title="${_('Changelog')}" href="${h.url('changelog_home',repo_name=repo_name)}">
18 18 <span>${_('Changelog')}</span>
19 19 </a>
20 20 </li>
21 21 <li>
22 22 <a title="${_('Files')}" href="${h.url('files_home',repo_name=repo_name)}">
23 23 <span>${_('Files')}</span>
24 24 </a>
25 25 </li>
26 26 <li>
27 27 <a title="${_('Fork')}" href="${h.url('repo_fork_home',repo_name=repo_name)}">
28 28 <span>${_('Fork')}</span>
29 29 </a>
30 30 </li>
31 31 </ul>
32 32 </div>
33 33 </%def>
34 34
35 35 <%def name="repo_name(name,rtype,rstate,private,fork_of,short_name=False,admin=False)">
36 36 <%
37 37 def get_name(name,short_name=short_name):
38 38 if short_name:
39 39 return name.split('/')[-1]
40 40 else:
41 41 return name
42 42 %>
43 43 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
44 44 ##NAME
45 45 <a href="${h.url('edit_repo' if admin else 'summary_home',repo_name=name)}">
46 46
47 47 ##TYPE OF REPO
48 48 %if h.is_hg(rtype):
49 49 <span title="${_('Mercurial repository')}"><i class="icon-hg"></i></span>
50 50 %elif h.is_git(rtype):
51 51 <span title="${_('Git repository')}"><i class="icon-git"></i></span>
52 52 %elif h.is_svn(rtype):
53 53 <span title="${_('Subversion repository')}"><i class="icon-svn"></i></span>
54 54 %endif
55 55
56 56 ##PRIVATE/PUBLIC
57 57 %if private and c.visual.show_private_icon:
58 58 <i class="icon-lock" title="${_('Private repository')}"></i>
59 59 %elif not private and c.visual.show_public_icon:
60 60 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
61 61 %else:
62 62 <span></span>
63 63 %endif
64 64 ${get_name(name)}
65 65 </a>
66 66 %if fork_of:
67 67 <a href="${h.url('summary_home',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
68 68 %endif
69 69 %if rstate == 'repo_state_pending':
70 70 <i class="icon-cogs" title="${_('Repository creating in progress...')}"></i>
71 71 %endif
72 72 </div>
73 73 </%def>
74 74
75 75 <%def name="repo_desc(description)">
76 76 <div class="truncate-wrap">${description}</div>
77 77 </%def>
78 78
79 79 <%def name="last_change(last_change)">
80 80 ${h.age_component(last_change)}
81 81 </%def>
82 82
83 83 <%def name="revision(name,rev,tip,author,last_msg)">
84 84 <div>
85 85 %if rev >= 0:
86 86 <code><a title="${h.tooltip('%s:\n\n%s' % (author,last_msg))}" class="tooltip" href="${h.url('changeset_home',repo_name=name,revision=tip)}">${'r%s:%s' % (rev,h.short_id(tip))}</a></code>
87 87 %else:
88 88 ${_('No commits yet')}
89 89 %endif
90 90 </div>
91 91 </%def>
92 92
93 93 <%def name="rss(name)">
94 94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 95 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
96 96 %else:
97 97 <a title="${_('Subscribe to %s rss feed')% name}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
98 98 %endif
99 99 </%def>
100 100
101 101 <%def name="atom(name)">
102 102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 103 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
104 104 %else:
105 105 <a title="${_('Subscribe to %s atom feed')% name}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
106 106 %endif
107 107 </%def>
108 108
109 109 <%def name="user_gravatar(email, size=16)">
110 110 <div class="rc-user tooltip" title="${h.author_string(email)}">
111 111 ${base.gravatar(email, 16)}
112 112 </div>
113 113 </%def>
114 114
115 115 <%def name="repo_actions(repo_name, super_user=True)">
116 116 <div>
117 117 <div class="grid_edit">
118 118 <a href="${h.url('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
119 119 <i class="icon-pencil"></i>Edit</a>
120 120 </div>
121 121 <div class="grid_delete">
122 122 ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')}
123 123 ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger",
124 124 onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")}
125 125 ${h.end_form()}
126 126 </div>
127 127 </div>
128 128 </%def>
129 129
130 130 <%def name="repo_state(repo_state)">
131 131 <div>
132 132 %if repo_state == 'repo_state_pending':
133 133 <div class="tag tag4">${_('Creating')}</div>
134 134 %elif repo_state == 'repo_state_created':
135 135 <div class="tag tag1">${_('Created')}</div>
136 136 %else:
137 137 <div class="tag alert2" title="${repo_state}">invalid</div>
138 138 %endif
139 139 </div>
140 140 </%def>
141 141
142 142
143 143 ## REPO GROUP RENDERERS
144 144 <%def name="quick_repo_group_menu(repo_group_name)">
145 145 <i class="pointer icon-more"></i>
146 146 <div class="menu_items_container hidden">
147 147 <ul class="menu_items">
148 148 <li>
149 149 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
150 150 <span class="icon">
151 151 <i class="icon-file-text"></i>
152 152 </span>
153 153 <span>${_('Summary')}</span>
154 154 </a>
155 155 </li>
156 156
157 157 </ul>
158 158 </div>
159 159 </%def>
160 160
161 161 <%def name="repo_group_name(repo_group_name, children_groups=None)">
162 162 <div>
163 163 <a href="${h.url('repo_group_home',group_name=repo_group_name)}">
164 164 <i class="icon-folder-close" title="${_('Repository group')}"></i>
165 165 %if children_groups:
166 166 ${h.literal(' &raquo; '.join(children_groups))}
167 167 %else:
168 168 ${repo_group_name}
169 169 %endif
170 170 </a>
171 171 </div>
172 172 </%def>
173 173
174 174 <%def name="repo_group_desc(description)">
175 175 <div class="truncate-wrap">${description}</div>
176 176 </%def>
177 177
178 178 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
179 179 <div class="grid_edit">
180 180 <a href="${h.url('edit_repo_group',group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
181 181 </div>
182 182 <div class="grid_delete">
183 183 ${h.secure_form(h.url('delete_repo_group', group_name=repo_group_name),method='delete')}
184 184 ${h.submit('remove_%s' % repo_group_name,_('Delete'),class_="btn btn-link btn-danger",
185 185 onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_count) % (repo_group_name, gr_count)+"');")}
186 186 ${h.end_form()}
187 187 </div>
188 188 </%def>
189 189
190 190
191 191 <%def name="user_actions(user_id, username)">
192 192 <div class="grid_edit">
193 193 <a href="${h.url('edit_user',user_id=user_id)}" title="${_('Edit')}">
194 194 <i class="icon-pencil"></i>Edit</a>
195 195 </div>
196 196 <div class="grid_delete">
197 197 ${h.secure_form(h.url('delete_user', user_id=user_id),method='delete')}
198 198 ${h.submit('remove_',_('Delete'),id="remove_user_%s" % user_id, class_="btn btn-link btn-danger",
199 199 onclick="return confirm('"+_('Confirm to delete this user: %s') % username+"');")}
200 200 ${h.end_form()}
201 201 </div>
202 202 </%def>
203 203
204 204 <%def name="user_group_actions(user_group_id, user_group_name)">
205 205 <div class="grid_edit">
206 206 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
207 207 </div>
208 208 <div class="grid_delete">
209 209 ${h.secure_form(h.url('delete_users_group', user_group_id=user_group_id),method='delete')}
210 210 ${h.submit('remove_',_('Delete'),id="remove_group_%s" % user_group_id, class_="btn btn-link btn-danger",
211 211 onclick="return confirm('"+_('Confirm to delete this user group: %s') % user_group_name+"');")}
212 212 ${h.end_form()}
213 213 </div>
214 214 </%def>
215 215
216 216
217 217 <%def name="user_name(user_id, username)">
218 218 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.url('edit_user', user_id=user_id))}
219 219 </%def>
220 220
221 221 <%def name="user_profile(username)">
222 222 ${base.gravatar_with_user(username, 16)}
223 223 </%def>
224 224
225 225 <%def name="user_group_name(user_group_id, user_group_name)">
226 226 <div>
227 227 <a href="${h.url('edit_users_group', user_group_id=user_group_id)}">
228 228 <i class="icon-group" title="${_('User group')}"></i> ${user_group_name}</a>
229 229 </div>
230 230 </%def>
231 231
232 232
233 233 ## GISTS
234 234
235 235 <%def name="gist_gravatar(full_contact)">
236 236 <div class="gist_gravatar">
237 237 ${base.gravatar(full_contact, 30)}
238 238 </div>
239 239 </%def>
240 240
241 241 <%def name="gist_access_id(gist_access_id, full_contact)">
242 242 <div>
243 243 <b>
244 244 <a href="${h.url('gist',gist_id=gist_access_id)}">gist: ${gist_access_id}</a>
245 245 </b>
246 246 </div>
247 247 </%def>
248 248
249 249 <%def name="gist_author(full_contact, created_on, expires)">
250 250 ${base.gravatar_with_user(full_contact, 16)}
251 251 </%def>
252 252
253 253
254 254 <%def name="gist_created(created_on)">
255 255 <div class="created">
256 256 ${h.age_component(created_on, time_is_local=True)}
257 257 </div>
258 258 </%def>
259 259
260 260 <%def name="gist_expires(expires)">
261 261 <div class="created">
262 262 %if expires == -1:
263 263 ${_('never')}
264 264 %else:
265 265 ${h.age_component(h.time_to_utcdatetime(expires))}
266 266 %endif
267 267 </div>
268 268 </%def>
269 269
270 270 <%def name="gist_type(gist_type)">
271 271 %if gist_type != 'public':
272 272 <div class="tag">${_('Private')}</div>
273 273 %endif
274 274 </%def>
275 275
276 276 <%def name="gist_description(gist_description)">
277 277 ${gist_description}
278 278 </%def>
279 279
280 280
281 281 ## PULL REQUESTS GRID RENDERERS
282 282
283 283 <%def name="pullrequest_target_repo(repo_name)">
284 284 <div class="truncate">
285 285 ${h.link_to(repo_name,h.url('summary_home',repo_name=repo_name))}
286 286 </div>
287 287 </%def>
288 288 <%def name="pullrequest_status(status)">
289 289 <div class="${'flag_status %s' % status} pull-left"></div>
290 290 </%def>
291 291
292 292 <%def name="pullrequest_title(title, description)">
293 293 ${title} <br/>
294 294 ${h.shorter(description, 40)}
295 295 </%def>
296 296
297 297 <%def name="pullrequest_comments(comments_nr)">
298 <i class="icon-comment icon-comment-colored"></i> ${comments_nr}
298 <i class="icon-comment"></i> ${comments_nr}
299 299 </%def>
300 300
301 301 <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)">
302 302 <a href="${h.url('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
303 303 % if short:
304 304 #${pull_request_id}
305 305 % else:
306 306 ${_('Pull request #%(pr_number)s') % {'pr_number': pull_request_id,}}
307 307 % endif
308 308 </a>
309 309 </%def>
310 310
311 311 <%def name="pullrequest_updated_on(updated_on)">
312 312 ${h.age_component(h.time_to_utcdatetime(updated_on))}
313 313 </%def>
314 314
315 315 <%def name="pullrequest_author(full_contact)">
316 316 ${base.gravatar_with_user(full_contact, 16)}
317 317 </%def>
@@ -1,545 +1,545 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/debug_style/index.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 6 &raquo;
7 7 ${c.active}
8 8 </%def>
9 9
10 10
11 11 <%def name="real_main()">
12 12 <div class="box">
13 13 <div class="title">
14 14 ${self.breadcrumbs()}
15 15 </div>
16 16
17 17 <div class='sidebar-col-wrapper'>
18 18 ##main
19 19 ${self.sidebar()}
20 20
21 21 <div class="main-content">
22 22
23 23 <div style="opacity:.5">
24 24
25 25 <h2>Simple tables</h2>
26 26
27 27 <p>These styles will be adjusted later to provide a baseline style
28 28 for all tables without classes added, whether part of the
29 29 application or not. Currently, some of the
30 30 application-specific styles are applied to this table.</p>
31 31 <p>This is a baseline style for all tables, whether part of the
32 32 application or not. It has no class applied for styling. Use
33 33 the "rctable" class as outlined before for tables which are
34 34 part of the RhodeCode application.</p>
35 35 <table>
36 36 <tbody>
37 37 <tr>
38 38 <th>Header A</th>
39 39 <th>Header B</th>
40 40 <th>Header C</th>
41 41 <th>Header D</th>
42 42 </tr>
43 43 <tr>
44 44 <td>Content of col A</td>
45 45 <td>Content of col B</td>
46 46 <td>Content of col C</td>
47 47 <td>Content of col D</td>
48 48 </tr>
49 49 <tr>
50 50 <td>Content of col A</td>
51 51 <td>Content of col B</td>
52 52 <td>Content of col C</td>
53 53 <td>Content of col D</td>
54 54 </tr>
55 55 <tr>
56 56 <td>Content of col A</td>
57 57 <td>Content of col B</td>
58 58 <td>Content of col C</td>
59 59 <td>Content of col D</td>
60 60 </tr>
61 61 <tr>
62 62 <td>Content of col A</td>
63 63 <td>Content of col B</td>
64 64 <td>Content of col C</td>
65 65 <td>Content of col D</td>
66 66 </tr>
67 67 </tbody>
68 68 </table>
69 69 </div>
70 70
71 71
72 72
73 73
74 74 <h2>RC application table with examples</h2>
75 75
76 76 <p>This is a standard table which applies the rhodecode-specific styling to be used
77 77 throughout the application; it has <code>&lt;table class="rctable"&gt;</code>.
78 78 <br/>
79 79 By default, table data is not truncated, and wraps inside of the <code>&lt;td&gt
80 80 ;</code>. To prevent wrapping and contain data on one line, use the <code>&lt;
81 81 class="truncate-wrap"&gt;</code> on the <code>&lt;td&gt;</code>, and <code>span
82 82 class="truncate"</code> around the specific data to be truncated.
83 83 </p>
84 84 <p>
85 85 Ellipsis is added via CSS. Please always add a row of headers using <code>&lt;th
86 86 &gt;</code> to the top of a table.
87 87 </p>
88 88
89 89 ## TODO: johbo: in case we have more tables with examples, we should
90 90 ## create a generic class here.
91 91 <table class="rctable issuetracker">
92 92 <thead>
93 93 <tr>
94 94 <th>Header A</th>
95 95 <th>Header B</th>
96 96 <th>Header C</th>
97 97 <th>Header D</th>
98 98 </tr>
99 99 </thead>
100 100 <tbody>
101 101 <tr>
102 102 <td class="issue-tracker-example">
103 103 Example of col A
104 104 </td>
105 105 <td class="issue-tracker-example">
106 106 Example of col B
107 107 </td>
108 108 <td class="issue-tracker-example">
109 109 Example of col C
110 110 </td>
111 111 <td class="issue-tracker-example">
112 112 Example of col D
113 113 </td>
114 114 </tr>
115 115 <tr>
116 116 <td>Content of col A</td>
117 117 <td>Content of col B</td>
118 118 <td>Content of col C which is very long and will not be
119 119 truncated because sometimes people just want to write
120 120 really, really long commit messages which explain what
121 121 they did in excruciating detail and you really, really
122 122 want to read them.</td>
123 123 <td>Content of col D</td>
124 124 </tr>
125 125 <tr>
126 126 <td>Content of col A</td>
127 127 <td>Content of col B</td>
128 128 <td>Content of col C</td>
129 129 <td class="truncate-wrap"><span class="truncate">Truncated
130 130 content of column D truncate truncate truncatetruncate
131 131 truncate truncate</span></td>
132 132 </tr>
133 133 </tbody>
134 134 </table>
135 135
136 136 <h2>RC application table data classes</h2>
137 137
138 138 <p>The following tables contain documentation of all existing table data classes.
139 139 Please update when new classes are made.
140 140 </p>
141 141 <table class="rctable examples">
142 142 <thead>
143 143 <tr>
144 144 <th>Class</th>
145 145 <th>Description</th>
146 146 <th>Example</th>
147 147 </tr>
148 148 </thead>
149 149 <tbody>
150 150 <td>td-user</td>
151 151 <td>Any username/gravatar combination (see also Icons style).</td>
152 152 <td class="td-user author">
153 153 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
154 154 <span title="Oliver Strobel <oliver@rhodecode.com>" class="user">ostrobel (Oliver Strobel)</span>
155 155 </td>
156 156 </tr>
157 157 <tr>
158 158 <td>td-hash</td>
159 159 <td>Any hash; a commit, revision, etc. Use <code>&lt;pre&gt;</code> and header 'Commit'</td>
160 160 <td class="td-commit">
161 161 <pre><a href="/anothercpythonforkkkk/files/8d6b27837c6979983b037693fe975cdbb761b500/">r93699:8d6b27837c69</a></pre>
162 162 </td>
163 163 </tr>
164 164 <tr>
165 165 <td>td-rss</td>
166 166 <td>RSS feed link icon</td>
167 167 <td class="td-rss">
168 168 <a title="Subscribe to rss feed" href="/feed/rss"><i class="icon-rss-sign"></i></a>
169 169 </td>
170 170 </tr>
171 171 <tr>
172 172 <td>td-componentname</td>
173 173 <td>Any group, file, gist, or directory name.</td>
174 174 <td class="td-componentname">
175 175 <a href="/cpythonfork">
176 176 <span title="Mercurial repository"><i class="icon-hg"></i></span>
177 177 <i class="icon-unlock-alt" title="Public repository"></i>
178 178 rhodecode-dev-restyle-fork
179 179 </a>
180 180 </td>
181 181 </tr>
182 182 <tr>
183 183 <td>td-tags</td>
184 184 <td>Any cell containing tags, including branches and bookmarks.</td>
185 185 <td class="td-tags">
186 186 <span class="branchtag tag" title="Branch default">
187 187 <a href="/rhodecode-dev-restyle- fork/changelog?branch=default"><i class="icon-code-fork"></i>default</a>
188 188 </span>
189 189 </td>
190 190 </tr>
191 191 <tr>
192 192 <td>tags-truncate</td>
193 193 <td>Used to truncate a cell containing tags; avoid if possible.</td>
194 194 <td class="td-tags truncate-wrap">
195 195 <div class="truncate tags-truncate">
196 196 <div class="autoexpand">
197 197 <span class="tagtag tag" title="Tag tip">
198 198 <a href="/rhodecode-dev-restyle-fork/files/e519d5a0e71466d27257ddff921c4a13c540408e/"><i class="icon-tag"></i>tip</a>
199 199 </span>
200 200 <span class="branchtag tag" title="Branch default">
201 201 <a href="/rhodecode-dev-restyle-fork/changelog?branch=default"><i class="icon-code-fork"></i>default</a>
202 202 </span>
203 203 <span class="branchtag tag" title="Branch default">
204 204 <a href="/rhodecode-dev-restyle-fork/changelog?branch=default"><i class="icon-code-fork"></i>default</a>
205 205 </span>
206 206 </div>
207 207 </div>
208 208 </td>
209 209 </tr>
210 210 <tr>
211 211 <td>td-ip</td>
212 212 <td>Any ip address.</td>
213 213 <td class="td-ip">
214 214 172.16.115.168
215 215 </td>
216 216 </tr>
217 217 <tr>
218 218 <td>td-type</td>
219 219 <td>A state or an auth type.</td>
220 220 <td class="td-type">
221 221 rhodecode
222 222 </td>
223 223 </tr>
224 224 <tr>
225 225 <td>td-authtoken</td>
226 226 <td>For auth tokens. Use truncate classes for hover expand; see html.</td>
227 227 <td class="truncate-wrap td-authtoken">
228 228 <div class="truncate autoexpand">
229 229 <code>688df65b87d3ad16ae9f8fc6338a551d40f41c7a</code>
230 230 </div>
231 231 </td>
232 232 </tr>
233 233 <tr>
234 234 <td>td-action</td>
235 235 <td>Buttons which perform an action.</td>
236 236 <td class="td-action">
237 237 <div class="grid_edit">
238 238 <a href="/_admin/users/2/edit" title="edit">
239 239 <i class="icon-pencil"></i>Edit</a>
240 240 </div>
241 241 <div class="grid_delete">
242 242 <form action="/_admin/users/2" method="post">
243 243 <i class="icon-remove-sign"></i>
244 244 <input class="btn btn-danger btn-link" id="remove_user_2" name="remove_" type="submit" value="delete">
245 245 </form>
246 246 </div>
247 247 </td>
248 248 </tr>
249 249 <tr>
250 250 <td>td-radio</td>
251 251 <td>Radio buttons for a form. Centers element.</td>
252 252 <td class="td-radio">
253 253 <input type="radio" checked="checked" value="" name="1" id="read"></td>
254 254 </tr>
255 255 <tr>
256 256 <td>td-checkbox</td>
257 257 <td>Checkbox for a form. Centers element.</td>
258 258 <td class="td-checkbox">
259 259 <input type="checkbox" checked="checked" value="" name="1" id="read"></td>
260 260 </tr>
261 261 <tr>
262 262 <tr>
263 263 <td>td-buttons</td>
264 264 <td>Buttons.</td>
265 265 <td class="td-buttons">
266 266 <span class="btn btn-mini btn-primary">feed access</span>
267 267 </td>
268 268 </tr>
269 269 <tr>
270 270 <td>td-compare</td>
271 271 <td>Radio buttons to compare commits.</td>
272 272 <td class=" td-compare">
273 273 <input class="compare-radio-button" type="radio" name="compare_source" value="2.0">
274 274 <input class="compare-radio-button" type="radio" name="compare_target" value="2.0">
275 275 </td>
276 276 </tr>
277 277 <tr>
278 278 <td>td-comments</td>
279 279 <td>Comments indicator icon.</td>
280 280 <td>
281 <i class="icon-comment icon-comment-colored"></i> 0
281 <i class="icon-comment"></i> 0
282 282 </td>
283 283 </tr>
284 284 <tr>
285 285 <td>td-status</td>
286 286 <td>Status indicator icon.</td>
287 287 <td class="td-description">
288 288 <div class="flag_status under_review pull-left"></div>
289 289 </td>
290 290 </tr>
291 291 </tbody>
292 292 </table>
293 293 <table class="dataTable rctable examples">
294 294 <tbody>
295 295 <tr>
296 296 <td>quick_repo_menu</td>
297 297 <td>Hidden menu generated by dataTable.</td>
298 298 <td class="quick_repo_menu">
299 299 <i class="pointer icon-more"></i>
300 300 <div class="menu_items_container" style="display: none;">
301 301 <ul class="menu_items">
302 302 <li>
303 303 <a title="Summary" href="/anothercpythonforkkkk-fork">
304 304 <span>Summary</span>
305 305 </a>
306 306 </li>
307 307 <li>
308 308 <a title="Changelog" href="/anothercpythonforkkkk-fork/changelog">
309 309 <span>Changelog</span>
310 310 </a>
311 311 </li>
312 312 <li>
313 313 <a title="Files" href="/anothercpythonforkkkk-fork/files/tip/">
314 314 <span>Files</span>
315 315 </a>
316 316 </li>
317 317 <li>
318 318 <a title="Fork" href="/anothercpythonforkkkk-fork/fork">
319 319 <span>Fork</span>
320 320 </a>
321 321 </li>
322 322 </ul>
323 323 </div>
324 324 </td>
325 325 <td></td>
326 326 </tr>
327 327 </tbody>
328 328 </table>
329 329 <script>quick_repo_menu();</script>
330 330 <table class="rctable examples">
331 331 <tbody>
332 332 <tr>
333 333 <td>td-description</td>
334 334 <td>Any description. They may be rather long, and using the expand_commit outlined below is recommended.</td>
335 335 <td class="td-description">
336 336 Ultrices mattis! Enim pellentesque lacus, sit magna natoque risus turpis ut, auctor ultrices facilisis dapibus odio? Parturient! Porta egestas nascetur, quis, elementum dolor, in magna ac dis sit etiam turpis, scelerisque! Integer tristique aliquam.
337 337 </td>
338 338 </tr>
339 339 </tbody>
340 340 </table>
341 341 <table id="changesets" class="rctable examples end">
342 342 <tbody>
343 343 <tr>
344 344 <td>expand_commit</td>
345 345 <td>Expands a long message; see html+js.</td>
346 346 <td class="expand_commit" data-commit-id="2ffc6faabc7a9c790b1b452943a3f0c047b8b436" title="Expand commit message">
347 347 <div class="show_more_col">
348 348 <i class="show_more"></i>
349 349 </div>
350 350 </td>
351 351 <td class="mid td-description">
352 352 <div class="log-container truncate-wrap">
353 353 <div id="c-2ffc6faabc7a9c790b1b452943a3f0c047b8b436" class="message truncate" data-message-raw="tests: Test echo method on the server object
354 354
355 355 This only works for Pyro4 so far, have to extend it still for HTTP to work.">tests: Test echo method on the server object
356 356
357 357 This only works for Pyro4 so far, have to extend it still for HTTP to work.</div>
358 358 </div>
359 359 </td>
360 360 </tr>
361 361 </tbody>
362 362 </table>
363 363 <script type="text/javascript">
364 364 var cache = {}
365 365 $('.expand_commit').on('click',function(e){
366 366 var target_expand = $(this);
367 367 var cid = target_expand.data('commitId');
368 368
369 369 if (target_expand.hasClass('open')){
370 370 $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'});
371 371 $('#t-'+cid).css({'height': '1.5em', 'max-height': '1.5em', 'text-overflow': 'ellipsis', 'overflow':'hidden', 'white-space':'nowrap'});
372 372 target_expand.removeClass('open');
373 373 }
374 374 else {
375 375 $('#c-'+cid).css({'height': 'auto', 'white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'});
376 376 $('#t-'+cid).css({'height': 'auto', 'max-height': 'none', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'});
377 377 target_expand.addClass('open');
378 378 }
379 379 });
380 380
381 381 </script>
382 382 <p>The following classes currently do not have unique styles applied.</p>
383 383 <table class="rctable examples end">
384 384 <tbody>
385 385 <tr>
386 386 <td>td-regex</td>
387 387 <td>Regex patterns</td>
388 388 <td class="td-regex">(?:#)(?P<issue_id>\d+)</td>
389 389 </tr>
390 390 <tr>
391 391 <td>td-url</td>
392 392 <td>Any URL.</td>
393 393 <td class="td-url">https://rhodecode.com</td>
394 394 </tr>
395 395 <tr>
396 396 <td>td-journalaction</td>
397 397 <td>Action listed in a journal</td>
398 398 <td class="td-journalaction">started following repository supervisor-fork-4</td>
399 399 </tr>
400 400 <tr>
401 401 <td>td-iprange</td>
402 402 <td>Any ip address.</td>
403 403 <td class="td-ip">127.0.0.1-127.0.0.10</td>
404 404 </tr>
405 405 <tr>
406 406 <td>td-exp</td>
407 407 <td>Expiration time.</td>
408 408 <td class="td-exp">never</td>
409 409 </tr>
410 410 <tr>
411 411 <td>td-prefix</td>
412 412 <td>Prefixes outlined in settings.</td>
413 413 <td class="td-prefix">ubuntu-92539</td>
414 414 </tr>
415 415 <tr>
416 416 <td>td-cachekey</td>
417 417 <td>Cache key value.</td>
418 418 <td class="td-cachekey">ubuntu-92539supervisor</td>
419 419 </tr>
420 420 <tr>
421 421 <td>td-email</td>
422 422 <td>Any email address.</td>
423 423 <td class="td-email">example@rhodecode.com</td>
424 424 </tr>
425 425 <tr>
426 426 <td>td-active</td>
427 427 <td>Shows active state with icon-true/icon-false.</td>
428 428 <td class="td-active"><i class="icon-false"></i></td>
429 429 </tr>
430 430 <tr>
431 431 <td>td-size</td>
432 432 <td>File, repo, or directory size.</td>
433 433 <td class="td-size">89 MB</td>
434 434 </tr>
435 435 <tr>
436 436 <td>td-number</td>
437 437 <td>Any numerical data.</td>
438 438 <td class="td-number">42</td>
439 439 </tr>
440 440 <tr>
441 441 <td>td-message</td>
442 442 <td>Any commit message. Often treated with the truncate class used for descriptions as well.</td>
443 443 <td class="td-message">Updated the files</td>
444 444 </tr>
445 445 </tbody>
446 446 </table>
447 447
448 448
449 449 <h2>Permissions table</h2>
450 450
451 451 <p>
452 452 This is a special-case table; it has
453 453 <code>table class="rctable permissions"</code>
454 454 where "rctable" applies the rhodecode styling as above, and
455 455 "permissions" adds an extra layer of customization specific to
456 456 permissions tables. Other special-case tables may exist or be
457 457 created if necessary.
458 458 </p>
459 459
460 460 <table class="rctable permissions">
461 461 <tr>
462 462 <th class="td-radio">none</th>
463 463 <th class="td-radio">read</th>
464 464 <th class="td-radio">write</th>
465 465 <th class="td-radio">admin</th>
466 466 <th>user/user group</th>
467 467 <th></th>
468 468 </tr>
469 469 <tr class="perm_admin_row">
470 470 <td class="td-radio"><input type="radio" value="repository.none"
471 471 name="admin_perm_2" id="admin_perm_2_repositorynone"
472 472 disabled="disabled"></td>
473 473 <td class="td-radio"><input type="radio" value="repository.read"
474 474 name="admin_perm_2" id="admin_perm_2_repositoryread"
475 475 disabled="disabled"></td>
476 476 <td class="td-radio"><input type="radio" value="repository.write"
477 477 name="admin_perm_2" id="admin_perm_2_repositorywrite"
478 478 disabled="disabled"></td>
479 479 <td class="td-radio"><input type="radio" value="repository.admin"
480 480 name="admin_perm_2" id="admin_perm_2_repositoryadmin"
481 481 disabled="disabled" checked="checked"></td>
482 482 <td>
483 483 <img class="gravatar" src="https://secure.gravatar.com/avatar/be9d18f611892a738e54f2a3a171e2f9?d=identicon&amp;s=32" height="16" width="16">
484 484 <span class="user">dev (super admin) (owner)</span>
485 485 </td>
486 486 <td></td>
487 487 </tr>
488 488 <tr>
489 489 <td colspan="4">
490 490 <span class="private_repo_msg">
491 491 private repository
492 492 </span>
493 493 </td>
494 494 <td class="private_repo_msg">
495 495 <i class="icon-user"></i>
496 496 default - only people explicitly added here will have access</td>
497 497 <td></td>
498 498 </tr>
499 499 <tr>
500 500 <td class="td-radio"><input type="radio" value="repository.none"
501 501 name="u_perm_1" id="u_perm_1_repositorynone"></td>
502 502 <td class="td-radio"><input type="radio" checked="checked"
503 503 value="repository.read" name="u_perm_1"
504 504 id="u_perm_1_repositoryread"></td>
505 505 <td class="td-radio"><input type="radio" value="repository.write"
506 506 name="u_perm_1" id="u_perm_1_repositorywrite"></td>
507 507 <td class="td-radio"><input type="radio" value="repository.admin"
508 508 name="u_perm_1" id="u_perm_1_repositoryadmin"></td>
509 509 <td>
510 510 <img class="gravatar" src="/_static/rhodecode/images/user30.png" height="16" width="16">
511 511 <span class="user">default</span>
512 512 </td>
513 513 <td></td>
514 514 </tr>
515 515 <tr>
516 516 <td class="td-radio"><input type="radio" value="repository.none"
517 517 name="u_perm_2" id="u_perm_2_repositorynone"></td>
518 518 <td class="td-radio"><input type="radio" checked="checked"
519 519 value="repository.read" name="u_perm_2"
520 520 id="u_perm_2_repositoryread"></td>
521 521 <td class="td-radio"><input type="radio" value="repository.write"
522 522 name="u_perm_2" id="u_perm_2_repositorywrite"></td>
523 523 <td class="td-radio"><input type="radio" value="repository.admin"
524 524 name="u_perm_2" id="u_perm_2_repositoryadmin"></td>
525 525 <td>
526 526 <img class="gravatar" src="https://secure.gravatar.com/avatar/be9d18f611892a738e54f2a3a171e2f9?d=identicon&amp;s=32" height="16" width="16">
527 527 <a class="user" href="/_admin/users/2/edit">dev</a>
528 528 </td>
529 529 <td>
530 530 <span member_type="user" member="2"
531 531 class="btn action_button btn-link btn-danger">revoke</span>
532 532 </td>
533 533 </tr>
534 534 </tbody>
535 535 </table>
536 536 <div class="link" id="add_perm">
537 537 Add new
538 538 </div>
539 539
540 540
541 541
542 542 </div>
543 543 </div>
544 544 </div>
545 545 </%def>
@@ -1,605 +1,637 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 <span id="pr-title">
12 12 ${c.pull_request.title}
13 13 %if c.pull_request.is_closed():
14 14 (${_('Closed')})
15 15 %endif
16 16 </span>
17 17 <div id="pr-title-edit" class="input" style="display: none;">
18 18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 19 </div>
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='showpullrequest')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31 <script type="text/javascript">
32 32 // TODO: marcink switch this to pyroutes
33 33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 35 </script>
36 36 <div class="box">
37 37 <div class="title">
38 38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 39 </div>
40 40
41 41 ${self.breadcrumbs()}
42 42
43 43
44 44 <div class="box pr-summary">
45 45 <div class="summary-details block-left">
46 46 <%summary = lambda n:{False:'summary-short'}.get(n)%>
47 47 <div class="pr-details-title">
48 48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
52 52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 55 ${h.end_form()}
56 56 % else:
57 57 ${_('Delete')}
58 58 % endif
59 59 </div>
60 60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel edit')}</div>
61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 62 %endif
63 63 </div>
64 64
65 65 <div id="summary" class="fields pr-details-content">
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Origin')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div class="pr-origininfo">
72 72 ## branch link is only valid if it is a branch
73 73 <span class="tag">
74 74 %if c.pull_request.source_ref_parts.type == 'branch':
75 75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 76 %else:
77 77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 78 %endif
79 79 </span>
80 80 <span class="clone-url">
81 81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 82 </span>
83 83 </div>
84 84 <div class="pr-pullinfo">
85 85 %if h.is_hg(c.pull_request.source_repo):
86 86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 87 %elif h.is_git(c.pull_request.source_repo):
88 88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 89 %endif
90 90 </div>
91 91 </div>
92 92 </div>
93 93 <div class="field">
94 94 <div class="label-summary">
95 95 <label>${_('Target')}:</label>
96 96 </div>
97 97 <div class="input">
98 98 <div class="pr-targetinfo">
99 99 ## branch link is only valid if it is a branch
100 100 <span class="tag">
101 101 %if c.pull_request.target_ref_parts.type == 'branch':
102 102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 103 %else:
104 104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 105 %endif
106 106 </span>
107 107 <span class="clone-url">
108 108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 109 </span>
110 110 </div>
111 111 </div>
112 112 </div>
113 113
114 114 ## Link to the shadow repository.
115 115 <div class="field">
116 116 <div class="label-summary">
117 117 <label>${_('Merge')}:</label>
118 118 </div>
119 119 <div class="input">
120 120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 121 <div class="pr-mergeinfo">
122 122 %if h.is_hg(c.pull_request.target_repo):
123 123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 124 %elif h.is_git(c.pull_request.target_repo):
125 125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 126 %endif
127 127 </div>
128 128 % else:
129 129 <div class="">
130 130 ${_('Shadow repository data not available')}.
131 131 </div>
132 132 % endif
133 133 </div>
134 134 </div>
135 135
136 136 <div class="field">
137 137 <div class="label-summary">
138 138 <label>${_('Review')}:</label>
139 139 </div>
140 140 <div class="input">
141 141 %if c.pull_request_review_status:
142 142 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
143 143 <span class="changeset-status-lbl tooltip">
144 144 %if c.pull_request.is_closed():
145 145 ${_('Closed')},
146 146 %endif
147 147 ${h.commit_status_lbl(c.pull_request_review_status)}
148 148 </span>
149 149 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
150 150 %endif
151 151 </div>
152 152 </div>
153 153 <div class="field">
154 154 <div class="pr-description-label label-summary">
155 155 <label>${_('Description')}:</label>
156 156 </div>
157 157 <div id="pr-desc" class="input">
158 158 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
159 159 </div>
160 160 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
161 161 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
162 162 </div>
163 163 </div>
164 <div class="field">
165 <div class="label-summary">
166 <label>${_('Comments')}:</label>
167 </div>
168 <div class="input">
169 <div>
170 <div class="comments-number">
171 %if c.comments:
172 <a href="#comments">${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}</a>,
173 %else:
174 ${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}
175 %endif
176
177 %if c.inline_cnt:
178 <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>
179 %else:
180 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
181 %endif
182
183 %if c.outdated_cnt:
184 , ${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
185 %endif
186 </div>
187 </div>
188 </div>
189
190 </div>
191
192 <style>
193 .pr-versions {
194 position: relative;
195 top: 6px;
196 }
197 </style>
198 164
199 165 <div class="field">
200 166 <div class="label-summary">
201 167 <label>${_('Versions')} (${len(c.versions)+1}):</label>
202 168 </div>
203 169
204 170 <div class="pr-versions">
205 171 % if c.show_version_changes:
206 172 <table>
207 ## current visible version
208 <tr class="version-pr" style="display: ${'' if c.at_version in [None, 'latest'] else 'none'}">
173 ## CURRENTLY SELECT PR VERSION
174 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
209 175 <td>
210 176 % if c.at_version in [None, 'latest']:
211 177 <i class="icon-ok link"></i>
178 % else:
179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
212 180 % endif
213 181 </td>
214 182 <td>
215 183 <code>
216 184 % if c.versions:
217 185 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
218 186 % else:
219 187 ${_('initial')}
220 188 % endif
221 189 </code>
222 190 </td>
223 191 <td>
224 192 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
225 193 </td>
226 <td>${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}</td>
227 194 <td>
228 % if c.versions and c.at_version in [None, 'latest']:
229 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('(Show all)')}</span>
195 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
196 </td>
197 <td align="right">
198 % if c.versions and c.at_version_num in [None, 'latest']:
199 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
230 200 % endif
231 201 </td>
232 202 </tr>
233 203
204 ## SHOW ALL VERSIONS OF PR
205 <% ver_pr = None %>
234 206 % for ver in reversed(c.pull_request.versions()):
235 <tr class="version-pr" style="display: ${'' if c.at_version == ver.pull_request_version_id else 'none'}">
207 <% ver_pr = ver.pull_request_version_id %>
208 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
236 209 <td>
237 % if c.at_version == ver.pull_request_version_id:
210 % if c.at_version == ver_pr:
238 211 <i class="icon-ok link"></i>
212 % else:
213 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
239 214 % endif
240 215 </td>
241 <td><code><a href="${h.url.current(version=ver.pull_request_version_id)}">version ${ver.pull_request_version_id}</a></code></td>
216 <td>
217 <code><a href="${h.url.current(version=ver_pr)}">version ${ver_pr}</a></code>
218 </td>
242 219 <td>
243 220 <code>${ver.source_ref_parts.commit_id[:6]}</code>
244 221 </td>
245 <td>${_('created')} ${h.age_component(ver.updated_on)}</td>
246 222 <td>
247 % if c.at_version == ver.pull_request_version_id:
248 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('(Show all)')}</span>
223 ${_('created')} ${h.age_component(ver.updated_on)}
224 </td>
225 <td align="right">
226 % if c.at_version == ver_pr:
227 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
249 228 % endif
250 229 </td>
251 230 </tr>
252 231 % endfor
253 </table>
232
233 ## show comment/inline comments summary
234 <tr>
235 <td>
236 </td>
237
238 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
239 <td colspan="4" style="border-top: 1px dashed #dbd9da">
240 ${_('Comments for this version')}:
241 %if c.comments:
242 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
243 %else:
244 ${_("%d General ") % len(c.comments)}
245 %endif
254 246
247 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
248 %if inline_comm_count_ver:
249 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
250 %else:
251 , ${_("%d Inline") % inline_comm_count_ver}
252 %endif
253
254 %if c.outdated_cnt:
255 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
256 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
257 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
258 %else:
259 , ${_("%d Outdated") % c.outdated_cnt}
260 %endif
261 </td>
262 </tr>
263
264 <tr>
265 <td></td>
266 <td colspan="4">
255 267 % if c.at_version:
256 268 <pre>
257 269 Changed commits:
258 270 * added: ${len(c.changes.added)}
259 271 * removed: ${len(c.changes.removed)}
260 272
261 273 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
262 274 No file changes found
263 275 % else:
264 276 Changed files:
265 277 %for file_name in c.file_changes.added:
266 278 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
267 279 %endfor
268 280 %for file_name in c.file_changes.modified:
269 281 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
270 282 %endfor
271 283 %for file_name in c.file_changes.removed:
272 284 * R ${file_name}
273 285 %endfor
274 286 % endif
275 287 </pre>
276 288 % endif
289 </td>
290 </tr>
291 </table>
277 292 % else:
278 293 ${_('Pull request versions not available')}.
279 294 % endif
280 295 </div>
281 296 </div>
282 297
283 298 <div id="pr-save" class="field" style="display: none;">
284 299 <div class="label-summary"></div>
285 300 <div class="input">
286 301 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
287 302 </div>
288 303 </div>
289 304 </div>
290 305 </div>
291 306 <div>
292 307 ## AUTHOR
293 308 <div class="reviewers-title block-right">
294 309 <div class="pr-details-title">
295 310 ${_('Author')}
296 311 </div>
297 312 </div>
298 313 <div class="block-right pr-details-content reviewers">
299 314 <ul class="group_members">
300 315 <li>
301 316 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
302 317 </li>
303 318 </ul>
304 319 </div>
305 320 ## REVIEWERS
306 321 <div class="reviewers-title block-right">
307 322 <div class="pr-details-title">
308 323 ${_('Pull request reviewers')}
309 324 %if c.allowed_to_update:
310 325 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
311 326 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
312 327 %endif
313 328 </div>
314 329 </div>
315 330 <div id="reviewers" class="block-right pr-details-content reviewers">
316 331 ## members goes here !
317 332 <input type="hidden" name="__start__" value="review_members:sequence">
318 333 <ul id="review_members" class="group_members">
319 334 %for member,reasons,status in c.pull_request_reviewers:
320 335 <li id="reviewer_${member.user_id}">
321 336 <div class="reviewers_member">
322 337 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
323 338 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
324 339 </div>
325 340 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
326 341 ${self.gravatar_with_user(member.email, 16)}
327 342 </div>
328 343 <input type="hidden" name="__start__" value="reviewer:mapping">
329 344 <input type="hidden" name="__start__" value="reasons:sequence">
330 345 %for reason in reasons:
331 346 <div class="reviewer_reason">- ${reason}</div>
332 347 <input type="hidden" name="reason" value="${reason}">
333 348
334 349 %endfor
335 350 <input type="hidden" name="__end__" value="reasons:sequence">
336 351 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
337 352 <input type="hidden" name="__end__" value="reviewer:mapping">
338 353 %if c.allowed_to_update:
339 354 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
340 355 <i class="icon-remove-sign" ></i>
341 356 </div>
342 357 %endif
343 358 </div>
344 359 </li>
345 360 %endfor
346 361 </ul>
347 362 <input type="hidden" name="__end__" value="review_members:sequence">
348 363 %if not c.pull_request.is_closed():
349 364 <div id="add_reviewer_input" class='ac' style="display: none;">
350 365 %if c.allowed_to_update:
351 366 <div class="reviewer_ac">
352 367 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
353 368 <div id="reviewers_container"></div>
354 369 </div>
355 370 <div>
356 371 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
357 372 </div>
358 373 %endif
359 374 </div>
360 375 %endif
361 376 </div>
362 377 </div>
363 378 </div>
364 379 <div class="box">
365 380 ##DIFF
366 381 <div class="table" >
367 382 <div id="changeset_compare_view_content">
368 383 ##CS
369 384 % if c.missing_requirements:
370 385 <div class="box">
371 386 <div class="alert alert-warning">
372 387 <div>
373 388 <strong>${_('Missing requirements:')}</strong>
374 389 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
375 390 </div>
376 391 </div>
377 392 </div>
378 393 % elif c.missing_commits:
379 394 <div class="box">
380 395 <div class="alert alert-warning">
381 396 <div>
382 397 <strong>${_('Missing commits')}:</strong>
383 398 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
384 399 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
385 400 </div>
386 401 </div>
387 402 </div>
388 403 % endif
389 404 <div class="compare_view_commits_title">
390 405
391 406 <div class="pull-left">
392 407 <div class="btn-group">
393 408 <a
394 409 class="btn"
395 410 href="#"
396 411 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
397 412 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
398 413 </a>
399 414 <a
400 415 class="btn"
401 416 href="#"
402 417 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
403 418 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
404 419 </a>
405 420 </div>
406 421 </div>
407 422
408 423 <div class="pull-right">
409 424 % if c.allowed_to_update and not c.pull_request.is_closed():
410 425 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
411 426 % else:
412 427 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
413 428 % endif
414 429
415 430 </div>
416 431
417 432 </div>
418 433 % if not c.missing_commits:
419 434 <%include file="/compare/compare_commits.html" />
420 435 <div class="cs_files">
421 436 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
422 437 ${cbdiffs.render_diffset_menu()}
423 438 ${cbdiffs.render_diffset(
424 439 c.diffset, use_comments=True,
425 440 collapse_when_files_over=30,
426 disable_new_comments=not c.allowed_to_comment)}
441 disable_new_comments=not c.allowed_to_comment,
442 deleted_files_comments=c.deleted_files_comments)}
427 443
428 444 </div>
429 445 % endif
430 446 </div>
431 447
432 448 ## template for inline comment form
433 449 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
434 450
435 451 ## render general comments
436 452 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
437 453
438 454 % if not c.pull_request.is_closed():
439 455 ## main comment form and it status
440 456 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
441 457 pull_request_id=c.pull_request.pull_request_id),
442 458 c.pull_request_review_status,
443 459 is_pull_request=True, change_status=c.allowed_to_change_status)}
444 460 %endif
445 461
446 462 <script type="text/javascript">
447 463 if (location.hash) {
448 464 var result = splitDelimitedHash(location.hash);
449 465 var line = $('html').find(result.loc);
450 466 if (line.length > 0){
451 467 offsetScroll(line, 70);
452 468 }
453 469 }
454 470 $(function(){
455 471 ReviewerAutoComplete('user');
456 472 // custom code mirror
457 473 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
458 474
459 475 var PRDetails = {
460 476 editButton: $('#open_edit_pullrequest'),
461 477 closeButton: $('#close_edit_pullrequest'),
462 478 deleteButton: $('#delete_pullrequest'),
463 479 viewFields: $('#pr-desc, #pr-title'),
464 480 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
465 481
466 482 init: function() {
467 483 var that = this;
468 484 this.editButton.on('click', function(e) { that.edit(); });
469 485 this.closeButton.on('click', function(e) { that.view(); });
470 486 },
471 487
472 488 edit: function(event) {
473 489 this.viewFields.hide();
474 490 this.editButton.hide();
475 491 this.deleteButton.hide();
476 492 this.closeButton.show();
477 493 this.editFields.show();
478 494 codeMirrorInstance.refresh();
479 495 },
480 496
481 497 view: function(event) {
482 498 this.editButton.show();
483 499 this.deleteButton.show();
484 500 this.editFields.hide();
485 501 this.closeButton.hide();
486 502 this.viewFields.show();
487 503 }
488 504 };
489 505
490 506 var ReviewersPanel = {
491 507 editButton: $('#open_edit_reviewers'),
492 508 closeButton: $('#close_edit_reviewers'),
493 509 addButton: $('#add_reviewer_input'),
494 510 removeButtons: $('.reviewer_member_remove'),
495 511
496 512 init: function() {
497 513 var that = this;
498 514 this.editButton.on('click', function(e) { that.edit(); });
499 515 this.closeButton.on('click', function(e) { that.close(); });
500 516 },
501 517
502 518 edit: function(event) {
503 519 this.editButton.hide();
504 520 this.closeButton.show();
505 521 this.addButton.show();
506 522 this.removeButtons.css('visibility', 'visible');
507 523 },
508 524
509 525 close: function(event) {
510 526 this.editButton.show();
511 527 this.closeButton.hide();
512 528 this.addButton.hide();
513 529 this.removeButtons.css('visibility', 'hidden');
514 530 }
515 531 };
516 532
517 533 PRDetails.init();
518 534 ReviewersPanel.init();
519 535
536 showOutdated = function(self){
537 $('.comment-outdated').show();
538 $('.filediff-outdated').show();
539 $('.showOutdatedComments').hide();
540 $('.hideOutdatedComments').show();
541
542 };
543
544 hideOutdated = function(self){
545 $('.comment-outdated').hide();
546 $('.filediff-outdated').hide();
547 $('.hideOutdatedComments').hide();
548 $('.showOutdatedComments').show();
549 };
550
520 551 $('#show-outdated-comments').on('click', function(e){
521 552 var button = $(this);
522 553 var outdated = $('.comment-outdated');
554
523 555 if (button.html() === "(Show)") {
524 556 button.html("(Hide)");
525 557 outdated.show();
526 558 } else {
527 559 button.html("(Show)");
528 560 outdated.hide();
529 561 }
530 562 });
531 563
532 564 $('.show-inline-comments').on('change', function(e){
533 565 var show = 'none';
534 566 var target = e.currentTarget;
535 567 if(target.checked){
536 568 show = ''
537 569 }
538 570 var boxid = $(target).attr('id_for');
539 571 var comments = $('#{0} .inline-comments'.format(boxid));
540 572 var fn_display = function(idx){
541 573 $(this).css('display', show);
542 574 };
543 575 $(comments).each(fn_display);
544 576 var btns = $('#{0} .inline-comments-button'.format(boxid));
545 577 $(btns).each(fn_display);
546 578 });
547 579
548 580 $('#merge_pull_request_form').submit(function() {
549 581 if (!$('#merge_pull_request').attr('disabled')) {
550 582 $('#merge_pull_request').attr('disabled', 'disabled');
551 583 }
552 584 return true;
553 585 });
554 586
555 587 $('#edit_pull_request').on('click', function(e){
556 588 var title = $('#pr-title-input').val();
557 589 var description = codeMirrorInstance.getValue();
558 590 editPullRequest(
559 591 "${c.repo_name}", "${c.pull_request.pull_request_id}",
560 592 title, description);
561 593 });
562 594
563 595 $('#update_pull_request').on('click', function(e){
564 596 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
565 597 });
566 598
567 599 $('#update_commits').on('click', function(e){
568 600 var isDisabled = !$(e.currentTarget).attr('disabled');
569 601 $(e.currentTarget).text(_gettext('Updating...'));
570 602 $(e.currentTarget).attr('disabled', 'disabled');
571 603 if(isDisabled){
572 604 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
573 605 }
574 606
575 607 });
576 608 // fixing issue with caches on firefox
577 609 $('#update_commits').removeAttr("disabled");
578 610
579 611 $('#close_pull_request').on('click', function(e){
580 612 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
581 613 });
582 614
583 615 $('.show-inline-comments').on('click', function(e){
584 616 var boxid = $(this).attr('data-comment-id');
585 617 var button = $(this);
586 618
587 619 if(button.hasClass("comments-visible")) {
588 620 $('#{0} .inline-comments'.format(boxid)).each(function(index){
589 621 $(this).hide();
590 622 });
591 623 button.removeClass("comments-visible");
592 624 } else {
593 625 $('#{0} .inline-comments'.format(boxid)).each(function(index){
594 626 $(this).show();
595 627 });
596 628 button.addClass("comments-visible");
597 629 }
598 630 });
599 631 })
600 632 </script>
601 633
602 634 </div>
603 635 </div>
604 636
605 637 </%def>
General Comments 0
You need to be logged in to leave comments. Login now