##// END OF EJS Templates
commits/pr pages various fixes....
marcink -
r4485:ac1b264f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,134 b''
1 ## snippet for sidebar elements
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
8 <%
9 if todo_comments:
10 cls_ = 'todos-content-table'
11 def sorter(entry):
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
15 # own comments first
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 else:
19 cls_ = 'comments-content-table'
20 def sorter(entry):
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
23
24 existing_ids = existing_ids or []
25
26 %>
27
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 <%
32 display = ''
33 _cls = ''
34 %>
35
36 <%
37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 prev_comment_ver_index = 0
39 if loop_obj.previous:
40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
41
42 ver_info = None
43 if getattr(c, 'versions', []):
44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
45 %>
46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
48 <%
49 if (prev_comment_ver_index > comment_ver_index):
50 comments_ver_divider = comment_ver_index
51 else:
52 comments_ver_divider = None
53 %>
54
55 % if todo_comments:
56 % if comment_obj.resolved:
57 <% _cls = 'resolved-todo' %>
58 <% display = 'none' %>
59 % endif
60 % else:
61 ## SKIP TODOs we display them in other area
62 % if comment_obj.is_todo:
63 <% display = 'none' %>
64 % endif
65 ## Skip outdated comments
66 % if comment_obj.outdated:
67 <% display = 'none' %>
68 <% _cls = 'hidden-comment' %>
69 % endif
70 % endif
71
72 % if not todo_comments and comments_ver_divider:
73 <tr class="old-comments-marker">
74 <td colspan="3">
75 % if ver_info:
76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
77 % else:
78 <code>v${comments_ver_divider}</code>
79 % endif
80 </td>
81 </tr>
82
83 % endif
84
85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
86 <td class="td-todo-number">
87
88 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
89 href="#comment-${comment_obj.comment_id}"
90 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
91
92 <%
93 version_info = ''
94 if is_pr:
95 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
96 %>
97
98 % if todo_comments:
99 % if comment_obj.is_inline:
100 <i class="tooltip icon-code" title="Inline TODO comment${version_info}."></i>
101 % else:
102 <i class="tooltip icon-comment" title="General TODO comment${version_info}."></i>
103 % endif
104 % else:
105 % if comment_obj.outdated:
106 <i class="tooltip icon-comment-toggle" title="Inline Outdated made in v${comment_ver_index}."></i>
107 % elif comment_obj.is_inline:
108 <i class="tooltip icon-code" title="Inline comment${version_info}."></i>
109 % else:
110 <i class="tooltip icon-comment" title="General comment${version_info}."></i>
111 % endif
112 % endif
113
114 </a>
115 ## NEW, since refresh
116 % if existing_ids and comment_obj.comment_id not in existing_ids:
117 <span class="tag">NEW</span>
118 % endif
119 </td>
120
121 <td class="td-todo-gravatar">
122 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
123 </td>
124 <td class="todo-comment-text-wrapper">
125 <div class="tooltip todo-comment-text timeago ${('todo-resolved' if comment_obj.resolved else '')} " title="${h.format_date(comment_obj.created_on)}" datetime="${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}">
126 <code>${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}</code>
127 </div>
128 </td>
129 </tr>
130 % endfor
131
132 </table>
133
134 </%def> No newline at end of file
@@ -1,507 +1,494 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import pytest
22 22
23 23 from rhodecode.tests import TestController
24 24
25 25 from rhodecode.model.db import ChangesetComment, Notification
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.lib import helpers as h
28 28
29 29
30 30 def route_path(name, params=None, **kwargs):
31 31 import urllib
32 32
33 33 base_url = {
34 34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 @pytest.mark.backends("git", "hg", "svn")
47 47 class TestRepoCommitCommentsView(TestController):
48 48
49 49 @pytest.fixture(autouse=True)
50 50 def prepare(self, request, baseapp):
51 51 for x in ChangesetComment.query().all():
52 52 Session().delete(x)
53 53 Session().commit()
54 54
55 55 for x in Notification.query().all():
56 56 Session().delete(x)
57 57 Session().commit()
58 58
59 59 request.addfinalizer(self.cleanup)
60 60
61 61 def cleanup(self):
62 62 for x in ChangesetComment.query().all():
63 63 Session().delete(x)
64 64 Session().commit()
65 65
66 66 for x in Notification.query().all():
67 67 Session().delete(x)
68 68 Session().commit()
69 69
70 70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 71 def test_create(self, comment_type, backend):
72 72 self.log_user()
73 73 commit = backend.repo.get_commit('300')
74 74 commit_id = commit.raw_id
75 75 text = u'CommentOnCommit'
76 76
77 77 params = {'text': text, 'csrf_token': self.csrf_token,
78 78 'comment_type': comment_type}
79 79 self.app.post(
80 80 route_path('repo_commit_comment_create',
81 81 repo_name=backend.repo_name, commit_id=commit_id),
82 82 params=params)
83 83
84 84 response = self.app.get(
85 85 route_path('repo_commit',
86 86 repo_name=backend.repo_name, commit_id=commit_id))
87 87
88 88 # test DB
89 89 assert ChangesetComment.query().count() == 1
90 90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 91
92 92 assert Notification.query().count() == 1
93 93 assert ChangesetComment.query().count() == 1
94 94
95 95 notification = Notification.query().all()[0]
96 96
97 97 comment_id = ChangesetComment.query().first().comment_id
98 98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 99
100 100 author = notification.created_by_user.username_and_name
101 101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 102 author, comment_type, h.show_id(commit), backend.repo_name)
103 103 assert sbj == notification.subject
104 104
105 105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 106 backend.repo_name, commit_id, comment_id))
107 107 assert lnk in notification.body
108 108
109 109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 110 def test_create_inline(self, comment_type, backend):
111 111 self.log_user()
112 112 commit = backend.repo.get_commit('300')
113 113 commit_id = commit.raw_id
114 114 text = u'CommentOnCommit'
115 115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 116 line = 'n1'
117 117
118 118 params = {'text': text, 'f_path': f_path, 'line': line,
119 119 'comment_type': comment_type,
120 120 'csrf_token': self.csrf_token}
121 121
122 122 self.app.post(
123 123 route_path('repo_commit_comment_create',
124 124 repo_name=backend.repo_name, commit_id=commit_id),
125 125 params=params)
126 126
127 127 response = self.app.get(
128 128 route_path('repo_commit',
129 129 repo_name=backend.repo_name, commit_id=commit_id))
130 130
131 131 # test DB
132 132 assert ChangesetComment.query().count() == 1
133 133 assert_comment_links(response, 0, ChangesetComment.query().count())
134 134
135 135 if backend.alias == 'svn':
136 136 response.mustcontain(
137 137 '''data-f-path="vcs/commands/summary.py" '''
138 138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 139 )
140 140 if backend.alias == 'git':
141 141 response.mustcontain(
142 142 '''data-f-path="vcs/backends/hg.py" '''
143 143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 144 )
145 145
146 146 if backend.alias == 'hg':
147 147 response.mustcontain(
148 148 '''data-f-path="vcs/backends/hg.py" '''
149 149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 150 )
151 151
152 152 assert Notification.query().count() == 1
153 153 assert ChangesetComment.query().count() == 1
154 154
155 155 notification = Notification.query().all()[0]
156 156 comment = ChangesetComment.query().first()
157 157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 158
159 159 assert comment.revision == commit_id
160 160
161 161 author = notification.created_by_user.username_and_name
162 162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 164
165 165 assert sbj == notification.subject
166 166
167 167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 168 backend.repo_name, commit_id, comment.comment_id))
169 169 assert lnk in notification.body
170 170 assert 'on line n1' in notification.body
171 171
172 172 def test_create_with_mention(self, backend):
173 173 self.log_user()
174 174
175 175 commit_id = backend.repo.get_commit('300').raw_id
176 176 text = u'@test_regular check CommentOnCommit'
177 177
178 178 params = {'text': text, 'csrf_token': self.csrf_token}
179 179 self.app.post(
180 180 route_path('repo_commit_comment_create',
181 181 repo_name=backend.repo_name, commit_id=commit_id),
182 182 params=params)
183 183
184 184 response = self.app.get(
185 185 route_path('repo_commit',
186 186 repo_name=backend.repo_name, commit_id=commit_id))
187 187 # test DB
188 188 assert ChangesetComment.query().count() == 1
189 189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 190
191 191 notification = Notification.query().one()
192 192
193 193 assert len(notification.recipients) == 2
194 194 users = [x.username for x in notification.recipients]
195 195
196 196 # test_regular gets notification by @mention
197 197 assert sorted(users) == [u'test_admin', u'test_regular']
198 198
199 199 def test_create_with_status_change(self, backend):
200 200 self.log_user()
201 201 commit = backend.repo.get_commit('300')
202 202 commit_id = commit.raw_id
203 203 text = u'CommentOnCommit'
204 204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 205 line = 'n1'
206 206
207 207 params = {'text': text, 'changeset_status': 'approved',
208 208 'csrf_token': self.csrf_token}
209 209
210 210 self.app.post(
211 211 route_path(
212 212 'repo_commit_comment_create',
213 213 repo_name=backend.repo_name, commit_id=commit_id),
214 214 params=params)
215 215
216 216 response = self.app.get(
217 217 route_path('repo_commit',
218 218 repo_name=backend.repo_name, commit_id=commit_id))
219 219
220 220 # test DB
221 221 assert ChangesetComment.query().count() == 1
222 222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 223
224 224 assert Notification.query().count() == 1
225 225 assert ChangesetComment.query().count() == 1
226 226
227 227 notification = Notification.query().all()[0]
228 228
229 229 comment_id = ChangesetComment.query().first().comment_id
230 230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 231
232 232 author = notification.created_by_user.username_and_name
233 233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 234 author, h.show_id(commit), backend.repo_name)
235 235 assert sbj == notification.subject
236 236
237 237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 238 backend.repo_name, commit_id, comment_id))
239 239 assert lnk in notification.body
240 240
241 241 def test_delete(self, backend):
242 242 self.log_user()
243 243 commit_id = backend.repo.get_commit('300').raw_id
244 244 text = u'CommentOnCommit'
245 245
246 246 params = {'text': text, 'csrf_token': self.csrf_token}
247 247 self.app.post(
248 248 route_path(
249 249 'repo_commit_comment_create',
250 250 repo_name=backend.repo_name, commit_id=commit_id),
251 251 params=params)
252 252
253 253 comments = ChangesetComment.query().all()
254 254 assert len(comments) == 1
255 255 comment_id = comments[0].comment_id
256 256
257 257 self.app.post(
258 258 route_path('repo_commit_comment_delete',
259 259 repo_name=backend.repo_name,
260 260 commit_id=commit_id,
261 261 comment_id=comment_id),
262 262 params={'csrf_token': self.csrf_token})
263 263
264 264 comments = ChangesetComment.query().all()
265 265 assert len(comments) == 0
266 266
267 267 response = self.app.get(
268 268 route_path('repo_commit',
269 269 repo_name=backend.repo_name, commit_id=commit_id))
270 270 assert_comment_links(response, 0, 0)
271 271
272 272 def test_edit(self, backend):
273 273 self.log_user()
274 274 commit_id = backend.repo.get_commit('300').raw_id
275 275 text = u'CommentOnCommit'
276 276
277 277 params = {'text': text, 'csrf_token': self.csrf_token}
278 278 self.app.post(
279 279 route_path(
280 280 'repo_commit_comment_create',
281 281 repo_name=backend.repo_name, commit_id=commit_id),
282 282 params=params)
283 283
284 284 comments = ChangesetComment.query().all()
285 285 assert len(comments) == 1
286 286 comment_id = comments[0].comment_id
287 287 test_text = 'test_text'
288 288 self.app.post(
289 289 route_path(
290 290 'repo_commit_comment_edit',
291 291 repo_name=backend.repo_name,
292 292 commit_id=commit_id,
293 293 comment_id=comment_id,
294 294 ),
295 295 params={
296 296 'csrf_token': self.csrf_token,
297 297 'text': test_text,
298 298 'version': '0',
299 299 })
300 300
301 301 text_form_db = ChangesetComment.query().filter(
302 302 ChangesetComment.comment_id == comment_id).first().text
303 303 assert test_text == text_form_db
304 304
305 305 def test_edit_without_change(self, backend):
306 306 self.log_user()
307 307 commit_id = backend.repo.get_commit('300').raw_id
308 308 text = u'CommentOnCommit'
309 309
310 310 params = {'text': text, 'csrf_token': self.csrf_token}
311 311 self.app.post(
312 312 route_path(
313 313 'repo_commit_comment_create',
314 314 repo_name=backend.repo_name, commit_id=commit_id),
315 315 params=params)
316 316
317 317 comments = ChangesetComment.query().all()
318 318 assert len(comments) == 1
319 319 comment_id = comments[0].comment_id
320 320
321 321 response = self.app.post(
322 322 route_path(
323 323 'repo_commit_comment_edit',
324 324 repo_name=backend.repo_name,
325 325 commit_id=commit_id,
326 326 comment_id=comment_id,
327 327 ),
328 328 params={
329 329 'csrf_token': self.csrf_token,
330 330 'text': text,
331 331 'version': '0',
332 332 },
333 333 status=404,
334 334 )
335 335 assert response.status_int == 404
336 336
337 337 def test_edit_try_edit_already_edited(self, backend):
338 338 self.log_user()
339 339 commit_id = backend.repo.get_commit('300').raw_id
340 340 text = u'CommentOnCommit'
341 341
342 342 params = {'text': text, 'csrf_token': self.csrf_token}
343 343 self.app.post(
344 344 route_path(
345 345 'repo_commit_comment_create',
346 346 repo_name=backend.repo_name, commit_id=commit_id
347 347 ),
348 348 params=params,
349 349 )
350 350
351 351 comments = ChangesetComment.query().all()
352 352 assert len(comments) == 1
353 353 comment_id = comments[0].comment_id
354 354 test_text = 'test_text'
355 355 self.app.post(
356 356 route_path(
357 357 'repo_commit_comment_edit',
358 358 repo_name=backend.repo_name,
359 359 commit_id=commit_id,
360 360 comment_id=comment_id,
361 361 ),
362 362 params={
363 363 'csrf_token': self.csrf_token,
364 364 'text': test_text,
365 365 'version': '0',
366 366 }
367 367 )
368 368 test_text_v2 = 'test_v2'
369 369 response = self.app.post(
370 370 route_path(
371 371 'repo_commit_comment_edit',
372 372 repo_name=backend.repo_name,
373 373 commit_id=commit_id,
374 374 comment_id=comment_id,
375 375 ),
376 376 params={
377 377 'csrf_token': self.csrf_token,
378 378 'text': test_text_v2,
379 379 'version': '0',
380 380 },
381 381 status=409,
382 382 )
383 383 assert response.status_int == 409
384 384
385 385 text_form_db = ChangesetComment.query().filter(
386 386 ChangesetComment.comment_id == comment_id).first().text
387 387
388 388 assert test_text == text_form_db
389 389 assert test_text_v2 != text_form_db
390 390
391 391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 392 self.log_user()
393 393 commit_id = backend.repo.get_commit('300').raw_id
394 394 text = u'CommentOnCommit'
395 395
396 396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 397 self.app.post(
398 398 route_path(
399 399 'repo_commit_comment_create',
400 400 repo_name=backend.repo_name,
401 401 commit_id=commit_id,
402 402 ),
403 403 params=params
404 404 )
405 405
406 406 comments = ChangesetComment.query().all()
407 407 assert len(comments) == 1
408 408 comment_id = comments[0].comment_id
409 409
410 410 comment = ChangesetComment.get(comment_id)
411 411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 412 Session().add(comment)
413 413 Session().commit()
414 414
415 415 response = self.app.post(
416 416 route_path(
417 417 'repo_commit_comment_edit',
418 418 repo_name=backend.repo_name,
419 419 commit_id=commit_id,
420 420 comment_id=comment_id,
421 421 ),
422 422 params={
423 423 'csrf_token': self.csrf_token,
424 424 'text': 'test_text',
425 425 },
426 426 status=403,
427 427 )
428 428 assert response.status_int == 403
429 429
430 430 def test_delete_forbidden_for_immutable_comments(self, backend):
431 431 self.log_user()
432 432 commit_id = backend.repo.get_commit('300').raw_id
433 433 text = u'CommentOnCommit'
434 434
435 435 params = {'text': text, 'csrf_token': self.csrf_token}
436 436 self.app.post(
437 437 route_path(
438 438 'repo_commit_comment_create',
439 439 repo_name=backend.repo_name, commit_id=commit_id),
440 440 params=params)
441 441
442 442 comments = ChangesetComment.query().all()
443 443 assert len(comments) == 1
444 444 comment_id = comments[0].comment_id
445 445
446 446 comment = ChangesetComment.get(comment_id)
447 447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 448 Session().add(comment)
449 449 Session().commit()
450 450
451 451 self.app.post(
452 452 route_path('repo_commit_comment_delete',
453 453 repo_name=backend.repo_name,
454 454 commit_id=commit_id,
455 455 comment_id=comment_id),
456 456 params={'csrf_token': self.csrf_token},
457 457 status=403)
458 458
459 459 @pytest.mark.parametrize('renderer, text_input, output', [
460 460 ('rst', 'plain text', '<p>plain text</p>'),
461 461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 462 ('rst', '*italics*', '<em>italics</em>'),
463 463 ('rst', '**bold**', '<strong>bold</strong>'),
464 464 ('markdown', 'plain text', '<p>plain text</p>'),
465 465 ('markdown', '# header', '<h1>header</h1>'),
466 466 ('markdown', '*italics*', '<em>italics</em>'),
467 467 ('markdown', '**bold**', '<strong>bold</strong>'),
468 468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 469 'md-header', 'md-italics', 'md-bold', ])
470 470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 471 self.log_user()
472 472 params = {
473 473 'renderer': renderer,
474 474 'text': text_input,
475 475 'csrf_token': self.csrf_token
476 476 }
477 477 commit_id = '0' * 16 # fake this for tests
478 478 response = self.app.post(
479 479 route_path('repo_commit_comment_preview',
480 480 repo_name=backend.repo_name, commit_id=commit_id,),
481 481 params=params,
482 482 extra_environ=xhr_header)
483 483
484 484 response.mustcontain(output)
485 485
486 486
487 487 def assert_comment_links(response, comments, inline_comments):
488 if comments == 1:
489 comments_text = "%d General" % comments
490 else:
491 comments_text = "%d General" % comments
492
493 if inline_comments == 1:
494 inline_comments_text = "%d Inline" % inline_comments
495 else:
496 inline_comments_text = "%d Inline" % inline_comments
488 response.mustcontain(
489 '<span class="display-none" id="general-comments-count">{}</span>'.format(comments))
490 response.mustcontain(
491 '<span class="display-none" id="inline-comments-count">{}</span>'.format(inline_comments))
497 492
498 if comments:
499 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
500 else:
501 response.mustcontain(comments_text)
502 493
503 if inline_comments:
504 response.mustcontain(
505 'id="inline-comments-counter">%s' % inline_comments_text)
506 else:
507 response.mustcontain(inline_comments_text)
494
@@ -1,725 +1,782 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
22 21 import logging
22 import collections
23 23
24 24 from pyramid.httpexceptions import (
25 25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31 from rhodecode.apps.file_store import utils as store_utils
32 32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33 33
34 34 from rhodecode.lib import diffs, codeblocks
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.compat import OrderedDict
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 49 ChangesetCommentHistory
50 50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 51 from rhodecode.model.comment import CommentsModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 def _update_with_GET(params, request):
59 59 for k in ['diff1', 'diff2', 'diff']:
60 60 params[k] += request.GET.getall(k)
61 61
62 62
63 63 class RepoCommitsView(RepoAppView):
64 64 def load_default_context(self):
65 65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 66 c.rhodecode_repo = self.rhodecode_vcs_repo
67 67
68 68 return c
69 69
70 70 def _is_diff_cache_enabled(self, target_repo):
71 71 caching_enabled = self._get_general_setting(
72 72 target_repo, 'rhodecode_diff_cache')
73 73 log.debug('Diff caching enabled: %s', caching_enabled)
74 74 return caching_enabled
75 75
76 76 def _commit(self, commit_id_range, method):
77 77 _ = self.request.translate
78 78 c = self.load_default_context()
79 79 c.fulldiff = self.request.GET.get('fulldiff')
80 80
81 81 # fetch global flags of ignore ws or context lines
82 82 diff_context = get_diff_context(self.request)
83 83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84 84
85 85 # diff_limit will cut off the whole diff if the limit is applied
86 86 # otherwise it will just hide the big files from the front-end
87 87 diff_limit = c.visual.cut_off_limit_diff
88 88 file_limit = c.visual.cut_off_limit_file
89 89
90
91 90 # get ranges of commit ids if preset
92 91 commit_range = commit_id_range.split('...')[:2]
93 92
94 93 try:
95 94 pre_load = ['affected_files', 'author', 'branch', 'date',
96 95 'message', 'parents']
97 96 if self.rhodecode_vcs_repo.alias == 'hg':
98 97 pre_load += ['hidden', 'obsolete', 'phase']
99 98
100 99 if len(commit_range) == 2:
101 100 commits = self.rhodecode_vcs_repo.get_commits(
102 101 start_id=commit_range[0], end_id=commit_range[1],
103 102 pre_load=pre_load, translate_tags=False)
104 103 commits = list(commits)
105 104 else:
106 105 commits = [self.rhodecode_vcs_repo.get_commit(
107 106 commit_id=commit_id_range, pre_load=pre_load)]
108 107
109 108 c.commit_ranges = commits
110 109 if not c.commit_ranges:
111 110 raise RepositoryError('The commit range returned an empty result')
112 111 except CommitDoesNotExistError as e:
113 112 msg = _('No such commit exists. Org exception: `{}`').format(e)
114 113 h.flash(msg, category='error')
115 114 raise HTTPNotFound()
116 115 except Exception:
117 116 log.exception("General failure")
118 117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
119 119
120 120 c.changes = OrderedDict()
121 121 c.lines_added = 0
122 122 c.lines_deleted = 0
123 123
124 124 # auto collapse if we have more than limit
125 125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 127
128 128 c.commit_statuses = ChangesetStatus.STATUSES
129 129 c.inline_comments = []
130 130 c.files = []
131 131
132 c.statuses = []
133 132 c.comments = []
134 133 c.unresolved_comments = []
135 134 c.resolved_comments = []
136 if len(c.commit_ranges) == 1:
135
136 # Single commit
137 if single_commit:
137 138 commit = c.commit_ranges[0]
138 139 c.comments = CommentsModel().get_comments(
139 140 self.db_repo.repo_id,
140 141 revision=commit.raw_id)
141 c.statuses.append(ChangesetStatusModel().get_status(
142 self.db_repo.repo_id, commit.raw_id))
142
143 143 # comments from PR
144 144 statuses = ChangesetStatusModel().get_statuses(
145 145 self.db_repo.repo_id, commit.raw_id,
146 146 with_revisions=True)
147 prs = set(st.pull_request for st in statuses
148 if st.pull_request is not None)
147
148 prs = set()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
152
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
156
157 # extract reviewers
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
161 StrictAttributeDict({
162 'user': c_status.author,
163
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
166 'mandatory': False,
167 'reasons': [],
168 'rule_user_group_data': lambda: None
169 })
170 )
171 reviewers_duplicates.add(_user_id)
172
173 c.allowed_reviewers = reviewers
149 174 # from associated statuses, check the pull requests, and
150 175 # show comments from them
151 176 for pr in prs:
152 177 c.comments.extend(pr.comments)
153 178
154 179 c.unresolved_comments = CommentsModel()\
155 180 .get_commit_unresolved_todos(commit.raw_id)
156 181 c.resolved_comments = CommentsModel()\
157 182 .get_commit_resolved_todos(commit.raw_id)
158 183
184 c.inline_comments_flat = CommentsModel()\
185 .get_commit_inline_comments(commit.raw_id)
186
187 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
188 statuses, reviewers)
189
190 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
191
192 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
193
194 for review_obj, member, reasons, mandatory, status in review_statuses:
195 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
197 user_group=None
198 )
199
200 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
201 member_reviewer['review_status'] = current_review_status
202 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
203 member_reviewer['allowed_to_update'] = False
204 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
205
206 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
207
208 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
211 c.repo_name,
212 commit.raw_id
213 )
214
159 215 diff = None
160 216 # Iterate over ranges (default commit view is always one commit)
161 217 for commit in c.commit_ranges:
162 218 c.changes[commit.raw_id] = []
163 219
164 220 commit2 = commit
165 221 commit1 = commit.first_parent
166 222
167 223 if method == 'show':
168 224 inline_comments = CommentsModel().get_inline_comments(
169 225 self.db_repo.repo_id, revision=commit.raw_id)
170 226 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
171 227 inline_comments))
172 228 c.inline_comments = inline_comments
173 229
174 230 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
175 231 self.db_repo)
176 232 cache_file_path = diff_cache_exist(
177 233 cache_path, 'diff', commit.raw_id,
178 234 hide_whitespace_changes, diff_context, c.fulldiff)
179 235
180 236 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
181 237 force_recache = str2bool(self.request.GET.get('force_recache'))
182 238
183 239 cached_diff = None
184 240 if caching_enabled:
185 241 cached_diff = load_cached_diff(cache_file_path)
186 242
187 243 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
188 244 if not force_recache and has_proper_diff_cache:
189 245 diffset = cached_diff['diff']
190 246 else:
191 247 vcs_diff = self.rhodecode_vcs_repo.get_diff(
192 248 commit1, commit2,
193 249 ignore_whitespace=hide_whitespace_changes,
194 250 context=diff_context)
195 251
196 252 diff_processor = diffs.DiffProcessor(
197 253 vcs_diff, format='newdiff', diff_limit=diff_limit,
198 254 file_limit=file_limit, show_full_diff=c.fulldiff)
199 255
200 256 _parsed = diff_processor.prepare()
201 257
202 258 diffset = codeblocks.DiffSet(
203 259 repo_name=self.db_repo_name,
204 260 source_node_getter=codeblocks.diffset_node_getter(commit1),
205 261 target_node_getter=codeblocks.diffset_node_getter(commit2))
206 262
207 263 diffset = self.path_filter.render_patchset_filtered(
208 264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
209 265
210 266 # save cached diff
211 267 if caching_enabled:
212 268 cache_diff(cache_file_path, diffset, None)
213 269
214 270 c.limited_diff = diffset.limited_diff
215 271 c.changes[commit.raw_id] = diffset
216 272 else:
217 273 # TODO(marcink): no cache usage here...
218 274 _diff = self.rhodecode_vcs_repo.get_diff(
219 275 commit1, commit2,
220 276 ignore_whitespace=hide_whitespace_changes, context=diff_context)
221 277 diff_processor = diffs.DiffProcessor(
222 278 _diff, format='newdiff', diff_limit=diff_limit,
223 279 file_limit=file_limit, show_full_diff=c.fulldiff)
224 280 # downloads/raw we only need RAW diff nothing else
225 281 diff = self.path_filter.get_raw_patch(diff_processor)
226 282 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
227 283
228 284 # sort comments by how they were generated
229 285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
230 286 c.at_version_num = None
231 287
232 288 if len(c.commit_ranges) == 1:
233 289 c.commit = c.commit_ranges[0]
234 290 c.parent_tmpl = ''.join(
235 291 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
236 292
237 293 if method == 'download':
238 294 response = Response(diff)
239 295 response.content_type = 'text/plain'
240 296 response.content_disposition = (
241 297 'attachment; filename=%s.diff' % commit_id_range[:12])
242 298 return response
243 299 elif method == 'patch':
244 300 c.diff = safe_unicode(diff)
245 301 patch = render(
246 302 'rhodecode:templates/changeset/patch_changeset.mako',
247 303 self._get_template_context(c), self.request)
248 304 response = Response(patch)
249 305 response.content_type = 'text/plain'
250 306 return response
251 307 elif method == 'raw':
252 308 response = Response(diff)
253 309 response.content_type = 'text/plain'
254 310 return response
255 311 elif method == 'show':
256 312 if len(c.commit_ranges) == 1:
257 313 html = render(
258 314 'rhodecode:templates/changeset/changeset.mako',
259 315 self._get_template_context(c), self.request)
260 316 return Response(html)
261 317 else:
262 318 c.ancestor = None
263 319 c.target_repo = self.db_repo
264 320 html = render(
265 321 'rhodecode:templates/changeset/changeset_range.mako',
266 322 self._get_template_context(c), self.request)
267 323 return Response(html)
268 324
269 325 raise HTTPBadRequest()
270 326
271 327 @LoginRequired()
272 328 @HasRepoPermissionAnyDecorator(
273 329 'repository.read', 'repository.write', 'repository.admin')
274 330 @view_config(
275 331 route_name='repo_commit', request_method='GET',
276 332 renderer=None)
277 333 def repo_commit_show(self):
278 334 commit_id = self.request.matchdict['commit_id']
279 335 return self._commit(commit_id, method='show')
280 336
281 337 @LoginRequired()
282 338 @HasRepoPermissionAnyDecorator(
283 339 'repository.read', 'repository.write', 'repository.admin')
284 340 @view_config(
285 341 route_name='repo_commit_raw', request_method='GET',
286 342 renderer=None)
287 343 @view_config(
288 344 route_name='repo_commit_raw_deprecated', request_method='GET',
289 345 renderer=None)
290 346 def repo_commit_raw(self):
291 347 commit_id = self.request.matchdict['commit_id']
292 348 return self._commit(commit_id, method='raw')
293 349
294 350 @LoginRequired()
295 351 @HasRepoPermissionAnyDecorator(
296 352 'repository.read', 'repository.write', 'repository.admin')
297 353 @view_config(
298 354 route_name='repo_commit_patch', request_method='GET',
299 355 renderer=None)
300 356 def repo_commit_patch(self):
301 357 commit_id = self.request.matchdict['commit_id']
302 358 return self._commit(commit_id, method='patch')
303 359
304 360 @LoginRequired()
305 361 @HasRepoPermissionAnyDecorator(
306 362 'repository.read', 'repository.write', 'repository.admin')
307 363 @view_config(
308 364 route_name='repo_commit_download', request_method='GET',
309 365 renderer=None)
310 366 def repo_commit_download(self):
311 367 commit_id = self.request.matchdict['commit_id']
312 368 return self._commit(commit_id, method='download')
313 369
314 370 @LoginRequired()
315 371 @NotAnonymous()
316 372 @HasRepoPermissionAnyDecorator(
317 373 'repository.read', 'repository.write', 'repository.admin')
318 374 @CSRFRequired()
319 375 @view_config(
320 376 route_name='repo_commit_comment_create', request_method='POST',
321 377 renderer='json_ext')
322 378 def repo_commit_comment_create(self):
323 379 _ = self.request.translate
324 380 commit_id = self.request.matchdict['commit_id']
325 381
326 382 c = self.load_default_context()
327 383 status = self.request.POST.get('changeset_status', None)
328 384 text = self.request.POST.get('text')
329 385 comment_type = self.request.POST.get('comment_type')
330 386 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
331 387
332 388 if status:
333 389 text = text or (_('Status change %(transition_icon)s %(status)s')
334 390 % {'transition_icon': '>',
335 391 'status': ChangesetStatus.get_status_lbl(status)})
336 392
337 393 multi_commit_ids = []
338 394 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
339 395 if _commit_id not in ['', None, EmptyCommit.raw_id]:
340 396 if _commit_id not in multi_commit_ids:
341 397 multi_commit_ids.append(_commit_id)
342 398
343 399 commit_ids = multi_commit_ids or [commit_id]
344 400
345 401 comment = None
346 402 for current_id in filter(None, commit_ids):
347 403 comment = CommentsModel().create(
348 404 text=text,
349 405 repo=self.db_repo.repo_id,
350 406 user=self._rhodecode_db_user.user_id,
351 407 commit_id=current_id,
352 408 f_path=self.request.POST.get('f_path'),
353 409 line_no=self.request.POST.get('line'),
354 410 status_change=(ChangesetStatus.get_status_lbl(status)
355 411 if status else None),
356 412 status_change_type=status,
357 413 comment_type=comment_type,
358 414 resolves_comment_id=resolves_comment_id,
359 415 auth_user=self._rhodecode_user
360 416 )
361 417
362 418 # get status if set !
363 419 if status:
364 420 # if latest status was from pull request and it's closed
365 421 # disallow changing status !
366 422 # dont_allow_on_closed_pull_request = True !
367 423
368 424 try:
369 425 ChangesetStatusModel().set_status(
370 426 self.db_repo.repo_id,
371 427 status,
372 428 self._rhodecode_db_user.user_id,
373 429 comment,
374 430 revision=current_id,
375 431 dont_allow_on_closed_pull_request=True
376 432 )
377 433 except StatusChangeOnClosedPullRequestError:
378 434 msg = _('Changing the status of a commit associated with '
379 435 'a closed pull request is not allowed')
380 436 log.exception(msg)
381 437 h.flash(msg, category='warning')
382 438 raise HTTPFound(h.route_path(
383 439 'repo_commit', repo_name=self.db_repo_name,
384 440 commit_id=current_id))
385 441
386 442 commit = self.db_repo.get_commit(current_id)
387 443 CommentsModel().trigger_commit_comment_hook(
388 444 self.db_repo, self._rhodecode_user, 'create',
389 445 data={'comment': comment, 'commit': commit})
390 446
391 447 # finalize, commit and redirect
392 448 Session().commit()
393 449
394 450 data = {
395 451 'target_id': h.safeid(h.safe_unicode(
396 452 self.request.POST.get('f_path'))),
397 453 }
398 454 if comment:
399 455 c.co = comment
456 c.at_version_num = 0
400 457 rendered_comment = render(
401 458 'rhodecode:templates/changeset/changeset_comment_block.mako',
402 459 self._get_template_context(c), self.request)
403 460
404 461 data.update(comment.get_dict())
405 462 data.update({'rendered_text': rendered_comment})
406 463
407 464 return data
408 465
409 466 @LoginRequired()
410 467 @NotAnonymous()
411 468 @HasRepoPermissionAnyDecorator(
412 469 'repository.read', 'repository.write', 'repository.admin')
413 470 @CSRFRequired()
414 471 @view_config(
415 472 route_name='repo_commit_comment_preview', request_method='POST',
416 473 renderer='string', xhr=True)
417 474 def repo_commit_comment_preview(self):
418 475 # Technically a CSRF token is not needed as no state changes with this
419 476 # call. However, as this is a POST is better to have it, so automated
420 477 # tools don't flag it as potential CSRF.
421 478 # Post is required because the payload could be bigger than the maximum
422 479 # allowed by GET.
423 480
424 481 text = self.request.POST.get('text')
425 482 renderer = self.request.POST.get('renderer') or 'rst'
426 483 if text:
427 484 return h.render(text, renderer=renderer, mentions=True,
428 485 repo_name=self.db_repo_name)
429 486 return ''
430 487
431 488 @LoginRequired()
432 489 @NotAnonymous()
433 490 @HasRepoPermissionAnyDecorator(
434 491 'repository.read', 'repository.write', 'repository.admin')
435 492 @CSRFRequired()
436 493 @view_config(
437 494 route_name='repo_commit_comment_history_view', request_method='POST',
438 495 renderer='string', xhr=True)
439 496 def repo_commit_comment_history_view(self):
440 497 c = self.load_default_context()
441 498
442 499 comment_history_id = self.request.matchdict['comment_history_id']
443 500 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
444 501 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
445 502
446 503 if is_repo_comment:
447 504 c.comment_history = comment_history
448 505
449 506 rendered_comment = render(
450 507 'rhodecode:templates/changeset/comment_history.mako',
451 508 self._get_template_context(c)
452 509 , self.request)
453 510 return rendered_comment
454 511 else:
455 512 log.warning('No permissions for user %s to show comment_history_id: %s',
456 513 self._rhodecode_db_user, comment_history_id)
457 514 raise HTTPNotFound()
458 515
459 516 @LoginRequired()
460 517 @NotAnonymous()
461 518 @HasRepoPermissionAnyDecorator(
462 519 'repository.read', 'repository.write', 'repository.admin')
463 520 @CSRFRequired()
464 521 @view_config(
465 522 route_name='repo_commit_comment_attachment_upload', request_method='POST',
466 523 renderer='json_ext', xhr=True)
467 524 def repo_commit_comment_attachment_upload(self):
468 525 c = self.load_default_context()
469 526 upload_key = 'attachment'
470 527
471 528 file_obj = self.request.POST.get(upload_key)
472 529
473 530 if file_obj is None:
474 531 self.request.response.status = 400
475 532 return {'store_fid': None,
476 533 'access_path': None,
477 534 'error': '{} data field is missing'.format(upload_key)}
478 535
479 536 if not hasattr(file_obj, 'filename'):
480 537 self.request.response.status = 400
481 538 return {'store_fid': None,
482 539 'access_path': None,
483 540 'error': 'filename cannot be read from the data field'}
484 541
485 542 filename = file_obj.filename
486 543 file_display_name = filename
487 544
488 545 metadata = {
489 546 'user_uploaded': {'username': self._rhodecode_user.username,
490 547 'user_id': self._rhodecode_user.user_id,
491 548 'ip': self._rhodecode_user.ip_addr}}
492 549
493 550 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
494 551 allowed_extensions = [
495 552 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
496 553 '.pptx', '.txt', '.xlsx', '.zip']
497 554 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
498 555
499 556 try:
500 557 storage = store_utils.get_file_storage(self.request.registry.settings)
501 558 store_uid, metadata = storage.save_file(
502 559 file_obj.file, filename, extra_metadata=metadata,
503 560 extensions=allowed_extensions, max_filesize=max_file_size)
504 561 except FileNotAllowedException:
505 562 self.request.response.status = 400
506 563 permitted_extensions = ', '.join(allowed_extensions)
507 564 error_msg = 'File `{}` is not allowed. ' \
508 565 'Only following extensions are permitted: {}'.format(
509 566 filename, permitted_extensions)
510 567 return {'store_fid': None,
511 568 'access_path': None,
512 569 'error': error_msg}
513 570 except FileOverSizeException:
514 571 self.request.response.status = 400
515 572 limit_mb = h.format_byte_size_binary(max_file_size)
516 573 return {'store_fid': None,
517 574 'access_path': None,
518 575 'error': 'File {} is exceeding allowed limit of {}.'.format(
519 576 filename, limit_mb)}
520 577
521 578 try:
522 579 entry = FileStore.create(
523 580 file_uid=store_uid, filename=metadata["filename"],
524 581 file_hash=metadata["sha256"], file_size=metadata["size"],
525 582 file_display_name=file_display_name,
526 583 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
527 584 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
528 585 scope_repo_id=self.db_repo.repo_id
529 586 )
530 587 Session().add(entry)
531 588 Session().commit()
532 589 log.debug('Stored upload in DB as %s', entry)
533 590 except Exception:
534 591 log.exception('Failed to store file %s', filename)
535 592 self.request.response.status = 400
536 593 return {'store_fid': None,
537 594 'access_path': None,
538 595 'error': 'File {} failed to store in DB.'.format(filename)}
539 596
540 597 Session().commit()
541 598
542 599 return {
543 600 'store_fid': store_uid,
544 601 'access_path': h.route_path(
545 602 'download_file', fid=store_uid),
546 603 'fqn_access_path': h.route_url(
547 604 'download_file', fid=store_uid),
548 605 'repo_access_path': h.route_path(
549 606 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
550 607 'repo_fqn_access_path': h.route_url(
551 608 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
552 609 }
553 610
554 611 @LoginRequired()
555 612 @NotAnonymous()
556 613 @HasRepoPermissionAnyDecorator(
557 614 'repository.read', 'repository.write', 'repository.admin')
558 615 @CSRFRequired()
559 616 @view_config(
560 617 route_name='repo_commit_comment_delete', request_method='POST',
561 618 renderer='json_ext')
562 619 def repo_commit_comment_delete(self):
563 620 commit_id = self.request.matchdict['commit_id']
564 621 comment_id = self.request.matchdict['comment_id']
565 622
566 623 comment = ChangesetComment.get_or_404(comment_id)
567 624 if not comment:
568 625 log.debug('Comment with id:%s not found, skipping', comment_id)
569 626 # comment already deleted in another call probably
570 627 return True
571 628
572 629 if comment.immutable:
573 630 # don't allow deleting comments that are immutable
574 631 raise HTTPForbidden()
575 632
576 633 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
577 634 super_admin = h.HasPermissionAny('hg.admin')()
578 635 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
579 636 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
580 637 comment_repo_admin = is_repo_admin and is_repo_comment
581 638
582 639 if super_admin or comment_owner or comment_repo_admin:
583 640 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
584 641 Session().commit()
585 642 return True
586 643 else:
587 644 log.warning('No permissions for user %s to delete comment_id: %s',
588 645 self._rhodecode_db_user, comment_id)
589 646 raise HTTPNotFound()
590 647
591 648 @LoginRequired()
592 649 @NotAnonymous()
593 650 @HasRepoPermissionAnyDecorator(
594 651 'repository.read', 'repository.write', 'repository.admin')
595 652 @CSRFRequired()
596 653 @view_config(
597 654 route_name='repo_commit_comment_edit', request_method='POST',
598 655 renderer='json_ext')
599 656 def repo_commit_comment_edit(self):
600 657 self.load_default_context()
601 658
602 659 comment_id = self.request.matchdict['comment_id']
603 660 comment = ChangesetComment.get_or_404(comment_id)
604 661
605 662 if comment.immutable:
606 663 # don't allow deleting comments that are immutable
607 664 raise HTTPForbidden()
608 665
609 666 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
610 667 super_admin = h.HasPermissionAny('hg.admin')()
611 668 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
612 669 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
613 670 comment_repo_admin = is_repo_admin and is_repo_comment
614 671
615 672 if super_admin or comment_owner or comment_repo_admin:
616 673 text = self.request.POST.get('text')
617 674 version = self.request.POST.get('version')
618 675 if text == comment.text:
619 676 log.warning(
620 677 'Comment(repo): '
621 678 'Trying to create new version '
622 679 'with the same comment body {}'.format(
623 680 comment_id,
624 681 )
625 682 )
626 683 raise HTTPNotFound()
627 684
628 685 if version.isdigit():
629 686 version = int(version)
630 687 else:
631 688 log.warning(
632 689 'Comment(repo): Wrong version type {} {} '
633 690 'for comment {}'.format(
634 691 version,
635 692 type(version),
636 693 comment_id,
637 694 )
638 695 )
639 696 raise HTTPNotFound()
640 697
641 698 try:
642 699 comment_history = CommentsModel().edit(
643 700 comment_id=comment_id,
644 701 text=text,
645 702 auth_user=self._rhodecode_user,
646 703 version=version,
647 704 )
648 705 except CommentVersionMismatch:
649 706 raise HTTPConflict()
650 707
651 708 if not comment_history:
652 709 raise HTTPNotFound()
653 710
654 711 commit_id = self.request.matchdict['commit_id']
655 712 commit = self.db_repo.get_commit(commit_id)
656 713 CommentsModel().trigger_commit_comment_hook(
657 714 self.db_repo, self._rhodecode_user, 'edit',
658 715 data={'comment': comment, 'commit': commit})
659 716
660 717 Session().commit()
661 718 return {
662 719 'comment_history_id': comment_history.comment_history_id,
663 720 'comment_id': comment.comment_id,
664 721 'comment_version': comment_history.version,
665 722 'comment_author_username': comment_history.author.username,
666 723 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
667 724 'comment_created_on': h.age_component(comment_history.created_on,
668 725 time_is_local=True),
669 726 }
670 727 else:
671 728 log.warning('No permissions for user %s to edit comment_id: %s',
672 729 self._rhodecode_db_user, comment_id)
673 730 raise HTTPNotFound()
674 731
675 732 @LoginRequired()
676 733 @HasRepoPermissionAnyDecorator(
677 734 'repository.read', 'repository.write', 'repository.admin')
678 735 @view_config(
679 736 route_name='repo_commit_data', request_method='GET',
680 737 renderer='json_ext', xhr=True)
681 738 def repo_commit_data(self):
682 739 commit_id = self.request.matchdict['commit_id']
683 740 self.load_default_context()
684 741
685 742 try:
686 743 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
687 744 except CommitDoesNotExistError as e:
688 745 return EmptyCommit(message=str(e))
689 746
690 747 @LoginRequired()
691 748 @HasRepoPermissionAnyDecorator(
692 749 'repository.read', 'repository.write', 'repository.admin')
693 750 @view_config(
694 751 route_name='repo_commit_children', request_method='GET',
695 752 renderer='json_ext', xhr=True)
696 753 def repo_commit_children(self):
697 754 commit_id = self.request.matchdict['commit_id']
698 755 self.load_default_context()
699 756
700 757 try:
701 758 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
702 759 children = commit.children
703 760 except CommitDoesNotExistError:
704 761 children = []
705 762
706 763 result = {"results": children}
707 764 return result
708 765
709 766 @LoginRequired()
710 767 @HasRepoPermissionAnyDecorator(
711 768 'repository.read', 'repository.write', 'repository.admin')
712 769 @view_config(
713 770 route_name='repo_commit_parents', request_method='GET',
714 771 renderer='json_ext')
715 772 def repo_commit_parents(self):
716 773 commit_id = self.request.matchdict['commit_id']
717 774 self.load_default_context()
718 775
719 776 try:
720 777 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
721 778 parents = commit.parents
722 779 except CommitDoesNotExistError:
723 780 parents = []
724 781 result = {"results": parents}
725 782 return result
@@ -1,1753 +1,1757 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (
49 49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 81 statuses=statuses, offset=start, length=limit,
82 82 order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 85 opened_by=opened_by)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by,
91 91 order_dir=order_dir)
92 92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 94 statuses=statuses, opened_by=opened_by)
95 95 else:
96 96 pull_requests = PullRequestModel().get_all(
97 97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 98 statuses=statuses, offset=start, length=limit,
99 99 order_by=order_by, order_dir=order_dir)
100 100 pull_requests_total_count = PullRequestModel().count_all(
101 101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 102 opened_by=opened_by)
103 103
104 104 data = []
105 105 comments_model = CommentsModel()
106 106 for pr in pull_requests:
107 107 comments = comments_model.get_all_comments(
108 108 self.db_repo.repo_id, pull_request=pr)
109 109
110 110 data.append({
111 111 'name': _render('pullrequest_name',
112 112 pr.pull_request_id, pr.pull_request_state,
113 113 pr.work_in_progress, pr.target_repo.repo_name),
114 114 'name_raw': pr.pull_request_id,
115 115 'status': _render('pullrequest_status',
116 116 pr.calculated_review_status()),
117 117 'title': _render('pullrequest_title', pr.title, pr.description),
118 118 'description': h.escape(pr.description),
119 119 'updated_on': _render('pullrequest_updated_on',
120 120 h.datetime_to_time(pr.updated_on)),
121 121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 122 'created_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.created_on)),
124 124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 125 'state': pr.pull_request_state,
126 126 'author': _render('pullrequest_author',
127 127 pr.author.full_contact, ),
128 128 'author_raw': pr.author.full_name,
129 129 'comments': _render('pullrequest_comments', len(comments)),
130 130 'comments_raw': len(comments),
131 131 'closed': pr.is_closed(),
132 132 })
133 133
134 134 data = ({
135 135 'draw': draw,
136 136 'data': data,
137 137 'recordsTotal': pull_requests_total_count,
138 138 'recordsFiltered': pull_requests_total_count,
139 139 })
140 140 return data
141 141
142 142 @LoginRequired()
143 143 @HasRepoPermissionAnyDecorator(
144 144 'repository.read', 'repository.write', 'repository.admin')
145 145 @view_config(
146 146 route_name='pullrequest_show_all', request_method='GET',
147 147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 148 def pull_request_list(self):
149 149 c = self.load_default_context()
150 150
151 151 req_get = self.request.GET
152 152 c.source = str2bool(req_get.get('source'))
153 153 c.closed = str2bool(req_get.get('closed'))
154 154 c.my = str2bool(req_get.get('my'))
155 155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 157
158 158 c.active = 'open'
159 159 if c.my:
160 160 c.active = 'my'
161 161 if c.closed:
162 162 c.active = 'closed'
163 163 if c.awaiting_review and not c.source:
164 164 c.active = 'awaiting'
165 165 if c.source and not c.awaiting_review:
166 166 c.active = 'source'
167 167 if c.awaiting_my_review:
168 168 c.active = 'awaiting_my'
169 169
170 170 return self._get_template_context(c)
171 171
172 172 @LoginRequired()
173 173 @HasRepoPermissionAnyDecorator(
174 174 'repository.read', 'repository.write', 'repository.admin')
175 175 @view_config(
176 176 route_name='pullrequest_show_all_data', request_method='GET',
177 177 renderer='json_ext', xhr=True)
178 178 def pull_request_list_data(self):
179 179 self.load_default_context()
180 180
181 181 # additional filters
182 182 req_get = self.request.GET
183 183 source = str2bool(req_get.get('source'))
184 184 closed = str2bool(req_get.get('closed'))
185 185 my = str2bool(req_get.get('my'))
186 186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 188
189 189 filter_type = 'awaiting_review' if awaiting_review \
190 190 else 'awaiting_my_review' if awaiting_my_review \
191 191 else None
192 192
193 193 opened_by = None
194 194 if my:
195 195 opened_by = [self._rhodecode_user.user_id]
196 196
197 197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 198 if closed:
199 199 statuses = [PullRequest.STATUS_CLOSED]
200 200
201 201 data = self._get_pull_requests_list(
202 202 repo_name=self.db_repo_name, source=source,
203 203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 204
205 205 return data
206 206
207 207 def _is_diff_cache_enabled(self, target_repo):
208 208 caching_enabled = self._get_general_setting(
209 209 target_repo, 'rhodecode_diff_cache')
210 210 log.debug('Diff caching enabled: %s', caching_enabled)
211 211 return caching_enabled
212 212
213 213 def _get_diffset(self, source_repo_name, source_repo,
214 214 ancestor_commit,
215 215 source_ref_id, target_ref_id,
216 216 target_commit, source_commit, diff_limit, file_limit,
217 217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 218
219 219 if use_ancestor:
220 220 # we might want to not use it for versions
221 221 target_ref_id = ancestor_commit.raw_id
222 222
223 223 vcs_diff = PullRequestModel().get_diff(
224 224 source_repo, source_ref_id, target_ref_id,
225 225 hide_whitespace_changes, diff_context)
226 226
227 227 diff_processor = diffs.DiffProcessor(
228 228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 229 file_limit=file_limit, show_full_diff=fulldiff)
230 230
231 231 _parsed = diff_processor.prepare()
232 232
233 233 diffset = codeblocks.DiffSet(
234 234 repo_name=self.db_repo_name,
235 235 source_repo_name=source_repo_name,
236 236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 238 )
239 239 diffset = self.path_filter.render_patchset_filtered(
240 240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 241
242 242 return diffset
243 243
244 244 def _get_range_diffset(self, source_scm, source_repo,
245 245 commit1, commit2, diff_limit, file_limit,
246 246 fulldiff, hide_whitespace_changes, diff_context):
247 247 vcs_diff = source_scm.get_diff(
248 248 commit1, commit2,
249 249 ignore_whitespace=hide_whitespace_changes,
250 250 context=diff_context)
251 251
252 252 diff_processor = diffs.DiffProcessor(
253 253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 254 file_limit=file_limit, show_full_diff=fulldiff)
255 255
256 256 _parsed = diff_processor.prepare()
257 257
258 258 diffset = codeblocks.DiffSet(
259 259 repo_name=source_repo.repo_name,
260 260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 262
263 263 diffset = self.path_filter.render_patchset_filtered(
264 264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 265
266 266 return diffset
267 267
268 268 def register_comments_vars(self, c, pull_request, versions):
269 269 comments_model = CommentsModel()
270 270
271 271 # GENERAL COMMENTS with versions #
272 272 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 273 q = q.order_by(ChangesetComment.comment_id.asc())
274 274 general_comments = q
275 275
276 276 # pick comments we want to render at current version
277 277 c.comment_versions = comments_model.aggregate_comments(
278 278 general_comments, versions, c.at_version_num)
279 279
280 280 # INLINE COMMENTS with versions #
281 281 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 282 q = q.order_by(ChangesetComment.comment_id.asc())
283 283 inline_comments = q
284 284
285 285 c.inline_versions = comments_model.aggregate_comments(
286 286 inline_comments, versions, c.at_version_num, inline=True)
287 287
288 288 # Comments inline+general
289 289 if c.at_version:
290 290 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 291 c.comments = c.comment_versions[c.at_version_num]['display']
292 292 else:
293 293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 294 c.comments = c.comment_versions[c.at_version_num]['until']
295 295
296 296 return general_comments, inline_comments
297 297
298 298 @LoginRequired()
299 299 @HasRepoPermissionAnyDecorator(
300 300 'repository.read', 'repository.write', 'repository.admin')
301 301 @view_config(
302 302 route_name='pullrequest_show', request_method='GET',
303 303 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
304 304 def pull_request_show(self):
305 305 _ = self.request.translate
306 306 c = self.load_default_context()
307 307
308 308 pull_request = PullRequest.get_or_404(
309 309 self.request.matchdict['pull_request_id'])
310 310 pull_request_id = pull_request.pull_request_id
311 311
312 312 c.state_progressing = pull_request.is_state_changing()
313 313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
315 315
316 316 _new_state = {
317 317 'created': PullRequest.STATE_CREATED,
318 318 }.get(self.request.GET.get('force_state'))
319 319
320 320 if c.is_super_admin and _new_state:
321 321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
322 322 h.flash(
323 323 _('Pull Request state was force changed to `{}`').format(_new_state),
324 324 category='success')
325 325 Session().commit()
326 326
327 327 raise HTTPFound(h.route_path(
328 328 'pullrequest_show', repo_name=self.db_repo_name,
329 329 pull_request_id=pull_request_id))
330 330
331 331 version = self.request.GET.get('version')
332 332 from_version = self.request.GET.get('from_version') or version
333 333 merge_checks = self.request.GET.get('merge_checks')
334 334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
337 337
338 338 # fetch global flags of ignore ws or context lines
339 339 diff_context = diffs.get_diff_context(self.request)
340 340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
341 341
342 342 (pull_request_latest,
343 343 pull_request_at_ver,
344 344 pull_request_display_obj,
345 345 at_version) = PullRequestModel().get_pr_version(
346 346 pull_request_id, version=version)
347 347
348 348 pr_closed = pull_request_latest.is_closed()
349 349
350 350 if pr_closed and (version or from_version):
351 351 # not allow to browse versions for closed PR
352 352 raise HTTPFound(h.route_path(
353 353 'pullrequest_show', repo_name=self.db_repo_name,
354 354 pull_request_id=pull_request_id))
355 355
356 356 versions = pull_request_display_obj.versions()
357 357 # used to store per-commit range diffs
358 358 c.changes = collections.OrderedDict()
359 359
360 360 c.at_version = at_version
361 361 c.at_version_num = (at_version
362 362 if at_version and at_version != PullRequest.LATEST_VER
363 363 else None)
364 364
365 365 c.at_version_index = ChangesetComment.get_index_from_version(
366 366 c.at_version_num, versions)
367 367
368 368 (prev_pull_request_latest,
369 369 prev_pull_request_at_ver,
370 370 prev_pull_request_display_obj,
371 371 prev_at_version) = PullRequestModel().get_pr_version(
372 372 pull_request_id, version=from_version)
373 373
374 374 c.from_version = prev_at_version
375 375 c.from_version_num = (prev_at_version
376 376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
377 377 else None)
378 378 c.from_version_index = ChangesetComment.get_index_from_version(
379 379 c.from_version_num, versions)
380 380
381 381 # define if we're in COMPARE mode or VIEW at version mode
382 382 compare = at_version != prev_at_version
383 383
384 384 # pull_requests repo_name we opened it against
385 385 # ie. target_repo must match
386 386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
387 387 log.warning('Mismatch between the current repo: %s, and target %s',
388 388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
389 389 raise HTTPNotFound()
390 390
391 391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
392 392
393 393 c.pull_request = pull_request_display_obj
394 394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
395 395 c.pull_request_latest = pull_request_latest
396 396
397 397 # inject latest version
398 398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 399 c.versions = versions + [latest_ver]
400 400
401 401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
402 402 c.allowed_to_change_status = False
403 403 c.allowed_to_update = False
404 404 c.allowed_to_merge = False
405 405 c.allowed_to_delete = False
406 406 c.allowed_to_comment = False
407 407 c.allowed_to_close = False
408 408 else:
409 409 can_change_status = PullRequestModel().check_user_change_status(
410 410 pull_request_at_ver, self._rhodecode_user)
411 411 c.allowed_to_change_status = can_change_status and not pr_closed
412 412
413 413 c.allowed_to_update = PullRequestModel().check_user_update(
414 414 pull_request_latest, self._rhodecode_user) and not pr_closed
415 415 c.allowed_to_merge = PullRequestModel().check_user_merge(
416 416 pull_request_latest, self._rhodecode_user) and not pr_closed
417 417 c.allowed_to_delete = PullRequestModel().check_user_delete(
418 418 pull_request_latest, self._rhodecode_user) and not pr_closed
419 419 c.allowed_to_comment = not pr_closed
420 420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
421 421
422 422 c.forbid_adding_reviewers = False
423 423 c.forbid_author_to_review = False
424 424 c.forbid_commit_author_to_review = False
425 425
426 426 if pull_request_latest.reviewer_data and \
427 427 'rules' in pull_request_latest.reviewer_data:
428 428 rules = pull_request_latest.reviewer_data['rules'] or {}
429 429 try:
430 430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
431 431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
432 432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
433 433 except Exception:
434 434 pass
435 435
436 436 # check merge capabilities
437 437 _merge_check = MergeCheck.validate(
438 438 pull_request_latest, auth_user=self._rhodecode_user,
439 439 translator=self.request.translate,
440 440 force_shadow_repo_refresh=force_refresh)
441 441
442 442 c.pr_merge_errors = _merge_check.error_details
443 443 c.pr_merge_possible = not _merge_check.failed
444 444 c.pr_merge_message = _merge_check.merge_msg
445 445 c.pr_merge_source_commit = _merge_check.source_commit
446 446 c.pr_merge_target_commit = _merge_check.target_commit
447 447
448 448 c.pr_merge_info = MergeCheck.get_merge_conditions(
449 449 pull_request_latest, translator=self.request.translate)
450 450
451 451 c.pull_request_review_status = _merge_check.review_status
452 452 if merge_checks:
453 453 self.request.override_renderer = \
454 454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 455 return self._get_template_context(c)
456 456
457 457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
458 458
459 459 # reviewers and statuses
460 460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
462 462
463 463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 464 member_reviewer = h.reviewer_as_json(
465 465 member, reasons=reasons, mandatory=mandatory,
466 466 user_group=review_obj.rule_user_group_data()
467 467 )
468 468
469 469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
470 470 member_reviewer['review_status'] = current_review_status
471 471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
472 472 member_reviewer['allowed_to_update'] = c.allowed_to_update
473 473 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
474 474
475 475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
476 476
477
478
479
480 477 general_comments, inline_comments = \
481 478 self.register_comments_vars(c, pull_request_latest, versions)
482 479
483 480 # TODOs
484 481 c.unresolved_comments = CommentsModel() \
485 482 .get_pull_request_unresolved_todos(pull_request_latest)
486 483 c.resolved_comments = CommentsModel() \
487 484 .get_pull_request_resolved_todos(pull_request_latest)
488 485
489 486 # if we use version, then do not show later comments
490 487 # than current version
491 488 display_inline_comments = collections.defaultdict(
492 489 lambda: collections.defaultdict(list))
493 490 for co in inline_comments:
494 491 if c.at_version_num:
495 492 # pick comments that are at least UPTO given version, so we
496 493 # don't render comments for higher version
497 494 should_render = co.pull_request_version_id and \
498 495 co.pull_request_version_id <= c.at_version_num
499 496 else:
500 497 # showing all, for 'latest'
501 498 should_render = True
502 499
503 500 if should_render:
504 501 display_inline_comments[co.f_path][co.line_no].append(co)
505 502
506 503 # load diff data into template context, if we use compare mode then
507 504 # diff is calculated based on changes between versions of PR
508 505
509 506 source_repo = pull_request_at_ver.source_repo
510 507 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
511 508
512 509 target_repo = pull_request_at_ver.target_repo
513 510 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
514 511
515 512 if compare:
516 513 # in compare switch the diff base to latest commit from prev version
517 514 target_ref_id = prev_pull_request_display_obj.revisions[0]
518 515
519 516 # despite opening commits for bookmarks/branches/tags, we always
520 517 # convert this to rev to prevent changes after bookmark or branch change
521 518 c.source_ref_type = 'rev'
522 519 c.source_ref = source_ref_id
523 520
524 521 c.target_ref_type = 'rev'
525 522 c.target_ref = target_ref_id
526 523
527 524 c.source_repo = source_repo
528 525 c.target_repo = target_repo
529 526
530 527 c.commit_ranges = []
531 528 source_commit = EmptyCommit()
532 529 target_commit = EmptyCommit()
533 530 c.missing_requirements = False
534 531
535 532 source_scm = source_repo.scm_instance()
536 533 target_scm = target_repo.scm_instance()
537 534
538 535 shadow_scm = None
539 536 try:
540 537 shadow_scm = pull_request_latest.get_shadow_repo()
541 538 except Exception:
542 539 log.debug('Failed to get shadow repo', exc_info=True)
543 540 # try first the existing source_repo, and then shadow
544 541 # repo if we can obtain one
545 542 commits_source_repo = source_scm
546 543 if shadow_scm:
547 544 commits_source_repo = shadow_scm
548 545
549 546 c.commits_source_repo = commits_source_repo
550 547 c.ancestor = None # set it to None, to hide it from PR view
551 548
552 549 # empty version means latest, so we keep this to prevent
553 550 # double caching
554 551 version_normalized = version or PullRequest.LATEST_VER
555 552 from_version_normalized = from_version or PullRequest.LATEST_VER
556 553
557 554 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
558 555 cache_file_path = diff_cache_exist(
559 556 cache_path, 'pull_request', pull_request_id, version_normalized,
560 557 from_version_normalized, source_ref_id, target_ref_id,
561 558 hide_whitespace_changes, diff_context, c.fulldiff)
562 559
563 560 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
564 561 force_recache = self.get_recache_flag()
565 562
566 563 cached_diff = None
567 564 if caching_enabled:
568 565 cached_diff = load_cached_diff(cache_file_path)
569 566
570 567 has_proper_commit_cache = (
571 568 cached_diff and cached_diff.get('commits')
572 569 and len(cached_diff.get('commits', [])) == 5
573 570 and cached_diff.get('commits')[0]
574 571 and cached_diff.get('commits')[3])
575 572
576 573 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
577 574 diff_commit_cache = \
578 575 (ancestor_commit, commit_cache, missing_requirements,
579 576 source_commit, target_commit) = cached_diff['commits']
580 577 else:
581 578 # NOTE(marcink): we reach potentially unreachable errors when a PR has
582 579 # merge errors resulting in potentially hidden commits in the shadow repo.
583 580 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
584 581 and _merge_check.merge_response
585 582 maybe_unreachable = maybe_unreachable \
586 583 and _merge_check.merge_response.metadata.get('unresolved_files')
587 584 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
588 585 diff_commit_cache = \
589 586 (ancestor_commit, commit_cache, missing_requirements,
590 587 source_commit, target_commit) = self.get_commits(
591 588 commits_source_repo,
592 589 pull_request_at_ver,
593 590 source_commit,
594 591 source_ref_id,
595 592 source_scm,
596 593 target_commit,
597 594 target_ref_id,
598 595 target_scm,
599 596 maybe_unreachable=maybe_unreachable)
600 597
601 598 # register our commit range
602 599 for comm in commit_cache.values():
603 600 c.commit_ranges.append(comm)
604 601
605 602 c.missing_requirements = missing_requirements
606 603 c.ancestor_commit = ancestor_commit
607 604 c.statuses = source_repo.statuses(
608 605 [x.raw_id for x in c.commit_ranges])
609 606
610 607 # auto collapse if we have more than limit
611 608 collapse_limit = diffs.DiffProcessor._collapse_commits_over
612 609 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
613 610 c.compare_mode = compare
614 611
615 612 # diff_limit is the old behavior, will cut off the whole diff
616 613 # if the limit is applied otherwise will just hide the
617 614 # big files from the front-end
618 615 diff_limit = c.visual.cut_off_limit_diff
619 616 file_limit = c.visual.cut_off_limit_file
620 617
621 618 c.missing_commits = False
622 619 if (c.missing_requirements
623 620 or isinstance(source_commit, EmptyCommit)
624 621 or source_commit == target_commit):
625 622
626 623 c.missing_commits = True
627 624 else:
628 625 c.inline_comments = display_inline_comments
629 626
630 627 use_ancestor = True
631 628 if from_version_normalized != version_normalized:
632 629 use_ancestor = False
633 630
634 631 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
635 632 if not force_recache and has_proper_diff_cache:
636 633 c.diffset = cached_diff['diff']
637 634 else:
638 635 try:
639 636 c.diffset = self._get_diffset(
640 637 c.source_repo.repo_name, commits_source_repo,
641 638 c.ancestor_commit,
642 639 source_ref_id, target_ref_id,
643 640 target_commit, source_commit,
644 641 diff_limit, file_limit, c.fulldiff,
645 642 hide_whitespace_changes, diff_context,
646 643 use_ancestor=use_ancestor
647 644 )
648 645
649 646 # save cached diff
650 647 if caching_enabled:
651 648 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
652 649 except CommitDoesNotExistError:
653 650 log.exception('Failed to generate diffset')
654 651 c.missing_commits = True
655 652
656 653 if not c.missing_commits:
657 654
658 655 c.limited_diff = c.diffset.limited_diff
659 656
660 657 # calculate removed files that are bound to comments
661 658 comment_deleted_files = [
662 659 fname for fname in display_inline_comments
663 660 if fname not in c.diffset.file_stats]
664 661
665 662 c.deleted_files_comments = collections.defaultdict(dict)
666 663 for fname, per_line_comments in display_inline_comments.items():
667 664 if fname in comment_deleted_files:
668 665 c.deleted_files_comments[fname]['stats'] = 0
669 666 c.deleted_files_comments[fname]['comments'] = list()
670 667 for lno, comments in per_line_comments.items():
671 668 c.deleted_files_comments[fname]['comments'].extend(comments)
672 669
673 670 # maybe calculate the range diff
674 671 if c.range_diff_on:
675 672 # TODO(marcink): set whitespace/context
676 673 context_lcl = 3
677 674 ign_whitespace_lcl = False
678 675
679 676 for commit in c.commit_ranges:
680 677 commit2 = commit
681 678 commit1 = commit.first_parent
682 679
683 680 range_diff_cache_file_path = diff_cache_exist(
684 681 cache_path, 'diff', commit.raw_id,
685 682 ign_whitespace_lcl, context_lcl, c.fulldiff)
686 683
687 684 cached_diff = None
688 685 if caching_enabled:
689 686 cached_diff = load_cached_diff(range_diff_cache_file_path)
690 687
691 688 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
692 689 if not force_recache and has_proper_diff_cache:
693 690 diffset = cached_diff['diff']
694 691 else:
695 692 diffset = self._get_range_diffset(
696 693 commits_source_repo, source_repo,
697 694 commit1, commit2, diff_limit, file_limit,
698 695 c.fulldiff, ign_whitespace_lcl, context_lcl
699 696 )
700 697
701 698 # save cached diff
702 699 if caching_enabled:
703 700 cache_diff(range_diff_cache_file_path, diffset, None)
704 701
705 702 c.changes[commit.raw_id] = diffset
706 703
707 704 # this is a hack to properly display links, when creating PR, the
708 705 # compare view and others uses different notation, and
709 706 # compare_commits.mako renders links based on the target_repo.
710 707 # We need to swap that here to generate it properly on the html side
711 708 c.target_repo = c.source_repo
712 709
713 710 c.commit_statuses = ChangesetStatus.STATUSES
714 711
715 712 c.show_version_changes = not pr_closed
716 713 if c.show_version_changes:
717 714 cur_obj = pull_request_at_ver
718 715 prev_obj = prev_pull_request_at_ver
719 716
720 717 old_commit_ids = prev_obj.revisions
721 718 new_commit_ids = cur_obj.revisions
722 719 commit_changes = PullRequestModel()._calculate_commit_id_changes(
723 720 old_commit_ids, new_commit_ids)
724 721 c.commit_changes_summary = commit_changes
725 722
726 723 # calculate the diff for commits between versions
727 724 c.commit_changes = []
728 725
729 726 def mark(cs, fw):
730 727 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
731 728
732 729 for c_type, raw_id in mark(commit_changes.added, 'a') \
733 730 + mark(commit_changes.removed, 'r') \
734 731 + mark(commit_changes.common, 'c'):
735 732
736 733 if raw_id in commit_cache:
737 734 commit = commit_cache[raw_id]
738 735 else:
739 736 try:
740 737 commit = commits_source_repo.get_commit(raw_id)
741 738 except CommitDoesNotExistError:
742 739 # in case we fail extracting still use "dummy" commit
743 740 # for display in commit diff
744 741 commit = h.AttributeDict(
745 742 {'raw_id': raw_id,
746 743 'message': 'EMPTY or MISSING COMMIT'})
747 744 c.commit_changes.append([c_type, commit])
748 745
749 746 # current user review statuses for each version
750 747 c.review_versions = {}
751 748 if self._rhodecode_user.user_id in c.allowed_reviewers:
752 749 for co in general_comments:
753 750 if co.author.user_id == self._rhodecode_user.user_id:
754 751 status = co.status_change
755 752 if status:
756 753 _ver_pr = status[0].comment.pull_request_version_id
757 754 c.review_versions[_ver_pr] = status[0]
758 755
759 756 return self._get_template_context(c)
760 757
761 758 def get_commits(
762 759 self, commits_source_repo, pull_request_at_ver, source_commit,
763 760 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
764 761 maybe_unreachable=False):
765 762
766 763 commit_cache = collections.OrderedDict()
767 764 missing_requirements = False
768 765
769 766 try:
770 767 pre_load = ["author", "date", "message", "branch", "parents"]
771 768
772 769 pull_request_commits = pull_request_at_ver.revisions
773 770 log.debug('Loading %s commits from %s',
774 771 len(pull_request_commits), commits_source_repo)
775 772
776 773 for rev in pull_request_commits:
777 774 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
778 775 maybe_unreachable=maybe_unreachable)
779 776 commit_cache[comm.raw_id] = comm
780 777
781 778 # Order here matters, we first need to get target, and then
782 779 # the source
783 780 target_commit = commits_source_repo.get_commit(
784 781 commit_id=safe_str(target_ref_id))
785 782
786 783 source_commit = commits_source_repo.get_commit(
787 784 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
788 785 except CommitDoesNotExistError:
789 786 log.warning('Failed to get commit from `{}` repo'.format(
790 787 commits_source_repo), exc_info=True)
791 788 except RepositoryRequirementError:
792 789 log.warning('Failed to get all required data from repo', exc_info=True)
793 790 missing_requirements = True
794 791
795 792 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
796 793
797 794 try:
798 795 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
799 796 except Exception:
800 797 ancestor_commit = None
801 798
802 799 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
803 800
804 801 def assure_not_empty_repo(self):
805 802 _ = self.request.translate
806 803
807 804 try:
808 805 self.db_repo.scm_instance().get_commit()
809 806 except EmptyRepositoryError:
810 807 h.flash(h.literal(_('There are no commits yet')),
811 808 category='warning')
812 809 raise HTTPFound(
813 810 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
814 811
815 812 @LoginRequired()
816 813 @NotAnonymous()
817 814 @HasRepoPermissionAnyDecorator(
818 815 'repository.read', 'repository.write', 'repository.admin')
819 816 @view_config(
820 817 route_name='pullrequest_new', request_method='GET',
821 818 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
822 819 def pull_request_new(self):
823 820 _ = self.request.translate
824 821 c = self.load_default_context()
825 822
826 823 self.assure_not_empty_repo()
827 824 source_repo = self.db_repo
828 825
829 826 commit_id = self.request.GET.get('commit')
830 827 branch_ref = self.request.GET.get('branch')
831 828 bookmark_ref = self.request.GET.get('bookmark')
832 829
833 830 try:
834 831 source_repo_data = PullRequestModel().generate_repo_data(
835 832 source_repo, commit_id=commit_id,
836 833 branch=branch_ref, bookmark=bookmark_ref,
837 834 translator=self.request.translate)
838 835 except CommitDoesNotExistError as e:
839 836 log.exception(e)
840 837 h.flash(_('Commit does not exist'), 'error')
841 838 raise HTTPFound(
842 839 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
843 840
844 841 default_target_repo = source_repo
845 842
846 843 if source_repo.parent and c.has_origin_repo_read_perm:
847 844 parent_vcs_obj = source_repo.parent.scm_instance()
848 845 if parent_vcs_obj and not parent_vcs_obj.is_empty():
849 846 # change default if we have a parent repo
850 847 default_target_repo = source_repo.parent
851 848
852 849 target_repo_data = PullRequestModel().generate_repo_data(
853 850 default_target_repo, translator=self.request.translate)
854 851
855 852 selected_source_ref = source_repo_data['refs']['selected_ref']
856 853 title_source_ref = ''
857 854 if selected_source_ref:
858 855 title_source_ref = selected_source_ref.split(':', 2)[1]
859 856 c.default_title = PullRequestModel().generate_pullrequest_title(
860 857 source=source_repo.repo_name,
861 858 source_ref=title_source_ref,
862 859 target=default_target_repo.repo_name
863 860 )
864 861
865 862 c.default_repo_data = {
866 863 'source_repo_name': source_repo.repo_name,
867 864 'source_refs_json': json.dumps(source_repo_data),
868 865 'target_repo_name': default_target_repo.repo_name,
869 866 'target_refs_json': json.dumps(target_repo_data),
870 867 }
871 868 c.default_source_ref = selected_source_ref
872 869
873 870 return self._get_template_context(c)
874 871
875 872 @LoginRequired()
876 873 @NotAnonymous()
877 874 @HasRepoPermissionAnyDecorator(
878 875 'repository.read', 'repository.write', 'repository.admin')
879 876 @view_config(
880 877 route_name='pullrequest_repo_refs', request_method='GET',
881 878 renderer='json_ext', xhr=True)
882 879 def pull_request_repo_refs(self):
883 880 self.load_default_context()
884 881 target_repo_name = self.request.matchdict['target_repo_name']
885 882 repo = Repository.get_by_repo_name(target_repo_name)
886 883 if not repo:
887 884 raise HTTPNotFound()
888 885
889 886 target_perm = HasRepoPermissionAny(
890 887 'repository.read', 'repository.write', 'repository.admin')(
891 888 target_repo_name)
892 889 if not target_perm:
893 890 raise HTTPNotFound()
894 891
895 892 return PullRequestModel().generate_repo_data(
896 893 repo, translator=self.request.translate)
897 894
898 895 @LoginRequired()
899 896 @NotAnonymous()
900 897 @HasRepoPermissionAnyDecorator(
901 898 'repository.read', 'repository.write', 'repository.admin')
902 899 @view_config(
903 900 route_name='pullrequest_repo_targets', request_method='GET',
904 901 renderer='json_ext', xhr=True)
905 902 def pullrequest_repo_targets(self):
906 903 _ = self.request.translate
907 904 filter_query = self.request.GET.get('query')
908 905
909 906 # get the parents
910 907 parent_target_repos = []
911 908 if self.db_repo.parent:
912 909 parents_query = Repository.query() \
913 910 .order_by(func.length(Repository.repo_name)) \
914 911 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
915 912
916 913 if filter_query:
917 914 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
918 915 parents_query = parents_query.filter(
919 916 Repository.repo_name.ilike(ilike_expression))
920 917 parents = parents_query.limit(20).all()
921 918
922 919 for parent in parents:
923 920 parent_vcs_obj = parent.scm_instance()
924 921 if parent_vcs_obj and not parent_vcs_obj.is_empty():
925 922 parent_target_repos.append(parent)
926 923
927 924 # get other forks, and repo itself
928 925 query = Repository.query() \
929 926 .order_by(func.length(Repository.repo_name)) \
930 927 .filter(
931 928 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
932 929 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
933 930 ) \
934 931 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
935 932
936 933 if filter_query:
937 934 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
938 935 query = query.filter(Repository.repo_name.ilike(ilike_expression))
939 936
940 937 limit = max(20 - len(parent_target_repos), 5) # not less then 5
941 938 target_repos = query.limit(limit).all()
942 939
943 940 all_target_repos = target_repos + parent_target_repos
944 941
945 942 repos = []
946 943 # This checks permissions to the repositories
947 944 for obj in ScmModel().get_repos(all_target_repos):
948 945 repos.append({
949 946 'id': obj['name'],
950 947 'text': obj['name'],
951 948 'type': 'repo',
952 949 'repo_id': obj['dbrepo']['repo_id'],
953 950 'repo_type': obj['dbrepo']['repo_type'],
954 951 'private': obj['dbrepo']['private'],
955 952
956 953 })
957 954
958 955 data = {
959 956 'more': False,
960 957 'results': [{
961 958 'text': _('Repositories'),
962 959 'children': repos
963 960 }] if repos else []
964 961 }
965 962 return data
966 963
967 964 @LoginRequired()
968 965 @NotAnonymous()
969 966 @HasRepoPermissionAnyDecorator(
970 967 'repository.read', 'repository.write', 'repository.admin')
971 968 @view_config(
972 969 route_name='pullrequest_comments', request_method='POST',
973 970 renderer='string', xhr=True)
974 971 def pullrequest_comments(self):
975 972 self.load_default_context()
976 973
977 974 pull_request = PullRequest.get_or_404(
978 975 self.request.matchdict['pull_request_id'])
979 976 pull_request_id = pull_request.pull_request_id
980 977 version = self.request.GET.get('version')
981 978
982 979 _render = self.request.get_partial_renderer(
983 'rhodecode:templates/pullrequests/pullrequest_show.mako')
980 'rhodecode:templates/base/sidebar.mako')
984 981 c = _render.get_call_context()
985 982
986 983 (pull_request_latest,
987 984 pull_request_at_ver,
988 985 pull_request_display_obj,
989 986 at_version) = PullRequestModel().get_pr_version(
990 987 pull_request_id, version=version)
991 988 versions = pull_request_display_obj.versions()
992 989 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
993 990 c.versions = versions + [latest_ver]
994 991
995 992 c.at_version = at_version
996 993 c.at_version_num = (at_version
997 994 if at_version and at_version != PullRequest.LATEST_VER
998 995 else None)
999 996
1000 997 self.register_comments_vars(c, pull_request_latest, versions)
1001 998 all_comments = c.inline_comments_flat + c.comments
1002 return _render('comments_table', all_comments, len(all_comments))
999
1000 existing_ids = filter(
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1002 return _render('comments_table', all_comments, len(all_comments),
1003 existing_ids=existing_ids)
1003 1004
1004 1005 @LoginRequired()
1005 1006 @NotAnonymous()
1006 1007 @HasRepoPermissionAnyDecorator(
1007 1008 'repository.read', 'repository.write', 'repository.admin')
1008 1009 @view_config(
1009 1010 route_name='pullrequest_todos', request_method='POST',
1010 1011 renderer='string', xhr=True)
1011 1012 def pullrequest_todos(self):
1012 1013 self.load_default_context()
1013 1014
1014 1015 pull_request = PullRequest.get_or_404(
1015 1016 self.request.matchdict['pull_request_id'])
1016 1017 pull_request_id = pull_request.pull_request_id
1017 1018 version = self.request.GET.get('version')
1018 1019
1019 1020 _render = self.request.get_partial_renderer(
1020 'rhodecode:templates/pullrequests/pullrequest_show.mako')
1021 'rhodecode:templates/base/sidebar.mako')
1021 1022 c = _render.get_call_context()
1022 1023 (pull_request_latest,
1023 1024 pull_request_at_ver,
1024 1025 pull_request_display_obj,
1025 1026 at_version) = PullRequestModel().get_pr_version(
1026 1027 pull_request_id, version=version)
1027 1028 versions = pull_request_display_obj.versions()
1028 1029 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1029 1030 c.versions = versions + [latest_ver]
1030 1031
1031 1032 c.at_version = at_version
1032 1033 c.at_version_num = (at_version
1033 1034 if at_version and at_version != PullRequest.LATEST_VER
1034 1035 else None)
1035 1036
1036 1037 c.unresolved_comments = CommentsModel() \
1037 1038 .get_pull_request_unresolved_todos(pull_request)
1038 1039 c.resolved_comments = CommentsModel() \
1039 1040 .get_pull_request_resolved_todos(pull_request)
1040 1041
1041 1042 all_comments = c.unresolved_comments + c.resolved_comments
1042 return _render('comments_table', all_comments, len(c.unresolved_comments), todo_comments=True)
1043 existing_ids = filter(
1044 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1045 return _render('comments_table', all_comments, len(c.unresolved_comments),
1046 todo_comments=True, existing_ids=existing_ids)
1043 1047
1044 1048 @LoginRequired()
1045 1049 @NotAnonymous()
1046 1050 @HasRepoPermissionAnyDecorator(
1047 1051 'repository.read', 'repository.write', 'repository.admin')
1048 1052 @CSRFRequired()
1049 1053 @view_config(
1050 1054 route_name='pullrequest_create', request_method='POST',
1051 1055 renderer=None)
1052 1056 def pull_request_create(self):
1053 1057 _ = self.request.translate
1054 1058 self.assure_not_empty_repo()
1055 1059 self.load_default_context()
1056 1060
1057 1061 controls = peppercorn.parse(self.request.POST.items())
1058 1062
1059 1063 try:
1060 1064 form = PullRequestForm(
1061 1065 self.request.translate, self.db_repo.repo_id)()
1062 1066 _form = form.to_python(controls)
1063 1067 except formencode.Invalid as errors:
1064 1068 if errors.error_dict.get('revisions'):
1065 1069 msg = 'Revisions: %s' % errors.error_dict['revisions']
1066 1070 elif errors.error_dict.get('pullrequest_title'):
1067 1071 msg = errors.error_dict.get('pullrequest_title')
1068 1072 else:
1069 1073 msg = _('Error creating pull request: {}').format(errors)
1070 1074 log.exception(msg)
1071 1075 h.flash(msg, 'error')
1072 1076
1073 1077 # would rather just go back to form ...
1074 1078 raise HTTPFound(
1075 1079 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1076 1080
1077 1081 source_repo = _form['source_repo']
1078 1082 source_ref = _form['source_ref']
1079 1083 target_repo = _form['target_repo']
1080 1084 target_ref = _form['target_ref']
1081 1085 commit_ids = _form['revisions'][::-1]
1082 1086 common_ancestor_id = _form['common_ancestor']
1083 1087
1084 1088 # find the ancestor for this pr
1085 1089 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1086 1090 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1087 1091
1088 1092 if not (source_db_repo or target_db_repo):
1089 1093 h.flash(_('source_repo or target repo not found'), category='error')
1090 1094 raise HTTPFound(
1091 1095 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1092 1096
1093 1097 # re-check permissions again here
1094 1098 # source_repo we must have read permissions
1095 1099
1096 1100 source_perm = HasRepoPermissionAny(
1097 1101 'repository.read', 'repository.write', 'repository.admin')(
1098 1102 source_db_repo.repo_name)
1099 1103 if not source_perm:
1100 1104 msg = _('Not Enough permissions to source repo `{}`.'.format(
1101 1105 source_db_repo.repo_name))
1102 1106 h.flash(msg, category='error')
1103 1107 # copy the args back to redirect
1104 1108 org_query = self.request.GET.mixed()
1105 1109 raise HTTPFound(
1106 1110 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1107 1111 _query=org_query))
1108 1112
1109 1113 # target repo we must have read permissions, and also later on
1110 1114 # we want to check branch permissions here
1111 1115 target_perm = HasRepoPermissionAny(
1112 1116 'repository.read', 'repository.write', 'repository.admin')(
1113 1117 target_db_repo.repo_name)
1114 1118 if not target_perm:
1115 1119 msg = _('Not Enough permissions to target repo `{}`.'.format(
1116 1120 target_db_repo.repo_name))
1117 1121 h.flash(msg, category='error')
1118 1122 # copy the args back to redirect
1119 1123 org_query = self.request.GET.mixed()
1120 1124 raise HTTPFound(
1121 1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1122 1126 _query=org_query))
1123 1127
1124 1128 source_scm = source_db_repo.scm_instance()
1125 1129 target_scm = target_db_repo.scm_instance()
1126 1130
1127 1131 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1128 1132 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1129 1133
1130 1134 ancestor = source_scm.get_common_ancestor(
1131 1135 source_commit.raw_id, target_commit.raw_id, target_scm)
1132 1136
1133 1137 # recalculate target ref based on ancestor
1134 1138 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1135 1139 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1136 1140
1137 1141 get_default_reviewers_data, validate_default_reviewers = \
1138 1142 PullRequestModel().get_reviewer_functions()
1139 1143
1140 1144 # recalculate reviewers logic, to make sure we can validate this
1141 1145 reviewer_rules = get_default_reviewers_data(
1142 1146 self._rhodecode_db_user, source_db_repo,
1143 1147 source_commit, target_db_repo, target_commit)
1144 1148
1145 1149 given_reviewers = _form['review_members']
1146 1150 reviewers = validate_default_reviewers(
1147 1151 given_reviewers, reviewer_rules)
1148 1152
1149 1153 pullrequest_title = _form['pullrequest_title']
1150 1154 title_source_ref = source_ref.split(':', 2)[1]
1151 1155 if not pullrequest_title:
1152 1156 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1153 1157 source=source_repo,
1154 1158 source_ref=title_source_ref,
1155 1159 target=target_repo
1156 1160 )
1157 1161
1158 1162 description = _form['pullrequest_desc']
1159 1163 description_renderer = _form['description_renderer']
1160 1164
1161 1165 try:
1162 1166 pull_request = PullRequestModel().create(
1163 1167 created_by=self._rhodecode_user.user_id,
1164 1168 source_repo=source_repo,
1165 1169 source_ref=source_ref,
1166 1170 target_repo=target_repo,
1167 1171 target_ref=target_ref,
1168 1172 revisions=commit_ids,
1169 1173 common_ancestor_id=common_ancestor_id,
1170 1174 reviewers=reviewers,
1171 1175 title=pullrequest_title,
1172 1176 description=description,
1173 1177 description_renderer=description_renderer,
1174 1178 reviewer_data=reviewer_rules,
1175 1179 auth_user=self._rhodecode_user
1176 1180 )
1177 1181 Session().commit()
1178 1182
1179 1183 h.flash(_('Successfully opened new pull request'),
1180 1184 category='success')
1181 1185 except Exception:
1182 1186 msg = _('Error occurred during creation of this pull request.')
1183 1187 log.exception(msg)
1184 1188 h.flash(msg, category='error')
1185 1189
1186 1190 # copy the args back to redirect
1187 1191 org_query = self.request.GET.mixed()
1188 1192 raise HTTPFound(
1189 1193 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1190 1194 _query=org_query))
1191 1195
1192 1196 raise HTTPFound(
1193 1197 h.route_path('pullrequest_show', repo_name=target_repo,
1194 1198 pull_request_id=pull_request.pull_request_id))
1195 1199
1196 1200 @LoginRequired()
1197 1201 @NotAnonymous()
1198 1202 @HasRepoPermissionAnyDecorator(
1199 1203 'repository.read', 'repository.write', 'repository.admin')
1200 1204 @CSRFRequired()
1201 1205 @view_config(
1202 1206 route_name='pullrequest_update', request_method='POST',
1203 1207 renderer='json_ext')
1204 1208 def pull_request_update(self):
1205 1209 pull_request = PullRequest.get_or_404(
1206 1210 self.request.matchdict['pull_request_id'])
1207 1211 _ = self.request.translate
1208 1212
1209 1213 c = self.load_default_context()
1210 1214 redirect_url = None
1211 1215
1212 1216 if pull_request.is_closed():
1213 1217 log.debug('update: forbidden because pull request is closed')
1214 1218 msg = _(u'Cannot update closed pull requests.')
1215 1219 h.flash(msg, category='error')
1216 1220 return {'response': True,
1217 1221 'redirect_url': redirect_url}
1218 1222
1219 1223 is_state_changing = pull_request.is_state_changing()
1220 1224 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1221 1225 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1222 1226
1223 1227 # only owner or admin can update it
1224 1228 allowed_to_update = PullRequestModel().check_user_update(
1225 1229 pull_request, self._rhodecode_user)
1226 1230 if allowed_to_update:
1227 1231 controls = peppercorn.parse(self.request.POST.items())
1228 1232 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1229 1233
1230 1234 if 'review_members' in controls:
1231 1235 self._update_reviewers(
1232 1236 pull_request, controls['review_members'],
1233 1237 pull_request.reviewer_data)
1234 1238 elif str2bool(self.request.POST.get('update_commits', 'false')):
1235 1239 if is_state_changing:
1236 1240 log.debug('commits update: forbidden because pull request is in state %s',
1237 1241 pull_request.pull_request_state)
1238 1242 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1239 1243 u'Current state is: `{}`').format(
1240 1244 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1241 1245 h.flash(msg, category='error')
1242 1246 return {'response': True,
1243 1247 'redirect_url': redirect_url}
1244 1248
1245 1249 self._update_commits(c, pull_request)
1246 1250 if force_refresh:
1247 1251 redirect_url = h.route_path(
1248 1252 'pullrequest_show', repo_name=self.db_repo_name,
1249 1253 pull_request_id=pull_request.pull_request_id,
1250 1254 _query={"force_refresh": 1})
1251 1255 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1252 1256 self._edit_pull_request(pull_request)
1253 1257 else:
1254 1258 raise HTTPBadRequest()
1255 1259
1256 1260 return {'response': True,
1257 1261 'redirect_url': redirect_url}
1258 1262 raise HTTPForbidden()
1259 1263
1260 1264 def _edit_pull_request(self, pull_request):
1261 1265 _ = self.request.translate
1262 1266
1263 1267 try:
1264 1268 PullRequestModel().edit(
1265 1269 pull_request,
1266 1270 self.request.POST.get('title'),
1267 1271 self.request.POST.get('description'),
1268 1272 self.request.POST.get('description_renderer'),
1269 1273 self._rhodecode_user)
1270 1274 except ValueError:
1271 1275 msg = _(u'Cannot update closed pull requests.')
1272 1276 h.flash(msg, category='error')
1273 1277 return
1274 1278 else:
1275 1279 Session().commit()
1276 1280
1277 1281 msg = _(u'Pull request title & description updated.')
1278 1282 h.flash(msg, category='success')
1279 1283 return
1280 1284
1281 1285 def _update_commits(self, c, pull_request):
1282 1286 _ = self.request.translate
1283 1287
1284 1288 with pull_request.set_state(PullRequest.STATE_UPDATING):
1285 1289 resp = PullRequestModel().update_commits(
1286 1290 pull_request, self._rhodecode_db_user)
1287 1291
1288 1292 if resp.executed:
1289 1293
1290 1294 if resp.target_changed and resp.source_changed:
1291 1295 changed = 'target and source repositories'
1292 1296 elif resp.target_changed and not resp.source_changed:
1293 1297 changed = 'target repository'
1294 1298 elif not resp.target_changed and resp.source_changed:
1295 1299 changed = 'source repository'
1296 1300 else:
1297 1301 changed = 'nothing'
1298 1302
1299 1303 msg = _(u'Pull request updated to "{source_commit_id}" with '
1300 1304 u'{count_added} added, {count_removed} removed commits. '
1301 1305 u'Source of changes: {change_source}')
1302 1306 msg = msg.format(
1303 1307 source_commit_id=pull_request.source_ref_parts.commit_id,
1304 1308 count_added=len(resp.changes.added),
1305 1309 count_removed=len(resp.changes.removed),
1306 1310 change_source=changed)
1307 1311 h.flash(msg, category='success')
1308 1312
1309 1313 message = msg + (
1310 1314 ' - <a onclick="window.location.reload()">'
1311 1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1312 1316
1313 1317 message_obj = {
1314 1318 'message': message,
1315 1319 'level': 'success',
1316 1320 'topic': '/notifications'
1317 1321 }
1318 1322
1319 1323 channelstream.post_message(
1320 1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1321 1325 registry=self.request.registry)
1322 1326 else:
1323 1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1324 1328 warning_reasons = [
1325 1329 UpdateFailureReason.NO_CHANGE,
1326 1330 UpdateFailureReason.WRONG_REF_TYPE,
1327 1331 ]
1328 1332 category = 'warning' if resp.reason in warning_reasons else 'error'
1329 1333 h.flash(msg, category=category)
1330 1334
1331 1335 @LoginRequired()
1332 1336 @NotAnonymous()
1333 1337 @HasRepoPermissionAnyDecorator(
1334 1338 'repository.read', 'repository.write', 'repository.admin')
1335 1339 @CSRFRequired()
1336 1340 @view_config(
1337 1341 route_name='pullrequest_merge', request_method='POST',
1338 1342 renderer='json_ext')
1339 1343 def pull_request_merge(self):
1340 1344 """
1341 1345 Merge will perform a server-side merge of the specified
1342 1346 pull request, if the pull request is approved and mergeable.
1343 1347 After successful merging, the pull request is automatically
1344 1348 closed, with a relevant comment.
1345 1349 """
1346 1350 pull_request = PullRequest.get_or_404(
1347 1351 self.request.matchdict['pull_request_id'])
1348 1352 _ = self.request.translate
1349 1353
1350 1354 if pull_request.is_state_changing():
1351 1355 log.debug('show: forbidden because pull request is in state %s',
1352 1356 pull_request.pull_request_state)
1353 1357 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1354 1358 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1355 1359 pull_request.pull_request_state)
1356 1360 h.flash(msg, category='error')
1357 1361 raise HTTPFound(
1358 1362 h.route_path('pullrequest_show',
1359 1363 repo_name=pull_request.target_repo.repo_name,
1360 1364 pull_request_id=pull_request.pull_request_id))
1361 1365
1362 1366 self.load_default_context()
1363 1367
1364 1368 with pull_request.set_state(PullRequest.STATE_UPDATING):
1365 1369 check = MergeCheck.validate(
1366 1370 pull_request, auth_user=self._rhodecode_user,
1367 1371 translator=self.request.translate)
1368 1372 merge_possible = not check.failed
1369 1373
1370 1374 for err_type, error_msg in check.errors:
1371 1375 h.flash(error_msg, category=err_type)
1372 1376
1373 1377 if merge_possible:
1374 1378 log.debug("Pre-conditions checked, trying to merge.")
1375 1379 extras = vcs_operation_context(
1376 1380 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1377 1381 username=self._rhodecode_db_user.username, action='push',
1378 1382 scm=pull_request.target_repo.repo_type)
1379 1383 with pull_request.set_state(PullRequest.STATE_UPDATING):
1380 1384 self._merge_pull_request(
1381 1385 pull_request, self._rhodecode_db_user, extras)
1382 1386 else:
1383 1387 log.debug("Pre-conditions failed, NOT merging.")
1384 1388
1385 1389 raise HTTPFound(
1386 1390 h.route_path('pullrequest_show',
1387 1391 repo_name=pull_request.target_repo.repo_name,
1388 1392 pull_request_id=pull_request.pull_request_id))
1389 1393
1390 1394 def _merge_pull_request(self, pull_request, user, extras):
1391 1395 _ = self.request.translate
1392 1396 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1393 1397
1394 1398 if merge_resp.executed:
1395 1399 log.debug("The merge was successful, closing the pull request.")
1396 1400 PullRequestModel().close_pull_request(
1397 1401 pull_request.pull_request_id, user)
1398 1402 Session().commit()
1399 1403 msg = _('Pull request was successfully merged and closed.')
1400 1404 h.flash(msg, category='success')
1401 1405 else:
1402 1406 log.debug(
1403 1407 "The merge was not successful. Merge response: %s", merge_resp)
1404 1408 msg = merge_resp.merge_status_message
1405 1409 h.flash(msg, category='error')
1406 1410
1407 1411 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1408 1412 _ = self.request.translate
1409 1413
1410 1414 get_default_reviewers_data, validate_default_reviewers = \
1411 1415 PullRequestModel().get_reviewer_functions()
1412 1416
1413 1417 try:
1414 1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1415 1419 except ValueError as e:
1416 1420 log.error('Reviewers Validation: {}'.format(e))
1417 1421 h.flash(e, category='error')
1418 1422 return
1419 1423
1420 1424 old_calculated_status = pull_request.calculated_review_status()
1421 1425 PullRequestModel().update_reviewers(
1422 1426 pull_request, reviewers, self._rhodecode_user)
1423 1427 h.flash(_('Pull request reviewers updated.'), category='success')
1424 1428 Session().commit()
1425 1429
1426 1430 # trigger status changed if change in reviewers changes the status
1427 1431 calculated_status = pull_request.calculated_review_status()
1428 1432 if old_calculated_status != calculated_status:
1429 1433 PullRequestModel().trigger_pull_request_hook(
1430 1434 pull_request, self._rhodecode_user, 'review_status_change',
1431 1435 data={'status': calculated_status})
1432 1436
1433 1437 @LoginRequired()
1434 1438 @NotAnonymous()
1435 1439 @HasRepoPermissionAnyDecorator(
1436 1440 'repository.read', 'repository.write', 'repository.admin')
1437 1441 @CSRFRequired()
1438 1442 @view_config(
1439 1443 route_name='pullrequest_delete', request_method='POST',
1440 1444 renderer='json_ext')
1441 1445 def pull_request_delete(self):
1442 1446 _ = self.request.translate
1443 1447
1444 1448 pull_request = PullRequest.get_or_404(
1445 1449 self.request.matchdict['pull_request_id'])
1446 1450 self.load_default_context()
1447 1451
1448 1452 pr_closed = pull_request.is_closed()
1449 1453 allowed_to_delete = PullRequestModel().check_user_delete(
1450 1454 pull_request, self._rhodecode_user) and not pr_closed
1451 1455
1452 1456 # only owner can delete it !
1453 1457 if allowed_to_delete:
1454 1458 PullRequestModel().delete(pull_request, self._rhodecode_user)
1455 1459 Session().commit()
1456 1460 h.flash(_('Successfully deleted pull request'),
1457 1461 category='success')
1458 1462 raise HTTPFound(h.route_path('pullrequest_show_all',
1459 1463 repo_name=self.db_repo_name))
1460 1464
1461 1465 log.warning('user %s tried to delete pull request without access',
1462 1466 self._rhodecode_user)
1463 1467 raise HTTPNotFound()
1464 1468
1465 1469 @LoginRequired()
1466 1470 @NotAnonymous()
1467 1471 @HasRepoPermissionAnyDecorator(
1468 1472 'repository.read', 'repository.write', 'repository.admin')
1469 1473 @CSRFRequired()
1470 1474 @view_config(
1471 1475 route_name='pullrequest_comment_create', request_method='POST',
1472 1476 renderer='json_ext')
1473 1477 def pull_request_comment_create(self):
1474 1478 _ = self.request.translate
1475 1479
1476 1480 pull_request = PullRequest.get_or_404(
1477 1481 self.request.matchdict['pull_request_id'])
1478 1482 pull_request_id = pull_request.pull_request_id
1479 1483
1480 1484 if pull_request.is_closed():
1481 1485 log.debug('comment: forbidden because pull request is closed')
1482 1486 raise HTTPForbidden()
1483 1487
1484 1488 allowed_to_comment = PullRequestModel().check_user_comment(
1485 1489 pull_request, self._rhodecode_user)
1486 1490 if not allowed_to_comment:
1487 1491 log.debug(
1488 1492 'comment: forbidden because pull request is from forbidden repo')
1489 1493 raise HTTPForbidden()
1490 1494
1491 1495 c = self.load_default_context()
1492 1496
1493 1497 status = self.request.POST.get('changeset_status', None)
1494 1498 text = self.request.POST.get('text')
1495 1499 comment_type = self.request.POST.get('comment_type')
1496 1500 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1497 1501 close_pull_request = self.request.POST.get('close_pull_request')
1498 1502
1499 1503 # the logic here should work like following, if we submit close
1500 1504 # pr comment, use `close_pull_request_with_comment` function
1501 1505 # else handle regular comment logic
1502 1506
1503 1507 if close_pull_request:
1504 1508 # only owner or admin or person with write permissions
1505 1509 allowed_to_close = PullRequestModel().check_user_update(
1506 1510 pull_request, self._rhodecode_user)
1507 1511 if not allowed_to_close:
1508 1512 log.debug('comment: forbidden because not allowed to close '
1509 1513 'pull request %s', pull_request_id)
1510 1514 raise HTTPForbidden()
1511 1515
1512 1516 # This also triggers `review_status_change`
1513 1517 comment, status = PullRequestModel().close_pull_request_with_comment(
1514 1518 pull_request, self._rhodecode_user, self.db_repo, message=text,
1515 1519 auth_user=self._rhodecode_user)
1516 1520 Session().flush()
1517 1521
1518 1522 PullRequestModel().trigger_pull_request_hook(
1519 1523 pull_request, self._rhodecode_user, 'comment',
1520 1524 data={'comment': comment})
1521 1525
1522 1526 else:
1523 1527 # regular comment case, could be inline, or one with status.
1524 1528 # for that one we check also permissions
1525 1529
1526 1530 allowed_to_change_status = PullRequestModel().check_user_change_status(
1527 1531 pull_request, self._rhodecode_user)
1528 1532
1529 1533 if status and allowed_to_change_status:
1530 1534 message = (_('Status change %(transition_icon)s %(status)s')
1531 1535 % {'transition_icon': '>',
1532 1536 'status': ChangesetStatus.get_status_lbl(status)})
1533 1537 text = text or message
1534 1538
1535 1539 comment = CommentsModel().create(
1536 1540 text=text,
1537 1541 repo=self.db_repo.repo_id,
1538 1542 user=self._rhodecode_user.user_id,
1539 1543 pull_request=pull_request,
1540 1544 f_path=self.request.POST.get('f_path'),
1541 1545 line_no=self.request.POST.get('line'),
1542 1546 status_change=(ChangesetStatus.get_status_lbl(status)
1543 1547 if status and allowed_to_change_status else None),
1544 1548 status_change_type=(status
1545 1549 if status and allowed_to_change_status else None),
1546 1550 comment_type=comment_type,
1547 1551 resolves_comment_id=resolves_comment_id,
1548 1552 auth_user=self._rhodecode_user
1549 1553 )
1550 1554
1551 1555 if allowed_to_change_status:
1552 1556 # calculate old status before we change it
1553 1557 old_calculated_status = pull_request.calculated_review_status()
1554 1558
1555 1559 # get status if set !
1556 1560 if status:
1557 1561 ChangesetStatusModel().set_status(
1558 1562 self.db_repo.repo_id,
1559 1563 status,
1560 1564 self._rhodecode_user.user_id,
1561 1565 comment,
1562 1566 pull_request=pull_request
1563 1567 )
1564 1568
1565 1569 Session().flush()
1566 1570 # this is somehow required to get access to some relationship
1567 1571 # loaded on comment
1568 1572 Session().refresh(comment)
1569 1573
1570 1574 PullRequestModel().trigger_pull_request_hook(
1571 1575 pull_request, self._rhodecode_user, 'comment',
1572 1576 data={'comment': comment})
1573 1577
1574 1578 # we now calculate the status of pull request, and based on that
1575 1579 # calculation we set the commits status
1576 1580 calculated_status = pull_request.calculated_review_status()
1577 1581 if old_calculated_status != calculated_status:
1578 1582 PullRequestModel().trigger_pull_request_hook(
1579 1583 pull_request, self._rhodecode_user, 'review_status_change',
1580 1584 data={'status': calculated_status})
1581 1585
1582 1586 Session().commit()
1583 1587
1584 1588 data = {
1585 1589 'target_id': h.safeid(h.safe_unicode(
1586 1590 self.request.POST.get('f_path'))),
1587 1591 }
1588 1592 if comment:
1589 1593 c.co = comment
1590 1594 c.at_version_num = None
1591 1595 rendered_comment = render(
1592 1596 'rhodecode:templates/changeset/changeset_comment_block.mako',
1593 1597 self._get_template_context(c), self.request)
1594 1598
1595 1599 data.update(comment.get_dict())
1596 1600 data.update({'rendered_text': rendered_comment})
1597 1601
1598 1602 return data
1599 1603
1600 1604 @LoginRequired()
1601 1605 @NotAnonymous()
1602 1606 @HasRepoPermissionAnyDecorator(
1603 1607 'repository.read', 'repository.write', 'repository.admin')
1604 1608 @CSRFRequired()
1605 1609 @view_config(
1606 1610 route_name='pullrequest_comment_delete', request_method='POST',
1607 1611 renderer='json_ext')
1608 1612 def pull_request_comment_delete(self):
1609 1613 pull_request = PullRequest.get_or_404(
1610 1614 self.request.matchdict['pull_request_id'])
1611 1615
1612 1616 comment = ChangesetComment.get_or_404(
1613 1617 self.request.matchdict['comment_id'])
1614 1618 comment_id = comment.comment_id
1615 1619
1616 1620 if comment.immutable:
1617 1621 # don't allow deleting comments that are immutable
1618 1622 raise HTTPForbidden()
1619 1623
1620 1624 if pull_request.is_closed():
1621 1625 log.debug('comment: forbidden because pull request is closed')
1622 1626 raise HTTPForbidden()
1623 1627
1624 1628 if not comment:
1625 1629 log.debug('Comment with id:%s not found, skipping', comment_id)
1626 1630 # comment already deleted in another call probably
1627 1631 return True
1628 1632
1629 1633 if comment.pull_request.is_closed():
1630 1634 # don't allow deleting comments on closed pull request
1631 1635 raise HTTPForbidden()
1632 1636
1633 1637 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1634 1638 super_admin = h.HasPermissionAny('hg.admin')()
1635 1639 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1636 1640 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1637 1641 comment_repo_admin = is_repo_admin and is_repo_comment
1638 1642
1639 1643 if super_admin or comment_owner or comment_repo_admin:
1640 1644 old_calculated_status = comment.pull_request.calculated_review_status()
1641 1645 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1642 1646 Session().commit()
1643 1647 calculated_status = comment.pull_request.calculated_review_status()
1644 1648 if old_calculated_status != calculated_status:
1645 1649 PullRequestModel().trigger_pull_request_hook(
1646 1650 comment.pull_request, self._rhodecode_user, 'review_status_change',
1647 1651 data={'status': calculated_status})
1648 1652 return True
1649 1653 else:
1650 1654 log.warning('No permissions for user %s to delete comment_id: %s',
1651 1655 self._rhodecode_db_user, comment_id)
1652 1656 raise HTTPNotFound()
1653 1657
1654 1658 @LoginRequired()
1655 1659 @NotAnonymous()
1656 1660 @HasRepoPermissionAnyDecorator(
1657 1661 'repository.read', 'repository.write', 'repository.admin')
1658 1662 @CSRFRequired()
1659 1663 @view_config(
1660 1664 route_name='pullrequest_comment_edit', request_method='POST',
1661 1665 renderer='json_ext')
1662 1666 def pull_request_comment_edit(self):
1663 1667 self.load_default_context()
1664 1668
1665 1669 pull_request = PullRequest.get_or_404(
1666 1670 self.request.matchdict['pull_request_id']
1667 1671 )
1668 1672 comment = ChangesetComment.get_or_404(
1669 1673 self.request.matchdict['comment_id']
1670 1674 )
1671 1675 comment_id = comment.comment_id
1672 1676
1673 1677 if comment.immutable:
1674 1678 # don't allow deleting comments that are immutable
1675 1679 raise HTTPForbidden()
1676 1680
1677 1681 if pull_request.is_closed():
1678 1682 log.debug('comment: forbidden because pull request is closed')
1679 1683 raise HTTPForbidden()
1680 1684
1681 1685 if not comment:
1682 1686 log.debug('Comment with id:%s not found, skipping', comment_id)
1683 1687 # comment already deleted in another call probably
1684 1688 return True
1685 1689
1686 1690 if comment.pull_request.is_closed():
1687 1691 # don't allow deleting comments on closed pull request
1688 1692 raise HTTPForbidden()
1689 1693
1690 1694 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1691 1695 super_admin = h.HasPermissionAny('hg.admin')()
1692 1696 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1693 1697 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1694 1698 comment_repo_admin = is_repo_admin and is_repo_comment
1695 1699
1696 1700 if super_admin or comment_owner or comment_repo_admin:
1697 1701 text = self.request.POST.get('text')
1698 1702 version = self.request.POST.get('version')
1699 1703 if text == comment.text:
1700 1704 log.warning(
1701 1705 'Comment(PR): '
1702 1706 'Trying to create new version '
1703 1707 'with the same comment body {}'.format(
1704 1708 comment_id,
1705 1709 )
1706 1710 )
1707 1711 raise HTTPNotFound()
1708 1712
1709 1713 if version.isdigit():
1710 1714 version = int(version)
1711 1715 else:
1712 1716 log.warning(
1713 1717 'Comment(PR): Wrong version type {} {} '
1714 1718 'for comment {}'.format(
1715 1719 version,
1716 1720 type(version),
1717 1721 comment_id,
1718 1722 )
1719 1723 )
1720 1724 raise HTTPNotFound()
1721 1725
1722 1726 try:
1723 1727 comment_history = CommentsModel().edit(
1724 1728 comment_id=comment_id,
1725 1729 text=text,
1726 1730 auth_user=self._rhodecode_user,
1727 1731 version=version,
1728 1732 )
1729 1733 except CommentVersionMismatch:
1730 1734 raise HTTPConflict()
1731 1735
1732 1736 if not comment_history:
1733 1737 raise HTTPNotFound()
1734 1738
1735 1739 Session().commit()
1736 1740
1737 1741 PullRequestModel().trigger_pull_request_hook(
1738 1742 pull_request, self._rhodecode_user, 'comment_edit',
1739 1743 data={'comment': comment})
1740 1744
1741 1745 return {
1742 1746 'comment_history_id': comment_history.comment_history_id,
1743 1747 'comment_id': comment.comment_id,
1744 1748 'comment_version': comment_history.version,
1745 1749 'comment_author_username': comment_history.author.username,
1746 1750 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1747 1751 'comment_created_on': h.age_component(comment_history.created_on,
1748 1752 time_is_local=True),
1749 1753 }
1750 1754 else:
1751 1755 log.warning('No permissions for user %s to edit comment_id: %s',
1752 1756 self._rhodecode_db_user, comment_id)
1753 1757 raise HTTPNotFound()
@@ -1,394 +1,397 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import itertools
23 23 import logging
24 24 import collections
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
28 28 ChangesetStatus, ChangesetComment, PullRequest, Session)
29 29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 30 from rhodecode.lib.markup_renderer import (
31 31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class ChangesetStatusModel(BaseModel):
37 37
38 38 cls = ChangesetStatus
39 39
40 40 def __get_changeset_status(self, changeset_status):
41 41 return self._get_instance(ChangesetStatus, changeset_status)
42 42
43 43 def __get_pull_request(self, pull_request):
44 44 return self._get_instance(PullRequest, pull_request)
45 45
46 46 def _get_status_query(self, repo, revision, pull_request,
47 47 with_revisions=False):
48 48 repo = self._get_repo(repo)
49 49
50 50 q = ChangesetStatus.query()\
51 51 .filter(ChangesetStatus.repo == repo)
52 52 if not with_revisions:
53 53 q = q.filter(ChangesetStatus.version == 0)
54 54
55 55 if revision:
56 56 q = q.filter(ChangesetStatus.revision == revision)
57 57 elif pull_request:
58 58 pull_request = self.__get_pull_request(pull_request)
59 59 # TODO: johbo: Think about the impact of this join, there must
60 60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 61 # to the pull request. Might be that we want to do the same for
62 62 # the pull_request_version_id.
63 63 q = q.join(ChangesetComment).filter(
64 64 ChangesetStatus.pull_request == pull_request,
65 65 ChangesetComment.pull_request_version_id == None)
66 66 else:
67 67 raise Exception('Please specify revision or pull_request')
68 68 q = q.order_by(ChangesetStatus.version.asc())
69 69 return q
70 70
71 71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 72 trim_votes=True):
73 73 """
74 74 Calculate status based on given group members, and voting rule
75 75
76 76
77 77 group1 - 4 members, 3 required for approval
78 78 user1 - approved
79 79 user2 - reject
80 80 user3 - approved
81 81 user4 - rejected
82 82
83 83 final_state: rejected, reasons not at least 3 votes
84 84
85 85
86 86 group1 - 4 members, 2 required for approval
87 87 user1 - approved
88 88 user2 - reject
89 89 user3 - approved
90 90 user4 - rejected
91 91
92 92 final_state: approved, reasons got at least 2 approvals
93 93
94 94 group1 - 4 members, ALL required for approval
95 95 user1 - approved
96 96 user2 - reject
97 97 user3 - approved
98 98 user4 - rejected
99 99
100 100 final_state: rejected, reasons not all approvals
101 101
102 102
103 103 group1 - 4 members, ALL required for approval
104 104 user1 - approved
105 105 user2 - approved
106 106 user3 - approved
107 107 user4 - approved
108 108
109 109 final_state: approved, reason all approvals received
110 110
111 111 group1 - 4 members, 5 required for approval
112 112 (approval should be shorted to number of actual members)
113 113
114 114 user1 - approved
115 115 user2 - approved
116 116 user3 - approved
117 117 user4 - approved
118 118
119 119 final_state: approved, reason all approvals received
120 120
121 121 """
122 122 group_vote_data = {}
123 123 got_rule = False
124 124 members = collections.OrderedDict()
125 125 for review_obj, user, reasons, mandatory, statuses \
126 126 in group_statuses_by_reviewers:
127 127
128 128 if not got_rule:
129 129 group_vote_data = review_obj.rule_user_group_data()
130 130 got_rule = bool(group_vote_data)
131 131
132 132 members[user.user_id] = statuses
133 133
134 134 if not group_vote_data:
135 135 return []
136 136
137 137 required_votes = group_vote_data['vote_rule']
138 138 if required_votes == -1:
139 139 # -1 means all required, so we replace it with how many people
140 140 # are in the members
141 141 required_votes = len(members)
142 142
143 143 if trim_votes and required_votes > len(members):
144 144 # we require more votes than we have members in the group
145 145 # in this case we trim the required votes to the number of members
146 146 required_votes = len(members)
147 147
148 148 approvals = sum([
149 149 1 for statuses in members.values()
150 150 if statuses and
151 151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152 152
153 153 calculated_votes = []
154 154 # we have all votes from users, now check if we have enough votes
155 155 # to fill other
156 156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 157 if approvals >= required_votes:
158 158 fill_in = ChangesetStatus.STATUS_APPROVED
159 159
160 160 for member, statuses in members.items():
161 161 if statuses:
162 162 ver, latest = statuses[0]
163 163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 164 calculated_votes.append(fill_in)
165 165 else:
166 166 calculated_votes.append(latest.status)
167 167 else:
168 168 calculated_votes.append(fill_in)
169 169
170 170 return calculated_votes
171 171
172 172 def calculate_status(self, statuses_by_reviewers):
173 173 """
174 174 Given the approval statuses from reviewers, calculates final approval
175 175 status. There can only be 3 results, all approved, all rejected. If
176 176 there is no consensus the PR is under review.
177 177
178 178 :param statuses_by_reviewers:
179 179 """
180 180
181 181 def group_rule(element):
182 182 review_obj = element[0]
183 183 rule_data = review_obj.rule_user_group_data()
184 184 if rule_data and rule_data['id']:
185 185 return rule_data['id']
186 186
187 187 voting_groups = itertools.groupby(
188 188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189 189
190 190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191 191
192 192 reviewers_number = len(statuses_by_reviewers)
193 193 votes = collections.defaultdict(int)
194 194 for group, group_statuses_by_reviewers in voting_by_groups:
195 195 if group:
196 196 # calculate how the "group" voted
197 197 for vote_status in self.calculate_group_vote(
198 198 group, group_statuses_by_reviewers):
199 199 votes[vote_status] += 1
200 200 else:
201 201
202 202 for review_obj, user, reasons, mandatory, statuses \
203 203 in group_statuses_by_reviewers:
204 204 # individual vote
205 205 if statuses:
206 206 ver, latest = statuses[0]
207 207 votes[latest.status] += 1
208 208
209 209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211 211
212 212 # TODO(marcink): with group voting, how does rejected work,
213 213 # do we ever get rejected state ?
214 214
215 215 if approved_votes_count == reviewers_number:
216 216 return ChangesetStatus.STATUS_APPROVED
217 217
218 218 if rejected_votes_count == reviewers_number:
219 219 return ChangesetStatus.STATUS_REJECTED
220 220
221 221 return ChangesetStatus.STATUS_UNDER_REVIEW
222 222
223 223 def get_statuses(self, repo, revision=None, pull_request=None,
224 224 with_revisions=False):
225 225 q = self._get_status_query(repo, revision, pull_request,
226 226 with_revisions)
227 227 return q.all()
228 228
229 229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 230 """
231 231 Returns latest status of changeset for given revision or for given
232 232 pull request. Statuses are versioned inside a table itself and
233 233 version == 0 is always the current one
234 234
235 235 :param repo:
236 236 :param revision: 40char hash or None
237 237 :param pull_request: pull_request reference
238 238 :param as_str: return status as string not object
239 239 """
240 240 q = self._get_status_query(repo, revision, pull_request)
241 241
242 242 # need to use first here since there can be multiple statuses
243 243 # returned from pull_request
244 244 status = q.first()
245 245 if as_str:
246 246 status = status.status if status else status
247 247 st = status or ChangesetStatus.DEFAULT
248 248 return str(st)
249 249 return status
250 250
251 251 def _render_auto_status_message(
252 252 self, status, commit_id=None, pull_request=None):
253 253 """
254 254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 255 so it's always looking the same disregarding on which default
256 256 renderer system is using.
257 257
258 258 :param status: status text to change into
259 259 :param commit_id: the commit_id we change the status for
260 260 :param pull_request: the pull request we change the status for
261 261 """
262 262
263 263 new_status = ChangesetStatus.get_status_lbl(status)
264 264
265 265 params = {
266 266 'new_status_label': new_status,
267 267 'pull_request': pull_request,
268 268 'commit_id': commit_id,
269 269 }
270 270 renderer = RstTemplateRenderer()
271 271 return renderer.render('auto_status_change.mako', **params)
272 272
273 273 def set_status(self, repo, status, user, comment=None, revision=None,
274 274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 275 """
276 276 Creates new status for changeset or updates the old ones bumping their
277 277 version, leaving the current status at
278 278
279 279 :param repo:
280 280 :param revision:
281 281 :param status:
282 282 :param user:
283 283 :param comment:
284 284 :param dont_allow_on_closed_pull_request: don't allow a status change
285 285 if last status was for pull request and it's closed. We shouldn't
286 286 mess around this manually
287 287 """
288 288 repo = self._get_repo(repo)
289 289
290 290 q = ChangesetStatus.query()
291 291
292 292 if revision:
293 293 q = q.filter(ChangesetStatus.repo == repo)
294 294 q = q.filter(ChangesetStatus.revision == revision)
295 295 elif pull_request:
296 296 pull_request = self.__get_pull_request(pull_request)
297 297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 299 cur_statuses = q.all()
300 300
301 301 # if statuses exists and last is associated with a closed pull request
302 302 # we need to check if we can allow this status change
303 303 if (dont_allow_on_closed_pull_request and cur_statuses
304 304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 305 == PullRequest.STATUS_CLOSED):
306 306 raise StatusChangeOnClosedPullRequestError(
307 307 'Changing status on closed pull request is not allowed'
308 308 )
309 309
310 310 # update all current statuses with older version
311 311 if cur_statuses:
312 312 for st in cur_statuses:
313 313 st.version += 1
314 314 Session().add(st)
315 315 Session().flush()
316 316
317 317 def _create_status(user, repo, status, comment, revision, pull_request):
318 318 new_status = ChangesetStatus()
319 319 new_status.author = self._get_user(user)
320 320 new_status.repo = self._get_repo(repo)
321 321 new_status.status = status
322 322 new_status.comment = comment
323 323 new_status.revision = revision
324 324 new_status.pull_request = pull_request
325 325 return new_status
326 326
327 327 if not comment:
328 328 from rhodecode.model.comment import CommentsModel
329 329 comment = CommentsModel().create(
330 330 text=self._render_auto_status_message(
331 331 status, commit_id=revision, pull_request=pull_request),
332 332 repo=repo,
333 333 user=user,
334 334 pull_request=pull_request,
335 335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 336 )
337 337
338 338 if revision:
339 339 new_status = _create_status(
340 340 user=user, repo=repo, status=status, comment=comment,
341 341 revision=revision, pull_request=pull_request)
342 342 Session().add(new_status)
343 343 return new_status
344 344 elif pull_request:
345 345 # pull request can have more than one revision associated to it
346 346 # we need to create new version for each one
347 347 new_statuses = []
348 348 repo = pull_request.source_repo
349 349 for rev in pull_request.revisions:
350 350 new_status = _create_status(
351 351 user=user, repo=repo, status=status, comment=comment,
352 352 revision=rev, pull_request=pull_request)
353 353 new_statuses.append(new_status)
354 354 Session().add(new_status)
355 355 return new_statuses
356 356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
358
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
362
363 reviewers = []
364
365 def version(commit_status):
366 return commit_status.version
367
368 for obj in reviewers_data:
369 if not obj.user:
370 continue
371 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
373 status_groups = itertools.groupby(
374 sorted(statuses, key=version), version)
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
376
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
379 return reviewers
380
357 381 def reviewers_statuses(self, pull_request):
358 382 _commit_statuses = self.get_statuses(
359 383 pull_request.source_repo,
360 384 pull_request=pull_request,
361 385 with_revisions=True)
362 386
363 commit_statuses = collections.defaultdict(list)
364 for st in _commit_statuses:
365 commit_statuses[st.author.username] += [st]
366
367 pull_request_reviewers = []
368
369 def version(commit_status):
370 return commit_status.version
371
372 for obj in pull_request.reviewers:
373 if not obj.user:
374 continue
375 statuses = commit_statuses.get(obj.user.username, None)
376 if statuses:
377 status_groups = itertools.groupby(
378 sorted(statuses, key=version), version)
379 statuses = [(x, list(y)[0]) for x, y in status_groups]
380
381 pull_request_reviewers.append(
382 (obj, obj.user, obj.reasons, obj.mandatory, statuses))
383
384 return pull_request_reviewers
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
385 388
386 389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
387 390 """
388 391 calculate pull request status based on reviewers, it should be a list
389 392 of two element lists.
390 393
391 394 :param reviewers_statuses:
392 395 """
393 396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
394 397 return self.calculate_status(reviewers)
@@ -1,855 +1,863 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 import datetime
25 25
26 26 import logging
27 27 import traceback
28 28 import collections
29 29
30 30 from pyramid.threadlocal import get_current_registry, get_current_request
31 31 from sqlalchemy.sql.expression import null
32 32 from sqlalchemy.sql.functions import coalesce
33 33
34 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 35 from rhodecode.lib import audit_logger
36 36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (
40 40 ChangesetComment,
41 41 User,
42 42 Notification,
43 43 PullRequest,
44 44 AttributeDict,
45 45 ChangesetCommentHistory,
46 46 )
47 47 from rhodecode.model.notification import NotificationModel
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.settings import VcsSettingsModel
50 50 from rhodecode.model.notification import EmailNotificationModel
51 51 from rhodecode.model.validation_schema.schemas import comment_schema
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class CommentsModel(BaseModel):
58 58
59 59 cls = ChangesetComment
60 60
61 61 DIFF_CONTEXT_BEFORE = 3
62 62 DIFF_CONTEXT_AFTER = 3
63 63
64 64 def __get_commit_comment(self, changeset_comment):
65 65 return self._get_instance(ChangesetComment, changeset_comment)
66 66
67 67 def __get_pull_request(self, pull_request):
68 68 return self._get_instance(PullRequest, pull_request)
69 69
70 70 def _extract_mentions(self, s):
71 71 user_objects = []
72 72 for username in extract_mentioned_users(s):
73 73 user_obj = User.get_by_username(username, case_insensitive=True)
74 74 if user_obj:
75 75 user_objects.append(user_obj)
76 76 return user_objects
77 77
78 78 def _get_renderer(self, global_renderer='rst', request=None):
79 79 request = request or get_current_request()
80 80
81 81 try:
82 82 global_renderer = request.call_context.visual.default_renderer
83 83 except AttributeError:
84 84 log.debug("Renderer not set, falling back "
85 85 "to default renderer '%s'", global_renderer)
86 86 except Exception:
87 87 log.error(traceback.format_exc())
88 88 return global_renderer
89 89
90 90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 91 # group by versions, and count until, and display objects
92 92
93 93 comment_groups = collections.defaultdict(list)
94 94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 95
96 96 def yield_comments(pos):
97 97 for co in comment_groups[pos]:
98 98 yield co
99 99
100 100 comment_versions = collections.defaultdict(
101 101 lambda: collections.defaultdict(list))
102 102 prev_prvid = -1
103 103 # fake last entry with None, to aggregate on "latest" version which
104 104 # doesn't have an pull_request_version_id
105 105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 106 prvid = ver.pull_request_version_id
107 107 if prev_prvid == -1:
108 108 prev_prvid = prvid
109 109
110 110 for co in yield_comments(prvid):
111 111 comment_versions[prvid]['at'].append(co)
112 112
113 113 # save until
114 114 current = comment_versions[prvid]['at']
115 115 prev_until = comment_versions[prev_prvid]['until']
116 116 cur_until = prev_until + current
117 117 comment_versions[prvid]['until'].extend(cur_until)
118 118
119 119 # save outdated
120 120 if inline:
121 121 outdated = [x for x in cur_until
122 122 if x.outdated_at_version(show_version)]
123 123 else:
124 124 outdated = [x for x in cur_until
125 125 if x.older_than_version(show_version)]
126 126 display = [x for x in cur_until if x not in outdated]
127 127
128 128 comment_versions[prvid]['outdated'] = outdated
129 129 comment_versions[prvid]['display'] = display
130 130
131 131 prev_prvid = prvid
132 132
133 133 return comment_versions
134 134
135 135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 136 qry = Session().query(ChangesetComment) \
137 137 .filter(ChangesetComment.repo == repo)
138 138
139 139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 141
142 142 if user:
143 143 user = self._get_user(user)
144 144 if user:
145 145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 146
147 147 if commit_id:
148 148 qry = qry.filter(ChangesetComment.revision == commit_id)
149 149
150 150 qry = qry.order_by(ChangesetComment.created_on)
151 151 return qry.all()
152 152
153 153 def get_repository_unresolved_todos(self, repo):
154 154 todos = Session().query(ChangesetComment) \
155 155 .filter(ChangesetComment.repo == repo) \
156 156 .filter(ChangesetComment.resolved_by == None) \
157 157 .filter(ChangesetComment.comment_type
158 158 == ChangesetComment.COMMENT_TYPE_TODO)
159 159 todos = todos.all()
160 160
161 161 return todos
162 162
163 163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 164
165 165 todos = Session().query(ChangesetComment) \
166 166 .filter(ChangesetComment.pull_request == pull_request) \
167 167 .filter(ChangesetComment.resolved_by == None) \
168 168 .filter(ChangesetComment.comment_type
169 169 == ChangesetComment.COMMENT_TYPE_TODO)
170 170
171 171 if not show_outdated:
172 172 todos = todos.filter(
173 173 coalesce(ChangesetComment.display_state, '') !=
174 174 ChangesetComment.COMMENT_OUTDATED)
175 175
176 176 todos = todos.all()
177 177
178 178 return todos
179 179
180 180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181 181
182 182 todos = Session().query(ChangesetComment) \
183 183 .filter(ChangesetComment.pull_request == pull_request) \
184 184 .filter(ChangesetComment.resolved_by != None) \
185 185 .filter(ChangesetComment.comment_type
186 186 == ChangesetComment.COMMENT_TYPE_TODO)
187 187
188 188 if not show_outdated:
189 189 todos = todos.filter(
190 190 coalesce(ChangesetComment.display_state, '') !=
191 191 ChangesetComment.COMMENT_OUTDATED)
192 192
193 193 todos = todos.all()
194 194
195 195 return todos
196 196
197 197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198 198
199 199 todos = Session().query(ChangesetComment) \
200 200 .filter(ChangesetComment.revision == commit_id) \
201 201 .filter(ChangesetComment.resolved_by == None) \
202 202 .filter(ChangesetComment.comment_type
203 203 == ChangesetComment.COMMENT_TYPE_TODO)
204 204
205 205 if not show_outdated:
206 206 todos = todos.filter(
207 207 coalesce(ChangesetComment.display_state, '') !=
208 208 ChangesetComment.COMMENT_OUTDATED)
209 209
210 210 todos = todos.all()
211 211
212 212 return todos
213 213
214 214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215 215
216 216 todos = Session().query(ChangesetComment) \
217 217 .filter(ChangesetComment.revision == commit_id) \
218 218 .filter(ChangesetComment.resolved_by != None) \
219 219 .filter(ChangesetComment.comment_type
220 220 == ChangesetComment.COMMENT_TYPE_TODO)
221 221
222 222 if not show_outdated:
223 223 todos = todos.filter(
224 224 coalesce(ChangesetComment.display_state, '') !=
225 225 ChangesetComment.COMMENT_OUTDATED)
226 226
227 227 todos = todos.all()
228 228
229 229 return todos
230 230
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
237 return inline_comments
238
231 239 def _log_audit_action(self, action, action_data, auth_user, comment):
232 240 audit_logger.store(
233 241 action=action,
234 242 action_data=action_data,
235 243 user=auth_user,
236 244 repo=comment.repo)
237 245
238 246 def create(self, text, repo, user, commit_id=None, pull_request=None,
239 247 f_path=None, line_no=None, status_change=None,
240 248 status_change_type=None, comment_type=None,
241 249 resolves_comment_id=None, closing_pr=False, send_email=True,
242 250 renderer=None, auth_user=None, extra_recipients=None):
243 251 """
244 252 Creates new comment for commit or pull request.
245 253 IF status_change is not none this comment is associated with a
246 254 status change of commit or commit associated with pull request
247 255
248 256 :param text:
249 257 :param repo:
250 258 :param user:
251 259 :param commit_id:
252 260 :param pull_request:
253 261 :param f_path:
254 262 :param line_no:
255 263 :param status_change: Label for status change
256 264 :param comment_type: Type of comment
257 265 :param resolves_comment_id: id of comment which this one will resolve
258 266 :param status_change_type: type of status change
259 267 :param closing_pr:
260 268 :param send_email:
261 269 :param renderer: pick renderer for this comment
262 270 :param auth_user: current authenticated user calling this method
263 271 :param extra_recipients: list of extra users to be added to recipients
264 272 """
265 273
266 274 if not text:
267 275 log.warning('Missing text for comment, skipping...')
268 276 return
269 277 request = get_current_request()
270 278 _ = request.translate
271 279
272 280 if not renderer:
273 281 renderer = self._get_renderer(request=request)
274 282
275 283 repo = self._get_repo(repo)
276 284 user = self._get_user(user)
277 285 auth_user = auth_user or user
278 286
279 287 schema = comment_schema.CommentSchema()
280 288 validated_kwargs = schema.deserialize(dict(
281 289 comment_body=text,
282 290 comment_type=comment_type,
283 291 comment_file=f_path,
284 292 comment_line=line_no,
285 293 renderer_type=renderer,
286 294 status_change=status_change_type,
287 295 resolves_comment_id=resolves_comment_id,
288 296 repo=repo.repo_id,
289 297 user=user.user_id,
290 298 ))
291 299
292 300 comment = ChangesetComment()
293 301 comment.renderer = validated_kwargs['renderer_type']
294 302 comment.text = validated_kwargs['comment_body']
295 303 comment.f_path = validated_kwargs['comment_file']
296 304 comment.line_no = validated_kwargs['comment_line']
297 305 comment.comment_type = validated_kwargs['comment_type']
298 306
299 307 comment.repo = repo
300 308 comment.author = user
301 309 resolved_comment = self.__get_commit_comment(
302 310 validated_kwargs['resolves_comment_id'])
303 311 # check if the comment actually belongs to this PR
304 312 if resolved_comment and resolved_comment.pull_request and \
305 313 resolved_comment.pull_request != pull_request:
306 314 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 315 resolved_comment)
308 316 # comment not bound to this pull request, forbid
309 317 resolved_comment = None
310 318
311 319 elif resolved_comment and resolved_comment.repo and \
312 320 resolved_comment.repo != repo:
313 321 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 322 resolved_comment)
315 323 # comment not bound to this repo, forbid
316 324 resolved_comment = None
317 325
318 326 comment.resolved_comment = resolved_comment
319 327
320 328 pull_request_id = pull_request
321 329
322 330 commit_obj = None
323 331 pull_request_obj = None
324 332
325 333 if commit_id:
326 334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
327 335 # do a lookup, so we don't pass something bad here
328 336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
329 337 comment.revision = commit_obj.raw_id
330 338
331 339 elif pull_request_id:
332 340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
333 341 pull_request_obj = self.__get_pull_request(pull_request_id)
334 342 comment.pull_request = pull_request_obj
335 343 else:
336 344 raise Exception('Please specify commit or pull_request_id')
337 345
338 346 Session().add(comment)
339 347 Session().flush()
340 348 kwargs = {
341 349 'user': user,
342 350 'renderer_type': renderer,
343 351 'repo_name': repo.repo_name,
344 352 'status_change': status_change,
345 353 'status_change_type': status_change_type,
346 354 'comment_body': text,
347 355 'comment_file': f_path,
348 356 'comment_line': line_no,
349 357 'comment_type': comment_type or 'note',
350 358 'comment_id': comment.comment_id
351 359 }
352 360
353 361 if commit_obj:
354 362 recipients = ChangesetComment.get_users(
355 363 revision=commit_obj.raw_id)
356 364 # add commit author if it's in RhodeCode system
357 365 cs_author = User.get_from_cs_author(commit_obj.author)
358 366 if not cs_author:
359 367 # use repo owner if we cannot extract the author correctly
360 368 cs_author = repo.user
361 369 recipients += [cs_author]
362 370
363 371 commit_comment_url = self.get_url(comment, request=request)
364 372 commit_comment_reply_url = self.get_url(
365 373 comment, request=request,
366 374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
367 375
368 376 target_repo_url = h.link_to(
369 377 repo.repo_name,
370 378 h.route_url('repo_summary', repo_name=repo.repo_name))
371 379
372 380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
373 381 commit_id=commit_id)
374 382
375 383 # commit specifics
376 384 kwargs.update({
377 385 'commit': commit_obj,
378 386 'commit_message': commit_obj.message,
379 387 'commit_target_repo_url': target_repo_url,
380 388 'commit_comment_url': commit_comment_url,
381 389 'commit_comment_reply_url': commit_comment_reply_url,
382 390 'commit_url': commit_url,
383 391 'thread_ids': [commit_url, commit_comment_url],
384 392 })
385 393
386 394 elif pull_request_obj:
387 395 # get the current participants of this pull request
388 396 recipients = ChangesetComment.get_users(
389 397 pull_request_id=pull_request_obj.pull_request_id)
390 398 # add pull request author
391 399 recipients += [pull_request_obj.author]
392 400
393 401 # add the reviewers to notification
394 402 recipients += [x.user for x in pull_request_obj.reviewers]
395 403
396 404 pr_target_repo = pull_request_obj.target_repo
397 405 pr_source_repo = pull_request_obj.source_repo
398 406
399 407 pr_comment_url = self.get_url(comment, request=request)
400 408 pr_comment_reply_url = self.get_url(
401 409 comment, request=request,
402 410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
403 411
404 412 pr_url = h.route_url(
405 413 'pullrequest_show',
406 414 repo_name=pr_target_repo.repo_name,
407 415 pull_request_id=pull_request_obj.pull_request_id, )
408 416
409 417 # set some variables for email notification
410 418 pr_target_repo_url = h.route_url(
411 419 'repo_summary', repo_name=pr_target_repo.repo_name)
412 420
413 421 pr_source_repo_url = h.route_url(
414 422 'repo_summary', repo_name=pr_source_repo.repo_name)
415 423
416 424 # pull request specifics
417 425 kwargs.update({
418 426 'pull_request': pull_request_obj,
419 427 'pr_id': pull_request_obj.pull_request_id,
420 428 'pull_request_url': pr_url,
421 429 'pull_request_target_repo': pr_target_repo,
422 430 'pull_request_target_repo_url': pr_target_repo_url,
423 431 'pull_request_source_repo': pr_source_repo,
424 432 'pull_request_source_repo_url': pr_source_repo_url,
425 433 'pr_comment_url': pr_comment_url,
426 434 'pr_comment_reply_url': pr_comment_reply_url,
427 435 'pr_closing': closing_pr,
428 436 'thread_ids': [pr_url, pr_comment_url],
429 437 })
430 438
431 439 recipients += [self._get_user(u) for u in (extra_recipients or [])]
432 440
433 441 if send_email:
434 442 # pre-generate the subject for notification itself
435 443 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
436 444 notification_type, **kwargs)
437 445
438 446 mention_recipients = set(
439 447 self._extract_mentions(text)).difference(recipients)
440 448
441 449 # create notification objects, and emails
442 450 NotificationModel().create(
443 451 created_by=user,
444 452 notification_subject=subject,
445 453 notification_body=body_plaintext,
446 454 notification_type=notification_type,
447 455 recipients=recipients,
448 456 mention_recipients=mention_recipients,
449 457 email_kwargs=kwargs,
450 458 )
451 459
452 460 Session().flush()
453 461 if comment.pull_request:
454 462 action = 'repo.pull_request.comment.create'
455 463 else:
456 464 action = 'repo.commit.comment.create'
457 465
458 466 comment_id = comment.comment_id
459 467 comment_data = comment.get_api_data()
460 468
461 469 self._log_audit_action(
462 470 action, {'data': comment_data}, auth_user, comment)
463 471
464 472 channel = None
465 473 if commit_obj:
466 474 repo_name = repo.repo_name
467 475 channel = u'/repo${}$/commit/{}'.format(
468 476 repo_name,
469 477 commit_obj.raw_id
470 478 )
471 479 elif pull_request_obj:
472 480 repo_name = pr_target_repo.repo_name
473 481 channel = u'/repo${}$/pr/{}'.format(
474 482 repo_name,
475 483 pull_request_obj.pull_request_id
476 484 )
477 485
478 486 if channel:
479 487 username = user.username
480 488 message = '<strong>{}</strong> {} #{}, {}'
481 489 message = message.format(
482 490 username,
483 491 _('posted a new comment'),
484 492 comment_id,
485 493 _('Refresh the page to see new comments.'))
486 494
487 495 message_obj = {
488 496 'message': message,
489 497 'level': 'success',
490 498 'topic': '/notifications'
491 499 }
492 500
493 501 channelstream.post_message(
494 502 channel, message_obj, user.username,
495 503 registry=get_current_registry())
496 504
497 505 message_obj = {
498 506 'message': None,
499 507 'user': username,
500 508 'comment_id': comment_id,
501 509 'topic': '/comment'
502 510 }
503 511 channelstream.post_message(
504 512 channel, message_obj, user.username,
505 513 registry=get_current_registry())
506 514
507 515 return comment
508 516
509 517 def edit(self, comment_id, text, auth_user, version):
510 518 """
511 519 Change existing comment for commit or pull request.
512 520
513 521 :param comment_id:
514 522 :param text:
515 523 :param auth_user: current authenticated user calling this method
516 524 :param version: last comment version
517 525 """
518 526 if not text:
519 527 log.warning('Missing text for comment, skipping...')
520 528 return
521 529
522 530 comment = ChangesetComment.get(comment_id)
523 531 old_comment_text = comment.text
524 532 comment.text = text
525 533 comment.modified_at = datetime.datetime.now()
526 534 version = safe_int(version)
527 535
528 536 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
529 537 # would return 3 here
530 538 comment_version = ChangesetCommentHistory.get_version(comment_id)
531 539
532 540 if isinstance(version, (int, long)) and (comment_version - version) != 1:
533 541 log.warning(
534 542 'Version mismatch comment_version {} submitted {}, skipping'.format(
535 543 comment_version-1, # -1 since note above
536 544 version
537 545 )
538 546 )
539 547 raise CommentVersionMismatch()
540 548
541 549 comment_history = ChangesetCommentHistory()
542 550 comment_history.comment_id = comment_id
543 551 comment_history.version = comment_version
544 552 comment_history.created_by_user_id = auth_user.user_id
545 553 comment_history.text = old_comment_text
546 554 # TODO add email notification
547 555 Session().add(comment_history)
548 556 Session().add(comment)
549 557 Session().flush()
550 558
551 559 if comment.pull_request:
552 560 action = 'repo.pull_request.comment.edit'
553 561 else:
554 562 action = 'repo.commit.comment.edit'
555 563
556 564 comment_data = comment.get_api_data()
557 565 comment_data['old_comment_text'] = old_comment_text
558 566 self._log_audit_action(
559 567 action, {'data': comment_data}, auth_user, comment)
560 568
561 569 return comment_history
562 570
563 571 def delete(self, comment, auth_user):
564 572 """
565 573 Deletes given comment
566 574 """
567 575 comment = self.__get_commit_comment(comment)
568 576 old_data = comment.get_api_data()
569 577 Session().delete(comment)
570 578
571 579 if comment.pull_request:
572 580 action = 'repo.pull_request.comment.delete'
573 581 else:
574 582 action = 'repo.commit.comment.delete'
575 583
576 584 self._log_audit_action(
577 585 action, {'old_data': old_data}, auth_user, comment)
578 586
579 587 return comment
580 588
581 589 def get_all_comments(self, repo_id, revision=None, pull_request=None):
582 590 q = ChangesetComment.query()\
583 591 .filter(ChangesetComment.repo_id == repo_id)
584 592 if revision:
585 593 q = q.filter(ChangesetComment.revision == revision)
586 594 elif pull_request:
587 595 pull_request = self.__get_pull_request(pull_request)
588 596 q = q.filter(ChangesetComment.pull_request == pull_request)
589 597 else:
590 598 raise Exception('Please specify commit or pull_request')
591 599 q = q.order_by(ChangesetComment.created_on)
592 600 return q.all()
593 601
594 602 def get_url(self, comment, request=None, permalink=False, anchor=None):
595 603 if not request:
596 604 request = get_current_request()
597 605
598 606 comment = self.__get_commit_comment(comment)
599 607 if anchor is None:
600 608 anchor = 'comment-{}'.format(comment.comment_id)
601 609
602 610 if comment.pull_request:
603 611 pull_request = comment.pull_request
604 612 if permalink:
605 613 return request.route_url(
606 614 'pull_requests_global',
607 615 pull_request_id=pull_request.pull_request_id,
608 616 _anchor=anchor)
609 617 else:
610 618 return request.route_url(
611 619 'pullrequest_show',
612 620 repo_name=safe_str(pull_request.target_repo.repo_name),
613 621 pull_request_id=pull_request.pull_request_id,
614 622 _anchor=anchor)
615 623
616 624 else:
617 625 repo = comment.repo
618 626 commit_id = comment.revision
619 627
620 628 if permalink:
621 629 return request.route_url(
622 630 'repo_commit', repo_name=safe_str(repo.repo_id),
623 631 commit_id=commit_id,
624 632 _anchor=anchor)
625 633
626 634 else:
627 635 return request.route_url(
628 636 'repo_commit', repo_name=safe_str(repo.repo_name),
629 637 commit_id=commit_id,
630 638 _anchor=anchor)
631 639
632 640 def get_comments(self, repo_id, revision=None, pull_request=None):
633 641 """
634 642 Gets main comments based on revision or pull_request_id
635 643
636 644 :param repo_id:
637 645 :param revision:
638 646 :param pull_request:
639 647 """
640 648
641 649 q = ChangesetComment.query()\
642 650 .filter(ChangesetComment.repo_id == repo_id)\
643 651 .filter(ChangesetComment.line_no == None)\
644 652 .filter(ChangesetComment.f_path == None)
645 653 if revision:
646 654 q = q.filter(ChangesetComment.revision == revision)
647 655 elif pull_request:
648 656 pull_request = self.__get_pull_request(pull_request)
649 657 q = q.filter(ChangesetComment.pull_request == pull_request)
650 658 else:
651 659 raise Exception('Please specify commit or pull_request')
652 660 q = q.order_by(ChangesetComment.created_on)
653 661 return q.all()
654 662
655 663 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
656 664 q = self._get_inline_comments_query(repo_id, revision, pull_request)
657 665 return self._group_comments_by_path_and_line_number(q)
658 666
659 667 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
660 668 version=None):
661 669 inline_comms = []
662 670 for fname, per_line_comments in inline_comments.iteritems():
663 671 for lno, comments in per_line_comments.iteritems():
664 672 for comm in comments:
665 673 if not comm.outdated_at_version(version) and skip_outdated:
666 674 inline_comms.append(comm)
667 675
668 676 return inline_comms
669 677
670 678 def get_outdated_comments(self, repo_id, pull_request):
671 679 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
672 680 # of a pull request.
673 681 q = self._all_inline_comments_of_pull_request(pull_request)
674 682 q = q.filter(
675 683 ChangesetComment.display_state ==
676 684 ChangesetComment.COMMENT_OUTDATED
677 685 ).order_by(ChangesetComment.comment_id.asc())
678 686
679 687 return self._group_comments_by_path_and_line_number(q)
680 688
681 689 def _get_inline_comments_query(self, repo_id, revision, pull_request):
682 690 # TODO: johbo: Split this into two methods: One for PR and one for
683 691 # commit.
684 692 if revision:
685 693 q = Session().query(ChangesetComment).filter(
686 694 ChangesetComment.repo_id == repo_id,
687 695 ChangesetComment.line_no != null(),
688 696 ChangesetComment.f_path != null(),
689 697 ChangesetComment.revision == revision)
690 698
691 699 elif pull_request:
692 700 pull_request = self.__get_pull_request(pull_request)
693 701 if not CommentsModel.use_outdated_comments(pull_request):
694 702 q = self._visible_inline_comments_of_pull_request(pull_request)
695 703 else:
696 704 q = self._all_inline_comments_of_pull_request(pull_request)
697 705
698 706 else:
699 707 raise Exception('Please specify commit or pull_request_id')
700 708 q = q.order_by(ChangesetComment.comment_id.asc())
701 709 return q
702 710
703 711 def _group_comments_by_path_and_line_number(self, q):
704 712 comments = q.all()
705 713 paths = collections.defaultdict(lambda: collections.defaultdict(list))
706 714 for co in comments:
707 715 paths[co.f_path][co.line_no].append(co)
708 716 return paths
709 717
710 718 @classmethod
711 719 def needed_extra_diff_context(cls):
712 720 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
713 721
714 722 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
715 723 if not CommentsModel.use_outdated_comments(pull_request):
716 724 return
717 725
718 726 comments = self._visible_inline_comments_of_pull_request(pull_request)
719 727 comments_to_outdate = comments.all()
720 728
721 729 for comment in comments_to_outdate:
722 730 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
723 731
724 732 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
725 733 diff_line = _parse_comment_line_number(comment.line_no)
726 734
727 735 try:
728 736 old_context = old_diff_proc.get_context_of_line(
729 737 path=comment.f_path, diff_line=diff_line)
730 738 new_context = new_diff_proc.get_context_of_line(
731 739 path=comment.f_path, diff_line=diff_line)
732 740 except (diffs.LineNotInDiffException,
733 741 diffs.FileNotInDiffException):
734 742 comment.display_state = ChangesetComment.COMMENT_OUTDATED
735 743 return
736 744
737 745 if old_context == new_context:
738 746 return
739 747
740 748 if self._should_relocate_diff_line(diff_line):
741 749 new_diff_lines = new_diff_proc.find_context(
742 750 path=comment.f_path, context=old_context,
743 751 offset=self.DIFF_CONTEXT_BEFORE)
744 752 if not new_diff_lines:
745 753 comment.display_state = ChangesetComment.COMMENT_OUTDATED
746 754 else:
747 755 new_diff_line = self._choose_closest_diff_line(
748 756 diff_line, new_diff_lines)
749 757 comment.line_no = _diff_to_comment_line_number(new_diff_line)
750 758 else:
751 759 comment.display_state = ChangesetComment.COMMENT_OUTDATED
752 760
753 761 def _should_relocate_diff_line(self, diff_line):
754 762 """
755 763 Checks if relocation shall be tried for the given `diff_line`.
756 764
757 765 If a comment points into the first lines, then we can have a situation
758 766 that after an update another line has been added on top. In this case
759 767 we would find the context still and move the comment around. This
760 768 would be wrong.
761 769 """
762 770 should_relocate = (
763 771 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
764 772 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
765 773 return should_relocate
766 774
767 775 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
768 776 candidate = new_diff_lines[0]
769 777 best_delta = _diff_line_delta(diff_line, candidate)
770 778 for new_diff_line in new_diff_lines[1:]:
771 779 delta = _diff_line_delta(diff_line, new_diff_line)
772 780 if delta < best_delta:
773 781 candidate = new_diff_line
774 782 best_delta = delta
775 783 return candidate
776 784
777 785 def _visible_inline_comments_of_pull_request(self, pull_request):
778 786 comments = self._all_inline_comments_of_pull_request(pull_request)
779 787 comments = comments.filter(
780 788 coalesce(ChangesetComment.display_state, '') !=
781 789 ChangesetComment.COMMENT_OUTDATED)
782 790 return comments
783 791
784 792 def _all_inline_comments_of_pull_request(self, pull_request):
785 793 comments = Session().query(ChangesetComment)\
786 794 .filter(ChangesetComment.line_no != None)\
787 795 .filter(ChangesetComment.f_path != None)\
788 796 .filter(ChangesetComment.pull_request == pull_request)
789 797 return comments
790 798
791 799 def _all_general_comments_of_pull_request(self, pull_request):
792 800 comments = Session().query(ChangesetComment)\
793 801 .filter(ChangesetComment.line_no == None)\
794 802 .filter(ChangesetComment.f_path == None)\
795 803 .filter(ChangesetComment.pull_request == pull_request)
796 804
797 805 return comments
798 806
799 807 @staticmethod
800 808 def use_outdated_comments(pull_request):
801 809 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
802 810 settings = settings_model.get_general_settings()
803 811 return settings.get('rhodecode_use_outdated_comments', False)
804 812
805 813 def trigger_commit_comment_hook(self, repo, user, action, data=None):
806 814 repo = self._get_repo(repo)
807 815 target_scm = repo.scm_instance()
808 816 if action == 'create':
809 817 trigger_hook = hooks_utils.trigger_comment_commit_hooks
810 818 elif action == 'edit':
811 819 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
812 820 else:
813 821 return
814 822
815 823 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
816 824 repo, action, trigger_hook)
817 825 trigger_hook(
818 826 username=user.username,
819 827 repo_name=repo.repo_name,
820 828 repo_type=target_scm.alias,
821 829 repo=repo,
822 830 data=data)
823 831
824 832
825 833 def _parse_comment_line_number(line_no):
826 834 """
827 835 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
828 836 """
829 837 old_line = None
830 838 new_line = None
831 839 if line_no.startswith('o'):
832 840 old_line = int(line_no[1:])
833 841 elif line_no.startswith('n'):
834 842 new_line = int(line_no[1:])
835 843 else:
836 844 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
837 845 return diffs.DiffLineNumber(old_line, new_line)
838 846
839 847
840 848 def _diff_to_comment_line_number(diff_line):
841 849 if diff_line.new is not None:
842 850 return u'n{}'.format(diff_line.new)
843 851 elif diff_line.old is not None:
844 852 return u'o{}'.format(diff_line.old)
845 853 return u''
846 854
847 855
848 856 def _diff_line_delta(a, b):
849 857 if None not in (a.new, b.new):
850 858 return abs(a.new - b.new)
851 859 elif None not in (a.old, b.old):
852 860 return abs(a.old - b.old)
853 861 else:
854 862 raise ValueError(
855 863 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,57 +1,70 b''
1 1 .alert1 { .border ( @border-thickness-tags, @alert1 ); color:@alert1; }
2 2 .alert2 { .border ( @border-thickness-tags, @alert2 ); color:@alert2; }
3 3 .alert3 { .border ( @border-thickness-tags, @alert3 ); color:@alert3; }
4 4 .alert4 { .border ( @border-thickness-tags, @alert4 ); color:@alert4; }
5 5
6 6 .alert {
7 7 clear: both;
8 8 padding: @padding;
9 9 border: @border-thickness solid;
10 10 border-radius: @border-radius;
11 11
12 12 // overwritter css from specific alerts
13 13 color: @grey3;
14 14 border-color: @alert4;
15 15 background-color: @alert4-inner;
16 16
17 17 a {
18 18 text-decoration: underline;
19 19 }
20 20 .close {
21 21 color: @grey3;
22 22 }
23 23 }
24 24
25 25 .infoform .alert {
26 26 width: 100%;
27 27 margin-top: 0;
28 28 }
29 29
30 30 .alert-success {
31 31 border-color: @alert1;
32 32 background-color: @alert1-inner;
33 33 }
34 34
35 35 .alert-error {
36 36 border-color: @alert2;
37 37 background-color: @alert2-inner;
38 38 }
39 39
40 40 .alert-warning {
41 41 border-color: @alert3;
42 42 background-color: @alert3-inner;
43 43 }
44 44
45 45 .alert-dismissable {
46 46 padding-right: 10px;
47 47
48 48 .close {
49 49 margin-top: -5px;
50 50 }
51 51 }
52 52
53 53 .loginbox {
54 54 .alert {
55 55 margin: 0 auto 35px auto;
56 56 }
57 57 }
58
59 .alert-text-success {
60 color: @alert1;
61
62 }
63
64 .alert-text-error {
65 color: @alert2;
66 }
67
68 .alert-text-warning {
69 color: @alert3;
70 }
@@ -1,541 +1,541 b''
1 1
2 2
3 3 //BUTTONS
4 4 button,
5 5 .btn,
6 6 input[type="button"] {
7 7 -webkit-appearance: none;
8 8 display: inline-block;
9 9 margin: 0 @padding/3 0 0;
10 10 padding: @button-padding;
11 11 text-align: center;
12 12 font-size: @basefontsize;
13 13 line-height: 1em;
14 14 font-family: @text-light;
15 15 text-decoration: none;
16 16 text-shadow: none;
17 17 color: @grey2;
18 18 background-color: white;
19 19 background-image: none;
20 20 border: none;
21 21 .border ( @border-thickness-buttons, @grey5 );
22 22 .border-radius (@border-radius);
23 23 cursor: pointer;
24 24 white-space: nowrap;
25 25 -webkit-transition: background .3s,color .3s;
26 26 -moz-transition: background .3s,color .3s;
27 27 -o-transition: background .3s,color .3s;
28 28 transition: background .3s,color .3s;
29 29 box-shadow: @button-shadow;
30 30 -webkit-box-shadow: @button-shadow;
31 31
32 32
33 33
34 34 a {
35 35 display: block;
36 36 margin: 0;
37 37 padding: 0;
38 38 color: inherit;
39 39 text-decoration: none;
40 40
41 41 &:hover {
42 42 text-decoration: none;
43 43 }
44 44 }
45 45
46 46 &:focus,
47 47 &:active {
48 48 outline:none;
49 49 }
50 50
51 51 &:hover {
52 52 color: @rcdarkblue;
53 53 background-color: @grey6;
54 54
55 55 }
56 56
57 57 &.btn-active {
58 58 color: @rcdarkblue;
59 59 background-color: @grey6;
60 60 }
61 61
62 62 .icon-remove {
63 63 display: none;
64 64 }
65 65
66 66 //disabled buttons
67 67 //last; overrides any other styles
68 68 &:disabled {
69 69 opacity: .7;
70 70 cursor: auto;
71 71 background-color: white;
72 72 color: @grey4;
73 73 text-shadow: none;
74 74 }
75 75
76 76 &.no-margin {
77 77 margin: 0 0 0 0;
78 78 }
79 79
80 80
81 81
82 82 }
83 83
84 84
85 85 .btn-default {
86 86 border: @border-thickness solid @grey5;
87 87 background-image: none;
88 88 color: @grey2;
89 89
90 90 a {
91 91 color: @grey2;
92 92 }
93 93
94 94 &:hover,
95 95 &.active {
96 96 color: @rcdarkblue;
97 97 background-color: @white;
98 98 .border ( @border-thickness, @grey4 );
99 99
100 100 a {
101 101 color: @grey2;
102 102 }
103 103 }
104 104 &:disabled {
105 105 .border ( @border-thickness-buttons, @grey5 );
106 106 background-color: transparent;
107 107 }
108 108 &.btn-active {
109 109 color: @rcdarkblue;
110 110 background-color: @white;
111 111 .border ( @border-thickness, @rcdarkblue );
112 112 }
113 113 }
114 114
115 115 .btn-primary,
116 116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 117 .btn-success {
118 118 .border ( @border-thickness, @rcblue );
119 119 background-color: @rcblue;
120 120 color: white;
121 121
122 122 a {
123 123 color: white;
124 124 }
125 125
126 126 &:hover,
127 127 &.active {
128 128 .border ( @border-thickness, @rcdarkblue );
129 129 color: white;
130 130 background-color: @rcdarkblue;
131 131
132 132 a {
133 133 color: white;
134 134 }
135 135 }
136 136 &:disabled {
137 137 background-color: @rcblue;
138 138 }
139 139 }
140 140
141 141 .btn-secondary {
142 142 &:extend(.btn-default);
143 143
144 144 background-color: white;
145 145
146 146 &:focus {
147 147 outline: 0;
148 148 }
149 149
150 150 &:hover {
151 151 &:extend(.btn-default:hover);
152 152 }
153 153
154 154 &.btn-link {
155 155 &:extend(.btn-link);
156 156 color: @rcblue;
157 157 }
158 158
159 159 &:disabled {
160 160 color: @rcblue;
161 161 background-color: white;
162 162 }
163 163 }
164 164
165 165 .btn-warning,
166 166 .btn-danger,
167 167 .revoke_perm,
168 168 .btn-x,
169 169 .form .action_button.btn-x {
170 170 .border ( @border-thickness, @alert2 );
171 171 background-color: white;
172 172 color: @alert2;
173 173
174 174 a {
175 175 color: @alert2;
176 176 }
177 177
178 178 &:hover,
179 179 &.active {
180 180 .border ( @border-thickness, @alert2 );
181 181 color: white;
182 182 background-color: @alert2;
183 183
184 184 a {
185 185 color: white;
186 186 }
187 187 }
188 188
189 189 i {
190 190 display:none;
191 191 }
192 192
193 193 &:disabled {
194 194 background-color: white;
195 195 color: @alert2;
196 196 }
197 197 }
198 198
199 199 .btn-approved-status {
200 200 .border ( @border-thickness, @alert1 );
201 201 background-color: white;
202 202 color: @alert1;
203 203
204 204 }
205 205
206 206 .btn-rejected-status {
207 207 .border ( @border-thickness, @alert2 );
208 208 background-color: white;
209 209 color: @alert2;
210 210 }
211 211
212 212 .btn-sm,
213 213 .btn-mini,
214 214 .field-sm .btn {
215 215 padding: @padding/3;
216 216 }
217 217
218 218 .btn-xs {
219 219 padding: @padding/4;
220 220 }
221 221
222 222 .btn-lg {
223 223 padding: @padding * 1.2;
224 224 }
225 225
226 226 .btn-group {
227 227 display: inline-block;
228 228 .btn {
229 229 float: left;
230 230 margin: 0 0 0 0;
231 231 // first item
232 232 &:first-of-type:not(:last-of-type) {
233 233 border-radius: @border-radius 0 0 @border-radius;
234 234
235 235 }
236 236 // middle elements
237 237 &:not(:first-of-type):not(:last-of-type) {
238 238 border-radius: 0;
239 239 border-left-width: 0;
240 240 border-right-width: 0;
241 241 }
242 242 // last item
243 243 &:last-of-type:not(:first-of-type) {
244 244 border-radius: 0 @border-radius @border-radius 0;
245 245 }
246 246
247 247 &:only-child {
248 248 border-radius: @border-radius;
249 249 }
250 250 }
251 251
252 252 }
253 253
254 254
255 255 .btn-group-actions {
256 256 position: relative;
257 z-index: 100;
257 z-index: 50;
258 258
259 259 &:not(.open) .btn-action-switcher-container {
260 260 display: none;
261 261 }
262 262
263 263 .btn-more-option {
264 264 margin-left: -1px;
265 265 padding-left: 2px;
266 266 padding-right: 2px;
267 267 border-left: 1px solid @grey3;
268 268 }
269 269 }
270 270
271 271
272 272 .btn-action-switcher-container {
273 273 position: absolute;
274 274 top: 100%;
275 275
276 276 &.left-align {
277 277 left: 0;
278 278 }
279 279 &.right-align {
280 280 right: 0;
281 281 }
282 282
283 283 }
284 284
285 285 .btn-action-switcher {
286 286 display: block;
287 287 position: relative;
288 288 z-index: 300;
289 289 max-width: 600px;
290 290 margin-top: 4px;
291 291 margin-bottom: 24px;
292 292 font-size: 14px;
293 293 font-weight: 400;
294 294 padding: 8px 0;
295 295 background-color: #fff;
296 296 border: 1px solid @grey4;
297 297 border-radius: 3px;
298 298 box-shadow: @dropdown-shadow;
299 299 overflow: auto;
300 300
301 301 li {
302 302 display: block;
303 303 text-align: left;
304 304 list-style: none;
305 305 padding: 5px 10px;
306 306 }
307 307
308 308 li .action-help-block {
309 309 font-size: 10px;
310 310 line-height: normal;
311 311 color: @grey4;
312 312 }
313 313
314 314 }
315 315
316 316 .btn-link {
317 317 background: transparent;
318 318 border: none;
319 319 padding: 0;
320 320 color: @rcblue;
321 321
322 322 &:hover {
323 323 background: transparent;
324 324 border: none;
325 325 color: @rcdarkblue;
326 326 }
327 327
328 328 //disabled buttons
329 329 //last; overrides any other styles
330 330 &:disabled {
331 331 opacity: .7;
332 332 cursor: auto;
333 333 background-color: white;
334 334 color: @grey4;
335 335 text-shadow: none;
336 336 }
337 337
338 338 // TODO: johbo: Check if we can avoid this, indicates that the structure
339 339 // is not yet good.
340 340 // lisa: The button CSS reflects the button HTML; both need a cleanup.
341 341 &.btn-danger {
342 342 color: @alert2;
343 343
344 344 &:hover {
345 345 color: darken(@alert2,30%);
346 346 }
347 347
348 348 &:disabled {
349 349 color: @alert2;
350 350 }
351 351 }
352 352 }
353 353
354 354 .btn-social {
355 355 &:extend(.btn-default);
356 356 margin: 5px 5px 5px 0px;
357 357 min-width: 160px;
358 358 }
359 359
360 360 // TODO: johbo: check these exceptions
361 361
362 362 .links {
363 363
364 364 .btn + .btn {
365 365 margin-top: @padding;
366 366 }
367 367 }
368 368
369 369
370 370 .action_button {
371 371 display:inline;
372 372 margin: 0;
373 373 padding: 0 1em 0 0;
374 374 font-size: inherit;
375 375 color: @rcblue;
376 376 border: none;
377 377 border-radius: 0;
378 378 background-color: transparent;
379 379
380 380 &.last-item {
381 381 border: none;
382 382 padding: 0 0 0 0;
383 383 }
384 384
385 385 &:last-child {
386 386 border: none;
387 387 padding: 0 0 0 0;
388 388 }
389 389
390 390 &:hover {
391 391 color: @rcdarkblue;
392 392 background-color: transparent;
393 393 border: none;
394 394 }
395 395 .noselect
396 396 }
397 397
398 398 .grid_delete {
399 399 .action_button {
400 400 border: none;
401 401 }
402 402 }
403 403
404 404
405 405 // TODO: johbo: Form button tweaks, check if we can use the classes instead
406 406 input[type="submit"] {
407 407 &:extend(.btn-primary);
408 408
409 409 &:focus {
410 410 outline: 0;
411 411 }
412 412
413 413 &:hover {
414 414 &:extend(.btn-primary:hover);
415 415 }
416 416
417 417 &.btn-link {
418 418 &:extend(.btn-link);
419 419 color: @rcblue;
420 420
421 421 &:disabled {
422 422 color: @rcblue;
423 423 background-color: transparent;
424 424 }
425 425 }
426 426
427 427 &:disabled {
428 428 .border ( @border-thickness-buttons, @rcblue );
429 429 background-color: @rcblue;
430 430 color: white;
431 431 opacity: 0.5;
432 432 }
433 433 }
434 434
435 435 input[type="reset"] {
436 436 &:extend(.btn-default);
437 437
438 438 // TODO: johbo: Check if this tweak can be avoided.
439 439 background: transparent;
440 440
441 441 &:focus {
442 442 outline: 0;
443 443 }
444 444
445 445 &:hover {
446 446 &:extend(.btn-default:hover);
447 447 }
448 448
449 449 &.btn-link {
450 450 &:extend(.btn-link);
451 451 color: @rcblue;
452 452
453 453 &:disabled {
454 454 border: none;
455 455 }
456 456 }
457 457
458 458 &:disabled {
459 459 .border ( @border-thickness-buttons, @rcblue );
460 460 background-color: white;
461 461 color: @rcblue;
462 462 }
463 463 }
464 464
465 465 input[type="submit"],
466 466 input[type="reset"] {
467 467 &.btn-danger {
468 468 &:extend(.btn-danger);
469 469
470 470 &:focus {
471 471 outline: 0;
472 472 }
473 473
474 474 &:hover {
475 475 &:extend(.btn-danger:hover);
476 476 }
477 477
478 478 &.btn-link {
479 479 &:extend(.btn-link);
480 480 color: @alert2;
481 481
482 482 &:hover {
483 483 color: darken(@alert2,30%);
484 484 }
485 485 }
486 486
487 487 &:disabled {
488 488 color: @alert2;
489 489 background-color: white;
490 490 }
491 491 }
492 492 &.btn-danger-action {
493 493 .border ( @border-thickness, @alert2 );
494 494 background-color: @alert2;
495 495 color: white;
496 496
497 497 a {
498 498 color: white;
499 499 }
500 500
501 501 &:hover {
502 502 background-color: darken(@alert2,20%);
503 503 }
504 504
505 505 &.active {
506 506 .border ( @border-thickness, @alert2 );
507 507 color: white;
508 508 background-color: @alert2;
509 509
510 510 a {
511 511 color: white;
512 512 }
513 513 }
514 514
515 515 &:disabled {
516 516 background-color: white;
517 517 color: @alert2;
518 518 }
519 519 }
520 520 }
521 521
522 522
523 523 .button-links {
524 524 float: left;
525 525 display: inline;
526 526 margin: 0;
527 527 padding-left: 0;
528 528 list-style: none;
529 529 text-align: right;
530 530
531 531 li {
532 532
533 533
534 534 }
535 535
536 536 li.active {
537 537 background-color: @grey6;
538 538 .border ( @border-thickness, @grey4 );
539 539 }
540 540
541 541 }
@@ -1,1341 +1,1347 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
22 22 div.diffblock .sidebyside {
23 23 background: #ffffff;
24 24 }
25 25
26 26 div.diffblock {
27 27 overflow-x: auto;
28 28 overflow-y: hidden;
29 29 clear: both;
30 30 padding: 0px;
31 31 background: @grey6;
32 32 border: @border-thickness solid @grey5;
33 33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 34 border-radius: @border-radius @border-radius 0px 0px;
35 35
36 36
37 37 .comments-number {
38 38 float: right;
39 39 }
40 40
41 41 // BEGIN CODE-HEADER STYLES
42 42
43 43 .code-header {
44 44 background: @grey6;
45 45 padding: 10px 0 10px 0;
46 46 height: auto;
47 47 width: 100%;
48 48
49 49 .hash {
50 50 float: left;
51 51 padding: 2px 0 0 2px;
52 52 }
53 53
54 54 .date {
55 55 float: left;
56 56 text-transform: uppercase;
57 57 padding: 4px 0px 0px 2px;
58 58 }
59 59
60 60 div {
61 61 margin-left: 4px;
62 62 }
63 63
64 64 div.compare_header {
65 65 min-height: 40px;
66 66 margin: 0;
67 67 padding: 0 @padding;
68 68
69 69 .drop-menu {
70 70 float:left;
71 71 display: block;
72 72 margin:0 0 @padding 0;
73 73 }
74 74
75 75 .compare-label {
76 76 float: left;
77 77 clear: both;
78 78 display: inline-block;
79 79 min-width: 5em;
80 80 margin: 0;
81 81 padding: @button-padding @button-padding @button-padding 0;
82 82 font-weight: @text-semibold-weight;
83 83 font-family: @text-semibold;
84 84 }
85 85
86 86 .compare-buttons {
87 87 float: left;
88 88 margin: 0;
89 89 padding: 0 0 @padding;
90 90
91 91 .btn {
92 92 margin: 0 @padding 0 0;
93 93 }
94 94 }
95 95 }
96 96
97 97 }
98 98
99 99 .parents {
100 100 float: left;
101 101 width: 100px;
102 102 font-weight: 400;
103 103 vertical-align: middle;
104 104 padding: 0px 2px 0px 2px;
105 105 background-color: @grey6;
106 106
107 107 #parent_link {
108 108 margin: 00px 2px;
109 109
110 110 &.double {
111 111 margin: 0px 2px;
112 112 }
113 113
114 114 &.disabled{
115 115 margin-right: @padding;
116 116 }
117 117 }
118 118 }
119 119
120 120 .children {
121 121 float: right;
122 122 width: 100px;
123 123 font-weight: 400;
124 124 vertical-align: middle;
125 125 text-align: right;
126 126 padding: 0px 2px 0px 2px;
127 127 background-color: @grey6;
128 128
129 129 #child_link {
130 130 margin: 0px 2px;
131 131
132 132 &.double {
133 133 margin: 0px 2px;
134 134 }
135 135
136 136 &.disabled{
137 137 margin-right: @padding;
138 138 }
139 139 }
140 140 }
141 141
142 142 .changeset_header {
143 143 height: 16px;
144 144
145 145 & > div{
146 146 margin-right: @padding;
147 147 }
148 148 }
149 149
150 150 .changeset_file {
151 151 text-align: left;
152 152 float: left;
153 153 padding: 0;
154 154
155 155 a{
156 156 display: inline-block;
157 157 margin-right: 0.5em;
158 158 }
159 159
160 160 #selected_mode{
161 161 margin-left: 0;
162 162 }
163 163 }
164 164
165 165 .diff-menu-wrapper {
166 166 float: left;
167 167 }
168 168
169 169 .diff-menu {
170 170 position: absolute;
171 171 background: none repeat scroll 0 0 #FFFFFF;
172 172 border-color: #003367 @grey3 @grey3;
173 173 border-right: 1px solid @grey3;
174 174 border-style: solid solid solid;
175 175 border-width: @border-thickness;
176 176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
177 177 margin-top: 5px;
178 178 margin-left: 1px;
179 179 }
180 180
181 181 .diff-actions, .editor-actions {
182 182 float: left;
183 183
184 184 input{
185 185 margin: 0 0.5em 0 0;
186 186 }
187 187 }
188 188
189 189 // END CODE-HEADER STYLES
190 190
191 191 // BEGIN CODE-BODY STYLES
192 192
193 193 .code-body {
194 194 padding: 0;
195 195 background-color: #ffffff;
196 196 position: relative;
197 197 max-width: none;
198 198 box-sizing: border-box;
199 199 // TODO: johbo: Parent has overflow: auto, this forces the child here
200 200 // to have the intended size and to scroll. Should be simplified.
201 201 width: 100%;
202 202 overflow-x: auto;
203 203 }
204 204
205 205 pre.raw {
206 206 background: white;
207 207 color: @grey1;
208 208 }
209 209 // END CODE-BODY STYLES
210 210
211 211 }
212 212
213 213
214 214 table.code-difftable {
215 215 border-collapse: collapse;
216 216 width: 99%;
217 217 border-radius: 0px !important;
218 218
219 219 td {
220 220 padding: 0 !important;
221 221 background: none !important;
222 222 border: 0 !important;
223 223 }
224 224
225 225 .context {
226 226 background: none repeat scroll 0 0 #DDE7EF;
227 227 }
228 228
229 229 .add {
230 230 background: none repeat scroll 0 0 #DDFFDD;
231 231
232 232 ins {
233 233 background: none repeat scroll 0 0 #AAFFAA;
234 234 text-decoration: none;
235 235 }
236 236 }
237 237
238 238 .del {
239 239 background: none repeat scroll 0 0 #FFDDDD;
240 240
241 241 del {
242 242 background: none repeat scroll 0 0 #FFAAAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 /** LINE NUMBERS **/
248 248 .lineno {
249 249 padding-left: 2px !important;
250 250 padding-right: 2px;
251 251 text-align: right;
252 252 width: 32px;
253 253 -moz-user-select: none;
254 254 -webkit-user-select: none;
255 255 border-right: @border-thickness solid @grey5 !important;
256 256 border-left: 0px solid #CCC !important;
257 257 border-top: 0px solid #CCC !important;
258 258 border-bottom: none !important;
259 259
260 260 a {
261 261 &:extend(pre);
262 262 text-align: right;
263 263 padding-right: 2px;
264 264 cursor: pointer;
265 265 display: block;
266 266 width: 32px;
267 267 }
268 268 }
269 269
270 270 .context {
271 271 cursor: auto;
272 272 &:extend(pre);
273 273 }
274 274
275 275 .lineno-inline {
276 276 background: none repeat scroll 0 0 #FFF !important;
277 277 padding-left: 2px;
278 278 padding-right: 2px;
279 279 text-align: right;
280 280 width: 30px;
281 281 -moz-user-select: none;
282 282 -webkit-user-select: none;
283 283 }
284 284
285 285 /** CODE **/
286 286 .code {
287 287 display: block;
288 288 width: 100%;
289 289
290 290 td {
291 291 margin: 0;
292 292 padding: 0;
293 293 }
294 294
295 295 pre {
296 296 margin: 0;
297 297 padding: 0;
298 298 margin-left: .5em;
299 299 }
300 300 }
301 301 }
302 302
303 303
304 304 // Comments
305 305 .comment-selected-hl {
306 306 border-left: 6px solid @comment-highlight-color !important;
307 307 padding-left: 3px !important;
308 308 margin-left: -7px !important;
309 309 }
310 310
311 311 div.comment:target,
312 312 div.comment-outdated:target {
313 313 .comment-selected-hl;
314 314 }
315 315
316 316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
317 317 //current values that might change. But to make it clear I put as a calculation
318 318 @comment-max-width: 1065px;
319 319 @pr-extra-margin: 34px;
320 320 @pr-border-spacing: 4px;
321 321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
322 322
323 323 // Pull Request
324 324 .cs_files .code-difftable {
325 325 border: @border-thickness solid @grey5; //borders only on PRs
326 326
327 327 .comment-inline-form,
328 328 div.comment {
329 329 width: @pr-comment-width;
330 330 }
331 331 }
332 332
333 333 // Changeset
334 334 .code-difftable {
335 335 .comment-inline-form,
336 336 div.comment {
337 337 width: @comment-max-width;
338 338 }
339 339 }
340 340
341 341 //Style page
342 342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
343 343 #style-page .code-difftable{
344 344 .comment-inline-form,
345 345 div.comment {
346 346 width: @comment-max-width - @style-extra-margin;
347 347 }
348 348 }
349 349
350 350 #context-bar > h2 {
351 351 font-size: 20px;
352 352 }
353 353
354 354 #context-bar > h2> a {
355 355 font-size: 20px;
356 356 }
357 357 // end of defaults
358 358
359 359 .file_diff_buttons {
360 360 padding: 0 0 @padding;
361 361
362 362 .drop-menu {
363 363 float: left;
364 364 margin: 0 @padding 0 0;
365 365 }
366 366 .btn {
367 367 margin: 0 @padding 0 0;
368 368 }
369 369 }
370 370
371 371 .code-body.textarea.editor {
372 372 max-width: none;
373 373 padding: 15px;
374 374 }
375 375
376 376 td.injected_diff{
377 377 max-width: 1178px;
378 378 overflow-x: auto;
379 379 overflow-y: hidden;
380 380
381 381 div.diff-container,
382 382 div.diffblock{
383 383 max-width: 100%;
384 384 }
385 385
386 386 div.code-body {
387 387 max-width: 1124px;
388 388 overflow-x: auto;
389 389 overflow-y: hidden;
390 390 padding: 0;
391 391 }
392 392 div.diffblock {
393 393 border: none;
394 394 }
395 395
396 396 &.inline-form {
397 397 width: 99%
398 398 }
399 399 }
400 400
401 401
402 402 table.code-difftable {
403 403 width: 100%;
404 404 }
405 405
406 406 /** PYGMENTS COLORING **/
407 407 div.codeblock {
408 408
409 409 // TODO: johbo: Added interim to get rid of the margin around
410 410 // Select2 widgets. This needs further cleanup.
411 411 overflow: auto;
412 412 padding: 0px;
413 413 border: @border-thickness solid @grey6;
414 414 .border-radius(@border-radius);
415 415
416 416 #remove_gist {
417 417 float: right;
418 418 }
419 419
420 420 .gist_url {
421 421 padding: 0px 0px 35px 0px;
422 422 }
423 423
424 424 .gist-desc {
425 425 clear: both;
426 426 margin: 0 0 10px 0;
427 427 code {
428 428 white-space: pre-line;
429 429 line-height: inherit
430 430 }
431 431 }
432 432
433 433 .author {
434 434 clear: both;
435 435 vertical-align: middle;
436 436 font-weight: @text-bold-weight;
437 437 font-family: @text-bold;
438 438 }
439 439
440 440 .btn-mini {
441 441 float: left;
442 442 margin: 0 5px 0 0;
443 443 }
444 444
445 445 .code-header {
446 446 padding: @padding;
447 447 border-bottom: @border-thickness solid @grey5;
448 448
449 449 .rc-user {
450 450 min-width: 0;
451 451 margin-right: .5em;
452 452 }
453 453
454 454 .stats {
455 455 clear: both;
456 456 margin: 0 0 @padding 0;
457 457 padding: 0;
458 458 .left {
459 459 float: left;
460 460 clear: left;
461 461 max-width: 75%;
462 462 margin: 0 0 @padding 0;
463 463
464 464 &.item {
465 465 margin-right: @padding;
466 466 &.last { border-right: none; }
467 467 }
468 468 }
469 469 .buttons { float: right; }
470 470 .author {
471 471 height: 25px; margin-left: 15px; font-weight: bold;
472 472 }
473 473 }
474 474
475 475 .commit {
476 476 margin: 5px 0 0 26px;
477 477 font-weight: normal;
478 478 white-space: pre-wrap;
479 479 }
480 480 }
481 481
482 482 .message {
483 483 position: relative;
484 484 margin: @padding;
485 485
486 486 .codeblock-label {
487 487 margin: 0 0 1em 0;
488 488 }
489 489 }
490 490
491 491 .code-body {
492 492 padding: 0.8em 1em;
493 493 background-color: #ffffff;
494 494 min-width: 100%;
495 495 box-sizing: border-box;
496 496 // TODO: johbo: Parent has overflow: auto, this forces the child here
497 497 // to have the intended size and to scroll. Should be simplified.
498 498 width: 100%;
499 499 overflow-x: auto;
500 500
501 501 img.rendered-binary {
502 502 height: auto;
503 503 width: auto;
504 504 }
505 505
506 506 .markdown-block {
507 507 padding: 1em 0;
508 508 }
509 509 }
510 510
511 511 .codeblock-header {
512 512 background: @grey7;
513 513 height: 36px;
514 514 }
515 515
516 516 .path {
517 517 border-bottom: 1px solid @grey6;
518 518 padding: .65em 1em;
519 519 height: 18px;
520 520 }
521 521 }
522 522
523 523 .code-highlighttable,
524 524 div.codeblock {
525 525
526 526 &.readme {
527 527 background-color: white;
528 528 }
529 529
530 530 .markdown-block table {
531 531 border-collapse: collapse;
532 532
533 533 th,
534 534 td {
535 535 padding: .5em;
536 536 border: @border-thickness solid @border-default-color;
537 537 }
538 538 }
539 539
540 540 table {
541 541 border: 0px;
542 542 margin: 0;
543 543 letter-spacing: normal;
544 544
545 545
546 546 td {
547 547 border: 0px;
548 548 vertical-align: top;
549 549 }
550 550 }
551 551 }
552 552
553 553 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
554 554 div.search-code-body {
555 555 background-color: #ffffff; padding: 5px 0 5px 10px;
556 556 pre {
557 557 .match { background-color: #faffa6;}
558 558 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
559 559 }
560 560 .code-highlighttable {
561 561 border-collapse: collapse;
562 562
563 563 tr:hover {
564 564 background: #fafafa;
565 565 }
566 566 td.code {
567 567 padding-left: 10px;
568 568 }
569 569 td.line {
570 570 border-right: 1px solid #ccc !important;
571 571 padding-right: 10px;
572 572 text-align: right;
573 573 font-family: @text-monospace;
574 574 span {
575 575 white-space: pre-wrap;
576 576 color: #666666;
577 577 }
578 578 }
579 579 }
580 580 }
581 581
582 582 div.annotatediv { margin-left: 2px; margin-right: 4px; }
583 583 .code-highlight {
584 584 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
585 585 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
586 586 pre div:target {background-color: @comment-highlight-color !important;}
587 587 }
588 588
589 589 .linenos a { text-decoration: none; }
590 590
591 591 .CodeMirror-selected { background: @rchighlightblue; }
592 592 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
593 593 .CodeMirror ::selection { background: @rchighlightblue; }
594 594 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
595 595
596 596 .code { display: block; border:0px !important; }
597 597
598 598 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
599 599 .codehilite {
600 600 /*ElasticMatch is custom RhodeCode TAG*/
601 601
602 602 .c-ElasticMatch {
603 603 background-color: #faffa6;
604 604 padding: 0.2em;
605 605 }
606 606 }
607 607
608 608 /* This can be generated with `pygmentize -S default -f html` */
609 609 .code-highlight,
610 610 .codehilite {
611 611 /*ElasticMatch is custom RhodeCode TAG*/
612 612 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
613 613 .hll { background-color: #ffffcc }
614 614 .c { color: #408080; font-style: italic } /* Comment */
615 615 .err, .codehilite .err { border: none } /* Error */
616 616 .k { color: #008000; font-weight: bold } /* Keyword */
617 617 .o { color: #666666 } /* Operator */
618 618 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
619 619 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
620 620 .cp { color: #BC7A00 } /* Comment.Preproc */
621 621 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
622 622 .c1 { color: #408080; font-style: italic } /* Comment.Single */
623 623 .cs { color: #408080; font-style: italic } /* Comment.Special */
624 624 .gd { color: #A00000 } /* Generic.Deleted */
625 625 .ge { font-style: italic } /* Generic.Emph */
626 626 .gr { color: #FF0000 } /* Generic.Error */
627 627 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
628 628 .gi { color: #00A000 } /* Generic.Inserted */
629 629 .go { color: #888888 } /* Generic.Output */
630 630 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
631 631 .gs { font-weight: bold } /* Generic.Strong */
632 632 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
633 633 .gt { color: #0044DD } /* Generic.Traceback */
634 634 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
635 635 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
636 636 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
637 637 .kp { color: #008000 } /* Keyword.Pseudo */
638 638 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
639 639 .kt { color: #B00040 } /* Keyword.Type */
640 640 .m { color: #666666 } /* Literal.Number */
641 641 .s { color: #BA2121 } /* Literal.String */
642 642 .na { color: #7D9029 } /* Name.Attribute */
643 643 .nb { color: #008000 } /* Name.Builtin */
644 644 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
645 645 .no { color: #880000 } /* Name.Constant */
646 646 .nd { color: #AA22FF } /* Name.Decorator */
647 647 .ni { color: #999999; font-weight: bold } /* Name.Entity */
648 648 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
649 649 .nf { color: #0000FF } /* Name.Function */
650 650 .nl { color: #A0A000 } /* Name.Label */
651 651 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
652 652 .nt { color: #008000; font-weight: bold } /* Name.Tag */
653 653 .nv { color: #19177C } /* Name.Variable */
654 654 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
655 655 .w { color: #bbbbbb } /* Text.Whitespace */
656 656 .mb { color: #666666 } /* Literal.Number.Bin */
657 657 .mf { color: #666666 } /* Literal.Number.Float */
658 658 .mh { color: #666666 } /* Literal.Number.Hex */
659 659 .mi { color: #666666 } /* Literal.Number.Integer */
660 660 .mo { color: #666666 } /* Literal.Number.Oct */
661 661 .sa { color: #BA2121 } /* Literal.String.Affix */
662 662 .sb { color: #BA2121 } /* Literal.String.Backtick */
663 663 .sc { color: #BA2121 } /* Literal.String.Char */
664 664 .dl { color: #BA2121 } /* Literal.String.Delimiter */
665 665 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
666 666 .s2 { color: #BA2121 } /* Literal.String.Double */
667 667 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
668 668 .sh { color: #BA2121 } /* Literal.String.Heredoc */
669 669 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
670 670 .sx { color: #008000 } /* Literal.String.Other */
671 671 .sr { color: #BB6688 } /* Literal.String.Regex */
672 672 .s1 { color: #BA2121 } /* Literal.String.Single */
673 673 .ss { color: #19177C } /* Literal.String.Symbol */
674 674 .bp { color: #008000 } /* Name.Builtin.Pseudo */
675 675 .fm { color: #0000FF } /* Name.Function.Magic */
676 676 .vc { color: #19177C } /* Name.Variable.Class */
677 677 .vg { color: #19177C } /* Name.Variable.Global */
678 678 .vi { color: #19177C } /* Name.Variable.Instance */
679 679 .vm { color: #19177C } /* Name.Variable.Magic */
680 680 .il { color: #666666 } /* Literal.Number.Integer.Long */
681 681
682 682 }
683 683
684 684 /* customized pre blocks for markdown/rst */
685 685 pre.literal-block, .codehilite pre{
686 686 padding: @padding;
687 687 border: 1px solid @grey6;
688 688 .border-radius(@border-radius);
689 689 background-color: @grey7;
690 690 }
691 691
692 692
693 693 /* START NEW CODE BLOCK CSS */
694 694
695 695 @cb-line-height: 18px;
696 696 @cb-line-code-padding: 10px;
697 697 @cb-text-padding: 5px;
698 698
699 699 @pill-padding: 2px 7px;
700 700 @pill-padding-small: 2px 2px 1px 2px;
701 701
702 702 input.filediff-collapse-state {
703 703 display: none;
704 704
705 705 &:checked + .filediff { /* file diff is collapsed */
706 706 .cb {
707 707 display: none
708 708 }
709 709 .filediff-collapse-indicator {
710 710 float: left;
711 711 cursor: pointer;
712 712 margin: 1px -5px;
713 713 }
714 714 .filediff-collapse-indicator:before {
715 715 content: '\f105';
716 716 }
717 717
718 718 .filediff-menu {
719 719 display: none;
720 720 }
721 721
722 722 }
723 723
724 724 &+ .filediff { /* file diff is expanded */
725 725
726 726 .filediff-collapse-indicator {
727 727 float: left;
728 728 cursor: pointer;
729 729 margin: 1px -5px;
730 730 }
731 731 .filediff-collapse-indicator:before {
732 732 content: '\f107';
733 733 }
734 734
735 735 .filediff-menu {
736 736 display: block;
737 737 }
738 738
739 739 margin: 10px 0;
740 740 &:nth-child(2) {
741 741 margin: 0;
742 742 }
743 743 }
744 744 }
745 745
746 746 .filediffs .anchor {
747 747 display: block;
748 748 height: 40px;
749 749 margin-top: -40px;
750 750 visibility: hidden;
751 751 }
752 752
753 753 .filediffs .anchor:nth-of-type(1) {
754 754 display: block;
755 755 height: 80px;
756 756 margin-top: -80px;
757 757 visibility: hidden;
758 758 }
759 759
760 760 .cs_files {
761 761 clear: both;
762 762 }
763 763
764 764 #diff-file-sticky{
765 765 will-change: min-height;
766 766 height: 80px;
767 767 }
768 768
769 769 .sidebar__inner{
770 770 transform: translate(0, 0); /* For browsers don't support translate3d. */
771 771 transform: translate3d(0, 0, 0);
772 772 will-change: position, transform;
773 773 height: 65px;
774 774 background-color: #fff;
775 775 padding: 5px 0px;
776 776 }
777 777
778 778 .sidebar__bar {
779 779 padding: 5px 0px 0px 0px
780 780 }
781 781
782 782 .fpath-placeholder {
783 783 clear: both;
784 784 visibility: hidden
785 785 }
786 786
787 787 .is-affixed {
788 788
789 789 .sidebar__inner {
790 790 z-index: 30;
791 791 }
792 792
793 793 .sidebar_inner_shadow {
794 794 position: fixed;
795 795 top: 75px;
796 796 right: -100%;
797 797 left: -100%;
798 798 z-index: 30;
799 799 display: block;
800 800 height: 5px;
801 801 content: "";
802 802 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
803 803 border-top: 1px solid rgba(0, 0, 0, 0.15);
804 804 }
805 805
806 806 .fpath-placeholder {
807 807 visibility: visible !important;
808 808 }
809 809 }
810 810
811 811 .diffset-menu {
812 812
813 813 }
814 814
815 815 #todo-box {
816 816 clear:both;
817 817 display: none;
818 818 text-align: right
819 819 }
820 820
821 821 .diffset {
822 822 margin: 0px auto;
823 823 .diffset-heading {
824 824 border: 1px solid @grey5;
825 825 margin-bottom: -1px;
826 826 // margin-top: 20px;
827 827 h2 {
828 828 margin: 0;
829 829 line-height: 38px;
830 830 padding-left: 10px;
831 831 }
832 832 .btn {
833 833 margin: 0;
834 834 }
835 835 background: @grey6;
836 836 display: block;
837 837 padding: 5px;
838 838 }
839 839 .diffset-heading-warning {
840 840 background: @alert3-inner;
841 841 border: 1px solid @alert3;
842 842 }
843 843 &.diffset-comments-disabled {
844 844 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
845 845 display: none !important;
846 846 }
847 847 }
848 848 }
849 849
850 850 .filelist {
851 851 .pill {
852 852 display: block;
853 853 float: left;
854 854 padding: @pill-padding-small;
855 855 }
856 856 }
857 857
858 858 .pill {
859 859 display: block;
860 860 float: left;
861 861 padding: @pill-padding;
862 862 }
863 863
864 864 .pill-group {
865 865 .pill {
866 866 opacity: .8;
867 867 margin-right: 3px;
868 868 font-size: 12px;
869 869 font-weight: normal;
870 870 min-width: 30px;
871 871 text-align: center;
872 872
873 873 &:first-child {
874 874 border-radius: @border-radius 0 0 @border-radius;
875 875 }
876 876 &:last-child {
877 877 border-radius: 0 @border-radius @border-radius 0;
878 878 }
879 879 &:only-child {
880 880 border-radius: @border-radius;
881 881 margin-right: 0;
882 882 }
883 883 }
884 884 }
885 885
886 886 /* Main comments*/
887 887 #comments {
888 888 .comment-selected {
889 889 border-left: 6px solid @comment-highlight-color;
890 890 padding-left: 3px;
891 891 margin-left: -9px;
892 892 }
893 893 }
894 894
895 895 .filediff {
896 896 border: 1px solid @grey5;
897 897
898 898 /* START OVERRIDES */
899 899 .code-highlight {
900 900 border: none; // TODO: remove this border from the global
901 901 // .code-highlight, it doesn't belong there
902 902 }
903 903 label {
904 904 margin: 0; // TODO: remove this margin definition from global label
905 905 // it doesn't belong there - if margin on labels
906 906 // are needed for a form they should be defined
907 907 // in the form's class
908 908 }
909 909 /* END OVERRIDES */
910 910
911 911 * {
912 912 box-sizing: border-box;
913 913 }
914 914
915 915 .on-hover-icon {
916 916 visibility: hidden;
917 917 }
918 918
919 919 .filediff-anchor {
920 920 visibility: hidden;
921 921 }
922 922 &:hover {
923 923 .filediff-anchor {
924 924 visibility: visible;
925 925 }
926 926 .on-hover-icon {
927 927 visibility: visible;
928 928 }
929 929 }
930 930
931 931 .filediff-heading {
932 932 cursor: pointer;
933 933 display: block;
934 934 padding: 10px 10px;
935 935 }
936 936 .filediff-heading:after {
937 937 content: "";
938 938 display: table;
939 939 clear: both;
940 940 }
941 941 .filediff-heading:hover {
942 942 background: #e1e9f4 !important;
943 943 }
944 944
945 945 .filediff-menu {
946 946 text-align: right;
947 947 padding: 5px 5px 5px 0px;
948 948 background: @grey7;
949 949
950 950 &> a,
951 951 &> span {
952 952 padding: 1px;
953 953 }
954 954 }
955 955
956 956 .filediff-collapse-button, .filediff-expand-button {
957 957 cursor: pointer;
958 958 }
959 959 .filediff-collapse-button {
960 960 display: inline;
961 961 }
962 962 .filediff-expand-button {
963 963 display: none;
964 964 }
965 965 .filediff-collapsed .filediff-collapse-button {
966 966 display: none;
967 967 }
968 968 .filediff-collapsed .filediff-expand-button {
969 969 display: inline;
970 970 }
971 971
972 972 /**** COMMENTS ****/
973 973
974 974 .filediff-menu {
975 975 .show-comment-button {
976 976 display: none;
977 977 }
978 978 }
979 979 &.hide-comments {
980 980 .inline-comments {
981 981 display: none;
982 982 }
983 983 .filediff-menu {
984 984 .show-comment-button {
985 985 display: inline;
986 986 }
987 987 .hide-comment-button {
988 988 display: none;
989 989 }
990 990 }
991 991 }
992 992
993 993 .hide-line-comments {
994 994 .inline-comments {
995 995 display: none;
996 996 }
997 997 }
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001 1001
1002 1002 .nav-chunk {
1003 1003 position: absolute;
1004 1004 right: 20px;
1005 1005 margin-top: -17px;
1006 1006 }
1007 1007
1008 1008 .nav-chunk.selected {
1009 1009 visibility: visible !important;
1010 1010 }
1011 1011
1012 1012 #diff_nav {
1013 1013 color: @grey3;
1014 1014 }
1015 1015
1016 1016 }
1017 1017
1018 1018
1019 1019 .op-added {
1020 1020 color: @alert1;
1021 1021 }
1022 1022
1023 1023 .op-deleted {
1024 1024 color: @alert2;
1025 1025 }
1026 1026
1027 1027 .filediff, .filelist {
1028 1028
1029 1029 .pill {
1030 1030 &[op="name"] {
1031 1031 background: none;
1032 1032 opacity: 1;
1033 1033 color: white;
1034 1034 }
1035 1035 &[op="limited"] {
1036 1036 background: @grey2;
1037 1037 color: white;
1038 1038 }
1039 1039 &[op="binary"] {
1040 1040 background: @color7;
1041 1041 color: white;
1042 1042 }
1043 1043 &[op="modified"] {
1044 1044 background: @alert1;
1045 1045 color: white;
1046 1046 }
1047 1047 &[op="renamed"] {
1048 1048 background: @color4;
1049 1049 color: white;
1050 1050 }
1051 1051 &[op="copied"] {
1052 1052 background: @color4;
1053 1053 color: white;
1054 1054 }
1055 1055 &[op="mode"] {
1056 1056 background: @grey3;
1057 1057 color: white;
1058 1058 }
1059 1059 &[op="symlink"] {
1060 1060 background: @color8;
1061 1061 color: white;
1062 1062 }
1063 1063
1064 1064 &[op="added"] { /* added lines */
1065 1065 background: @alert1;
1066 1066 color: white;
1067 1067 }
1068 1068 &[op="deleted"] { /* deleted lines */
1069 1069 background: @alert2;
1070 1070 color: white;
1071 1071 }
1072 1072
1073 1073 &[op="created"] { /* created file */
1074 1074 background: @alert1;
1075 1075 color: white;
1076 1076 }
1077 1077 &[op="removed"] { /* deleted file */
1078 1078 background: @color5;
1079 1079 color: white;
1080 1080 }
1081
1081 1082 &[op="comments"] { /* comments on file */
1082 1083 background: @grey4;
1083 1084 color: white;
1084 1085 }
1086
1087 &[op="options"] { /* context menu */
1088 background: @grey6;
1089 color: black;
1090 }
1085 1091 }
1086 1092 }
1087 1093
1088 1094
1089 1095 .filediff-outdated {
1090 1096 padding: 8px 0;
1091 1097
1092 1098 .filediff-heading {
1093 1099 opacity: .5;
1094 1100 }
1095 1101 }
1096 1102
1097 1103 table.cb {
1098 1104 width: 100%;
1099 1105 border-collapse: collapse;
1100 1106
1101 1107 .cb-text {
1102 1108 padding: @cb-text-padding;
1103 1109 }
1104 1110 .cb-hunk {
1105 1111 padding: @cb-text-padding;
1106 1112 }
1107 1113 .cb-expand {
1108 1114 display: none;
1109 1115 }
1110 1116 .cb-collapse {
1111 1117 display: inline;
1112 1118 }
1113 1119 &.cb-collapsed {
1114 1120 .cb-line {
1115 1121 display: none;
1116 1122 }
1117 1123 .cb-expand {
1118 1124 display: inline;
1119 1125 }
1120 1126 .cb-collapse {
1121 1127 display: none;
1122 1128 }
1123 1129 .cb-hunk {
1124 1130 display: none;
1125 1131 }
1126 1132 }
1127 1133
1128 1134 /* intentionally general selector since .cb-line-selected must override it
1129 1135 and they both use !important since the td itself may have a random color
1130 1136 generated by annotation blocks. TLDR: if you change it, make sure
1131 1137 annotated block selection and line selection in file view still work */
1132 1138 .cb-line-fresh .cb-content {
1133 1139 background: white !important;
1134 1140 }
1135 1141 .cb-warning {
1136 1142 background: #fff4dd;
1137 1143 }
1138 1144
1139 1145 &.cb-diff-sideside {
1140 1146 td {
1141 1147 &.cb-content {
1142 1148 width: 50%;
1143 1149 }
1144 1150 }
1145 1151 }
1146 1152
1147 1153 tr {
1148 1154 &.cb-annotate {
1149 1155 border-top: 1px solid #eee;
1150 1156 }
1151 1157
1152 1158 &.cb-comment-info {
1153 1159 border-top: 1px solid #eee;
1154 1160 color: rgba(0, 0, 0, 0.3);
1155 1161 background: #edf2f9;
1156 1162
1157 1163 td {
1158 1164
1159 1165 }
1160 1166 }
1161 1167
1162 1168 &.cb-hunk {
1163 1169 font-family: @text-monospace;
1164 1170 color: rgba(0, 0, 0, 0.3);
1165 1171
1166 1172 td {
1167 1173 &:first-child {
1168 1174 background: #edf2f9;
1169 1175 }
1170 1176 &:last-child {
1171 1177 background: #f4f7fb;
1172 1178 }
1173 1179 }
1174 1180 }
1175 1181 }
1176 1182
1177 1183
1178 1184 td {
1179 1185 vertical-align: top;
1180 1186 padding: 0;
1181 1187
1182 1188 &.cb-content {
1183 1189 font-size: 12.35px;
1184 1190
1185 1191 &.cb-line-selected .cb-code {
1186 1192 background: @comment-highlight-color !important;
1187 1193 }
1188 1194
1189 1195 span.cb-code {
1190 1196 line-height: @cb-line-height;
1191 1197 padding-left: @cb-line-code-padding;
1192 1198 padding-right: @cb-line-code-padding;
1193 1199 display: block;
1194 1200 white-space: pre-wrap;
1195 1201 font-family: @text-monospace;
1196 1202 word-break: break-all;
1197 1203 .nonl {
1198 1204 color: @color5;
1199 1205 }
1200 1206 .cb-action {
1201 1207 &:before {
1202 1208 content: " ";
1203 1209 }
1204 1210 &.cb-deletion:before {
1205 1211 content: "- ";
1206 1212 }
1207 1213 &.cb-addition:before {
1208 1214 content: "+ ";
1209 1215 }
1210 1216 }
1211 1217 }
1212 1218
1213 1219 &> button.cb-comment-box-opener {
1214 1220
1215 1221 padding: 2px 2px 1px 3px;
1216 1222 margin-left: -6px;
1217 1223 margin-top: -1px;
1218 1224
1219 1225 border-radius: @border-radius;
1220 1226 position: absolute;
1221 1227 display: none;
1222 1228 }
1223 1229 .cb-comment {
1224 1230 margin-top: 10px;
1225 1231 white-space: normal;
1226 1232 }
1227 1233 }
1228 1234 &:hover {
1229 1235 button.cb-comment-box-opener {
1230 1236 display: block;
1231 1237 }
1232 1238 &+ td button.cb-comment-box-opener {
1233 1239 display: block
1234 1240 }
1235 1241 }
1236 1242
1237 1243 &.cb-data {
1238 1244 text-align: right;
1239 1245 width: 30px;
1240 1246 font-family: @text-monospace;
1241 1247
1242 1248 .icon-comment {
1243 1249 cursor: pointer;
1244 1250 }
1245 1251 &.cb-line-selected {
1246 1252 background: @comment-highlight-color !important;
1247 1253 }
1248 1254 &.cb-line-selected > div {
1249 1255 display: block;
1250 1256 background: @comment-highlight-color !important;
1251 1257 line-height: @cb-line-height;
1252 1258 color: rgba(0, 0, 0, 0.3);
1253 1259 }
1254 1260 }
1255 1261
1256 1262 &.cb-lineno {
1257 1263 padding: 0;
1258 1264 width: 50px;
1259 1265 color: rgba(0, 0, 0, 0.3);
1260 1266 text-align: right;
1261 1267 border-right: 1px solid #eee;
1262 1268 font-family: @text-monospace;
1263 1269 -webkit-user-select: none;
1264 1270 -moz-user-select: none;
1265 1271 user-select: none;
1266 1272
1267 1273 a::before {
1268 1274 content: attr(data-line-no);
1269 1275 }
1270 1276 &.cb-line-selected {
1271 1277 background: @comment-highlight-color !important;
1272 1278 }
1273 1279
1274 1280 a {
1275 1281 display: block;
1276 1282 padding-right: @cb-line-code-padding;
1277 1283 padding-left: @cb-line-code-padding;
1278 1284 line-height: @cb-line-height;
1279 1285 color: rgba(0, 0, 0, 0.3);
1280 1286 }
1281 1287 }
1282 1288
1283 1289 &.cb-empty {
1284 1290 background: @grey7;
1285 1291 }
1286 1292
1287 1293 ins {
1288 1294 color: black;
1289 1295 background: #a6f3a6;
1290 1296 text-decoration: none;
1291 1297 }
1292 1298 del {
1293 1299 color: black;
1294 1300 background: #f8cbcb;
1295 1301 text-decoration: none;
1296 1302 }
1297 1303 &.cb-addition {
1298 1304 background: #ecffec;
1299 1305
1300 1306 &.blob-lineno {
1301 1307 background: #ddffdd;
1302 1308 }
1303 1309 }
1304 1310 &.cb-deletion {
1305 1311 background: #ffecec;
1306 1312
1307 1313 &.blob-lineno {
1308 1314 background: #ffdddd;
1309 1315 }
1310 1316 }
1311 1317 &.cb-annotate-message-spacer {
1312 1318 width:8px;
1313 1319 padding: 1px 0px 0px 3px;
1314 1320 }
1315 1321 &.cb-annotate-info {
1316 1322 width: 320px;
1317 1323 min-width: 320px;
1318 1324 max-width: 320px;
1319 1325 padding: 5px 2px;
1320 1326 font-size: 13px;
1321 1327
1322 1328 .cb-annotate-message {
1323 1329 padding: 2px 0px 0px 0px;
1324 1330 white-space: pre-line;
1325 1331 overflow: hidden;
1326 1332 }
1327 1333 .rc-user {
1328 1334 float: none;
1329 1335 padding: 0 6px 0 17px;
1330 1336 min-width: unset;
1331 1337 min-height: unset;
1332 1338 }
1333 1339 }
1334 1340
1335 1341 &.cb-annotate-revision {
1336 1342 cursor: pointer;
1337 1343 text-align: right;
1338 1344 padding: 1px 3px 0px 3px;
1339 1345 }
1340 1346 }
1341 1347 }
@@ -1,104 +1,108 b''
1 1 //--- RESETS ---//
2 2 :focus { outline: none; }
3 3 a { cursor: pointer; }
4 4
5 5 //--- clearfix --//
6 6 .clearfix {
7 7 &:before,
8 8 &:after {
9 9 content:"";
10 10 width: 100%;
11 11 clear: both;
12 12 float: left;
13 13 }
14 14 }
15 15
16 16 .clearinner:after { /* clears all floating divs inside a block */
17 17 content: "";
18 18 display: table;
19 19 clear: both;
20 20 }
21 21
22 22 .js-template { /* mark a template for javascript use */
23 23 display: none;
24 24 }
25 25
26 26 .linebreak {
27 27 display: block;
28 28 }
29 29
30 30 .clear-both {
31 31 clear: both;
32 32 }
33 33
34 .display-none {
35 display: none;
36 }
37
34 38 .pull-right {
35 39 float: right !important;
36 40 }
37 41
38 42 .pull-left {
39 43 float: left !important;
40 44 }
41 45
42 46 .block-left {
43 47 float: left;
44 48 }
45 49
46 50 .block-right {
47 51 float: right;
48 52 clear: right;
49 53
50 54 li {
51 55 list-style-type: none;
52 56 }
53 57 }
54 58
55 59 .noselect {
56 60 -webkit-touch-callout: none; /* iOS Safari */
57 61 -webkit-user-select: none; /* Safari */
58 62 -khtml-user-select: none; /* Konqueror HTML */
59 63 -moz-user-select: none; /* Firefox */
60 64 -ms-user-select: none; /* Internet Explorer/Edge */
61 65 user-select: none; /* Non-prefixed version, currently
62 66 supported by Chrome and Opera */
63 67 }
64 68
65 69 //--- DEVICE-SPECIFIC CLASSES ---------------//
66 70 //regular tablet and up
67 71 @media (min-width:768px) {
68 72 .no-mobile {
69 73 display: block;
70 74 }
71 75 .mobile-only {
72 76 display: none;
73 77 }
74 78 }
75 79 //small tablet and phone
76 80 @media (max-width:767px) {
77 81 .mobile-only {
78 82 display: block;
79 83 }
80 84 .no-mobile {
81 85 display: none;
82 86 }
83 87 }
84 88
85 89 //--- STICKY FOOTER ---//
86 90 html, body {
87 91 height: 100%;
88 92 margin: 0;
89 93 }
90 94 .outerwrapper {
91 95 height: 100%;
92 96 min-height: 100%;
93 97 margin: 0;
94 98 padding-bottom: 3em; /* must be equal to footer height */
95 99 }
96 100 .outerwrapper:after{
97 101 content:" ";
98 102 }
99 103 #footer {
100 104 clear: both;
101 105 position: relative;
102 106 height: 3em; /* footer height */
103 107 margin: -3em 0 0; /* must be equal to footer height */
104 108 }
@@ -1,3056 +1,3211 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 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
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 @import 'tooltips';
30 30 @import 'sweetalert2';
31 31
32 32
33 33 //--- BASE ------------------//
34 34 .noscript-error {
35 35 top: 0;
36 36 left: 0;
37 37 width: 100%;
38 38 z-index: 101;
39 39 text-align: center;
40 40 font-size: 120%;
41 41 color: white;
42 42 background-color: @alert2;
43 43 padding: 5px 0 5px 0;
44 44 font-weight: @text-semibold-weight;
45 45 font-family: @text-semibold;
46 46 }
47 47
48 48 html {
49 49 display: table;
50 50 height: 100%;
51 51 width: 100%;
52 52 }
53 53
54 54 body {
55 55 display: table-cell;
56 56 width: 100%;
57 57 }
58 58
59 59 //--- LAYOUT ------------------//
60 60
61 61 .hidden{
62 62 display: none !important;
63 63 }
64 64
65 65 .box{
66 66 float: left;
67 67 width: 100%;
68 68 }
69 69
70 70 .browser-header {
71 71 clear: both;
72 72 }
73 73 .main {
74 74 clear: both;
75 75 padding:0 0 @pagepadding;
76 76 height: auto;
77 77
78 78 &:after { //clearfix
79 79 content:"";
80 80 clear:both;
81 81 width:100%;
82 82 display:block;
83 83 }
84 84 }
85 85
86 .flex-container {
87 display: flex;
88 justify-content: space-between;
89 }
90
86 91 .action-link{
87 92 margin-left: @padding;
88 93 padding-left: @padding;
89 94 border-left: @border-thickness solid @border-default-color;
90 95 }
91 96
92 97 .cursor-pointer {
93 98 cursor: pointer;
94 99 }
95 100
96 101 input + .action-link, .action-link.first{
97 102 border-left: none;
98 103 }
99 104
100 105 .link-disabled {
101 106 color: @grey4;
102 107 cursor: default;
103 108 }
104 109
105 110 .action-link.last{
106 111 margin-right: @padding;
107 112 padding-right: @padding;
108 113 }
109 114
110 115 .action-link.active,
111 116 .action-link.active a{
112 117 color: @grey4;
113 118 }
114 119
115 120 .action-link.disabled {
116 121 color: @grey4;
117 122 cursor: inherit;
118 123 }
119 124
120 125 .grey-link-action {
121 126 cursor: pointer;
122 127 &:hover {
123 128 color: @grey2;
124 129 }
125 130 color: @grey4;
126 131 }
127 132
128 133 .clipboard-action {
129 134 cursor: pointer;
130 135 margin-left: 5px;
131 136
132 137 &:not(.no-grey) {
133 138
134 139 &:hover {
135 140 color: @grey2;
136 141 }
137 142 color: @grey4;
138 143 }
139 144 }
140 145
141 146 ul.simple-list{
142 147 list-style: none;
143 148 margin: 0;
144 149 padding: 0;
145 150 }
146 151
147 152 .main-content {
148 153 padding-bottom: @pagepadding;
149 154 }
150 155
151 156 .wide-mode-wrapper {
152 157 max-width:4000px !important;
153 158 }
154 159
155 160 .wrapper {
156 161 position: relative;
157 162 max-width: @wrapper-maxwidth;
158 163 margin: 0 auto;
159 164 }
160 165
161 166 #content {
162 167 clear: both;
163 168 padding: 0 @contentpadding;
164 169 }
165 170
166 171 .advanced-settings-fields{
167 172 input{
168 173 margin-left: @textmargin;
169 174 margin-right: @padding/2;
170 175 }
171 176 }
172 177
173 178 .cs_files_title {
174 179 margin: @pagepadding 0 0;
175 180 }
176 181
177 182 input.inline[type="file"] {
178 183 display: inline;
179 184 }
180 185
181 186 .error_page {
182 187 margin: 10% auto;
183 188
184 189 h1 {
185 190 color: @grey2;
186 191 }
187 192
188 193 .alert {
189 194 margin: @padding 0;
190 195 }
191 196
192 197 .error-branding {
193 198 color: @grey4;
194 199 font-weight: @text-semibold-weight;
195 200 font-family: @text-semibold;
196 201 }
197 202
198 203 .error_message {
199 204 font-family: @text-regular;
200 205 }
201 206
202 207 .sidebar {
203 208 min-height: 275px;
204 209 margin: 0;
205 210 padding: 0 0 @sidebarpadding @sidebarpadding;
206 211 border: none;
207 212 }
208 213
209 214 .main-content {
210 215 position: relative;
211 216 margin: 0 @sidebarpadding @sidebarpadding;
212 217 padding: 0 0 0 @sidebarpadding;
213 218 border-left: @border-thickness solid @grey5;
214 219
215 220 @media (max-width:767px) {
216 221 clear: both;
217 222 width: 100%;
218 223 margin: 0;
219 224 border: none;
220 225 }
221 226 }
222 227
223 228 .inner-column {
224 229 float: left;
225 230 width: 29.75%;
226 231 min-height: 150px;
227 232 margin: @sidebarpadding 2% 0 0;
228 233 padding: 0 2% 0 0;
229 234 border-right: @border-thickness solid @grey5;
230 235
231 236 @media (max-width:767px) {
232 237 clear: both;
233 238 width: 100%;
234 239 border: none;
235 240 }
236 241
237 242 ul {
238 243 padding-left: 1.25em;
239 244 }
240 245
241 246 &:last-child {
242 247 margin: @sidebarpadding 0 0;
243 248 border: none;
244 249 }
245 250
246 251 h4 {
247 252 margin: 0 0 @padding;
248 253 font-weight: @text-semibold-weight;
249 254 font-family: @text-semibold;
250 255 }
251 256 }
252 257 }
253 258 .error-page-logo {
254 259 width: 130px;
255 260 height: 160px;
256 261 }
257 262
258 263 // HEADER
259 264 .header {
260 265
261 266 // TODO: johbo: Fix login pages, so that they work without a min-height
262 267 // for the header and then remove the min-height. I chose a smaller value
263 268 // intentionally here to avoid rendering issues in the main navigation.
264 269 min-height: 49px;
265 270 min-width: 1024px;
266 271
267 272 position: relative;
268 273 vertical-align: bottom;
269 274 padding: 0 @header-padding;
270 275 background-color: @grey1;
271 276 color: @grey5;
272 277
273 278 .title {
274 279 overflow: visible;
275 280 }
276 281
277 282 &:before,
278 283 &:after {
279 284 content: "";
280 285 clear: both;
281 286 width: 100%;
282 287 }
283 288
284 289 // TODO: johbo: Avoids breaking "Repositories" chooser
285 290 .select2-container .select2-choice .select2-arrow {
286 291 display: none;
287 292 }
288 293 }
289 294
290 295 #header-inner {
291 296 &.title {
292 297 margin: 0;
293 298 }
294 299 &:before,
295 300 &:after {
296 301 content: "";
297 302 clear: both;
298 303 }
299 304 }
300 305
301 306 // Gists
302 307 #files_data {
303 308 clear: both; //for firefox
304 309 padding-top: 10px;
305 310 }
306 311
307 312 #gistid {
308 313 margin-right: @padding;
309 314 }
310 315
311 316 // Global Settings Editor
312 317 .textarea.editor {
313 318 float: left;
314 319 position: relative;
315 320 max-width: @texteditor-width;
316 321
317 322 select {
318 323 position: absolute;
319 324 top:10px;
320 325 right:0;
321 326 }
322 327
323 328 .CodeMirror {
324 329 margin: 0;
325 330 }
326 331
327 332 .help-block {
328 333 margin: 0 0 @padding;
329 334 padding:.5em;
330 335 background-color: @grey6;
331 336 &.pre-formatting {
332 337 white-space: pre;
333 338 }
334 339 }
335 340 }
336 341
337 342 ul.auth_plugins {
338 343 margin: @padding 0 @padding @legend-width;
339 344 padding: 0;
340 345
341 346 li {
342 347 margin-bottom: @padding;
343 348 line-height: 1em;
344 349 list-style-type: none;
345 350
346 351 .auth_buttons .btn {
347 352 margin-right: @padding;
348 353 }
349 354
350 355 }
351 356 }
352 357
353 358
354 359 // My Account PR list
355 360
356 361 #show_closed {
357 362 margin: 0 1em 0 0;
358 363 }
359 364
360 365 #pull_request_list_table {
361 366 .closed {
362 367 background-color: @grey6;
363 368 }
364 369
365 370 .state-creating,
366 371 .state-updating,
367 372 .state-merging
368 373 {
369 374 background-color: @grey6;
370 375 }
371 376
372 377 .td-status {
373 378 padding-left: .5em;
374 379 }
375 380 .log-container .truncate {
376 381 height: 2.75em;
377 382 white-space: pre-line;
378 383 }
379 384 table.rctable .user {
380 385 padding-left: 0;
381 386 }
382 387 table.rctable {
383 388 td.td-description,
384 389 .rc-user {
385 390 min-width: auto;
386 391 }
387 392 }
388 393 }
389 394
390 395 // Pull Requests
391 396
392 397 .pullrequests_section_head {
393 398 display: block;
394 399 clear: both;
395 400 margin: @padding 0;
396 401 font-weight: @text-bold-weight;
397 402 font-family: @text-bold;
398 403 }
399 404
400 405 .pr-commit-flow {
401 406 position: relative;
402 407 font-weight: 600;
403 408
404 409 .tag {
405 410 display: inline-block;
406 411 margin: 0 1em .5em 0;
407 412 }
408 413
409 414 .clone-url {
410 415 display: inline-block;
411 416 margin: 0 0 .5em 0;
412 417 padding: 0;
413 418 line-height: 1.2em;
414 419 }
415 420 }
416 421
417 422 .pr-mergeinfo {
418 423 min-width: 95% !important;
419 424 padding: 0 !important;
420 425 border: 0;
421 426 }
422 427 .pr-mergeinfo-copy {
423 428 padding: 0 0;
424 429 }
425 430
426 431 .pr-pullinfo {
427 432 min-width: 95% !important;
428 433 padding: 0 !important;
429 434 border: 0;
430 435 }
431 436 .pr-pullinfo-copy {
432 437 padding: 0 0;
433 438 }
434 439
435 440 .pr-title-input {
436 441 width: 100%;
437 442 font-size: 18px;
438 443 margin: 0 0 4px 0;
439 444 padding: 0;
440 445 line-height: 1.7em;
441 446 color: @text-color;
442 447 letter-spacing: .02em;
443 448 font-weight: @text-bold-weight;
444 449 font-family: @text-bold;
445 450
446 451 &:hover {
447 452 box-shadow: none;
448 453 }
449 454 }
450 455
451 456 #pr-title {
452 457 input {
453 458 border: 1px transparent;
454 459 color: black;
455 460 opacity: 1;
456 461 background: #fff;
457 462 font-size: 18px;
458 463 }
459 464 }
460 465
461 466 .pr-title-closed-tag {
462 467 font-size: 16px;
463 468 }
464 469
465 470 #pr-desc {
466 471 padding: 10px 0;
467 472
468 473 .markdown-block {
469 474 padding: 0;
470 475 margin-bottom: -30px;
471 476 }
472 477 }
473 478
474 479 #pullrequest_title {
475 480 width: 100%;
476 481 box-sizing: border-box;
477 482 }
478 483
479 484 #pr_open_message {
480 485 border: @border-thickness solid #fff;
481 486 border-radius: @border-radius;
482 487 text-align: left;
483 488 overflow: hidden;
484 489 white-space: pre-line;
490 padding-top: 5px
491 }
492
493 #add_reviewer {
494 padding-top: 10px;
495 }
496
497 #add_reviewer_input {
498 padding-top: 10px
485 499 }
486 500
487 501 .pr-details-title-author-pref {
488 502 padding-right: 10px
489 503 }
490 504
491 505 .label-pr-detail {
492 506 display: table-cell;
493 507 width: 120px;
494 508 padding-top: 7.5px;
495 509 padding-bottom: 7.5px;
496 510 padding-right: 7.5px;
497 511 }
498 512
499 513 .source-details ul {
500 514 padding: 10px 16px;
501 515 }
502 516
503 517 .source-details-action {
504 518 color: @grey4;
505 519 font-size: 11px
506 520 }
507 521
508 522 .pr-submit-button {
509 523 float: right;
510 524 margin: 0 0 0 5px;
511 525 }
512 526
513 527 .pr-spacing-container {
514 528 padding: 20px;
515 529 clear: both
516 530 }
517 531
518 532 #pr-description-input {
519 533 margin-bottom: 0;
520 534 }
521 535
522 536 .pr-description-label {
523 537 vertical-align: top;
524 538 }
525 539
526 540 #open_edit_pullrequest {
527 541 padding: 0;
528 542 }
529 543
530 544 #close_edit_pullrequest {
531 545
532 546 }
533 547
534 548 #delete_pullrequest {
535 549 clear: inherit;
536 550
537 551 form {
538 552 display: inline;
539 553 }
540 554
541 555 }
542 556
543 557 .perms_section_head {
544 558 min-width: 625px;
545 559
546 560 h2 {
547 561 margin-bottom: 0;
548 562 }
549 563
550 564 .label-checkbox {
551 565 float: left;
552 566 }
553 567
554 568 &.field {
555 569 margin: @space 0 @padding;
556 570 }
557 571
558 572 &:first-child.field {
559 573 margin-top: 0;
560 574
561 575 .label {
562 576 margin-top: 0;
563 577 padding-top: 0;
564 578 }
565 579
566 580 .radios {
567 581 padding-top: 0;
568 582 }
569 583 }
570 584
571 585 .radios {
572 586 position: relative;
573 587 width: 505px;
574 588 }
575 589 }
576 590
577 591 //--- MODULES ------------------//
578 592
579 593
580 594 // Server Announcement
581 595 #server-announcement {
582 596 width: 95%;
583 597 margin: @padding auto;
584 598 padding: @padding;
585 599 border-width: 2px;
586 600 border-style: solid;
587 601 .border-radius(2px);
588 602 font-weight: @text-bold-weight;
589 603 font-family: @text-bold;
590 604
591 605 &.info { border-color: @alert4; background-color: @alert4-inner; }
592 606 &.warning { border-color: @alert3; background-color: @alert3-inner; }
593 607 &.error { border-color: @alert2; background-color: @alert2-inner; }
594 608 &.success { border-color: @alert1; background-color: @alert1-inner; }
595 609 &.neutral { border-color: @grey3; background-color: @grey6; }
596 610 }
597 611
598 612 // Fixed Sidebar Column
599 613 .sidebar-col-wrapper {
600 614 padding-left: @sidebar-all-width;
601 615
602 616 .sidebar {
603 617 width: @sidebar-width;
604 618 margin-left: -@sidebar-all-width;
605 619 }
606 620 }
607 621
608 622 .sidebar-col-wrapper.scw-small {
609 623 padding-left: @sidebar-small-all-width;
610 624
611 625 .sidebar {
612 626 width: @sidebar-small-width;
613 627 margin-left: -@sidebar-small-all-width;
614 628 }
615 629 }
616 630
617 631
618 632 // FOOTER
619 633 #footer {
620 634 padding: 0;
621 635 text-align: center;
622 636 vertical-align: middle;
623 637 color: @grey2;
624 638 font-size: 11px;
625 639
626 640 p {
627 641 margin: 0;
628 642 padding: 1em;
629 643 line-height: 1em;
630 644 }
631 645
632 646 .server-instance { //server instance
633 647 display: none;
634 648 }
635 649
636 650 .title {
637 651 float: none;
638 652 margin: 0 auto;
639 653 }
640 654 }
641 655
642 656 button.close {
643 657 padding: 0;
644 658 cursor: pointer;
645 659 background: transparent;
646 660 border: 0;
647 661 .box-shadow(none);
648 662 -webkit-appearance: none;
649 663 }
650 664
651 665 .close {
652 666 float: right;
653 667 font-size: 21px;
654 668 font-family: @text-bootstrap;
655 669 line-height: 1em;
656 670 font-weight: bold;
657 671 color: @grey2;
658 672
659 673 &:hover,
660 674 &:focus {
661 675 color: @grey1;
662 676 text-decoration: none;
663 677 cursor: pointer;
664 678 }
665 679 }
666 680
667 681 // GRID
668 682 .sorting,
669 683 .sorting_desc,
670 684 .sorting_asc {
671 685 cursor: pointer;
672 686 }
673 687 .sorting_desc:after {
674 688 content: "\00A0\25B2";
675 689 font-size: .75em;
676 690 }
677 691 .sorting_asc:after {
678 692 content: "\00A0\25BC";
679 693 font-size: .68em;
680 694 }
681 695
682 696
683 697 .user_auth_tokens {
684 698
685 699 &.truncate {
686 700 white-space: nowrap;
687 701 overflow: hidden;
688 702 text-overflow: ellipsis;
689 703 }
690 704
691 705 .fields .field .input {
692 706 margin: 0;
693 707 }
694 708
695 709 input#description {
696 710 width: 100px;
697 711 margin: 0;
698 712 }
699 713
700 714 .drop-menu {
701 715 // TODO: johbo: Remove this, should work out of the box when
702 716 // having multiple inputs inline
703 717 margin: 0 0 0 5px;
704 718 }
705 719 }
706 720 #user_list_table {
707 721 .closed {
708 722 background-color: @grey6;
709 723 }
710 724 }
711 725
712 726
713 727 input, textarea {
714 728 &.disabled {
715 729 opacity: .5;
716 730 }
717 731
718 732 &:hover {
719 733 border-color: @grey3;
720 734 box-shadow: @button-shadow;
721 735 }
722 736
723 737 &:focus {
724 738 border-color: @rcblue;
725 739 box-shadow: @button-shadow;
726 740 }
727 741 }
728 742
729 743 // remove extra padding in firefox
730 744 input::-moz-focus-inner { border:0; padding:0 }
731 745
732 746 .adjacent input {
733 747 margin-bottom: @padding;
734 748 }
735 749
736 750 .permissions_boxes {
737 751 display: block;
738 752 }
739 753
740 754 //FORMS
741 755
742 756 .medium-inline,
743 757 input#description.medium-inline {
744 758 display: inline;
745 759 width: @medium-inline-input-width;
746 760 min-width: 100px;
747 761 }
748 762
749 763 select {
750 764 //reset
751 765 -webkit-appearance: none;
752 766 -moz-appearance: none;
753 767
754 768 display: inline-block;
755 769 height: 28px;
756 770 width: auto;
757 771 margin: 0 @padding @padding 0;
758 772 padding: 0 18px 0 8px;
759 773 line-height:1em;
760 774 font-size: @basefontsize;
761 775 border: @border-thickness solid @grey5;
762 776 border-radius: @border-radius;
763 777 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
764 778 color: @grey4;
765 779 box-shadow: @button-shadow;
766 780
767 781 &:after {
768 782 content: "\00A0\25BE";
769 783 }
770 784
771 785 &:focus, &:hover {
772 786 outline: none;
773 787 border-color: @grey4;
774 788 color: @rcdarkblue;
775 789 }
776 790 }
777 791
778 792 option {
779 793 &:focus {
780 794 outline: none;
781 795 }
782 796 }
783 797
784 798 input,
785 799 textarea {
786 800 padding: @input-padding;
787 801 border: @input-border-thickness solid @border-highlight-color;
788 802 .border-radius (@border-radius);
789 803 font-family: @text-light;
790 804 font-size: @basefontsize;
791 805
792 806 &.input-sm {
793 807 padding: 5px;
794 808 }
795 809
796 810 &#description {
797 811 min-width: @input-description-minwidth;
798 812 min-height: 1em;
799 813 padding: 10px;
800 814 }
801 815 }
802 816
803 817 .field-sm {
804 818 input,
805 819 textarea {
806 820 padding: 5px;
807 821 }
808 822 }
809 823
810 824 textarea {
811 825 display: block;
812 826 clear: both;
813 827 width: 100%;
814 828 min-height: 100px;
815 829 margin-bottom: @padding;
816 830 .box-sizing(border-box);
817 831 overflow: auto;
818 832 }
819 833
820 834 label {
821 835 font-family: @text-light;
822 836 }
823 837
824 838 // GRAVATARS
825 839 // centers gravatar on username to the right
826 840
827 841 .gravatar {
828 842 display: inline;
829 843 min-width: 16px;
830 844 min-height: 16px;
831 845 margin: -5px 0;
832 846 padding: 0;
833 847 line-height: 1em;
834 848 box-sizing: content-box;
835 849 border-radius: 50%;
836 850
837 851 &.gravatar-large {
838 852 margin: -0.5em .25em -0.5em 0;
839 853 }
840 854
841 855 & + .user {
842 856 display: inline;
843 857 margin: 0;
844 858 padding: 0 0 0 .17em;
845 859 line-height: 1em;
846 860 }
847 861
848 862 & + .no-margin {
849 863 margin: 0
850 864 }
851 865
852 866 }
853 867
854 868 .user-inline-data {
855 869 display: inline-block;
856 870 float: left;
857 871 padding-left: .5em;
858 872 line-height: 1.3em;
859 873 }
860 874
861 875 .rc-user { // gravatar + user wrapper
862 876 float: left;
863 877 position: relative;
864 878 min-width: 100px;
865 879 max-width: 200px;
866 880 min-height: (@gravatar-size + @border-thickness * 2); // account for border
867 881 display: block;
868 882 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
869 883
870 884
871 885 .gravatar {
872 886 display: block;
873 887 position: absolute;
874 888 top: 0;
875 889 left: 0;
876 890 min-width: @gravatar-size;
877 891 min-height: @gravatar-size;
878 892 margin: 0;
879 893 }
880 894
881 895 .user {
882 896 display: block;
883 897 max-width: 175px;
884 898 padding-top: 2px;
885 899 overflow: hidden;
886 900 text-overflow: ellipsis;
887 901 }
888 902 }
889 903
890 904 .gist-gravatar,
891 905 .journal_container {
892 906 .gravatar-large {
893 907 margin: 0 .5em -10px 0;
894 908 }
895 909 }
896 910
897 911 .gist-type-fields {
898 912 line-height: 30px;
899 913 height: 30px;
900 914
901 915 .gist-type-fields-wrapper {
902 916 vertical-align: middle;
903 917 display: inline-block;
904 918 line-height: 25px;
905 919 }
906 920 }
907 921
908 922 // ADMIN SETTINGS
909 923
910 924 // Tag Patterns
911 925 .tag_patterns {
912 926 .tag_input {
913 927 margin-bottom: @padding;
914 928 }
915 929 }
916 930
917 931 .locked_input {
918 932 position: relative;
919 933
920 934 input {
921 935 display: inline;
922 936 margin: 3px 5px 0px 0px;
923 937 }
924 938
925 939 br {
926 940 display: none;
927 941 }
928 942
929 943 .error-message {
930 944 float: left;
931 945 width: 100%;
932 946 }
933 947
934 948 .lock_input_button {
935 949 display: inline;
936 950 }
937 951
938 952 .help-block {
939 953 clear: both;
940 954 }
941 955 }
942 956
943 957 // Notifications
944 958
945 959 .notifications_buttons {
946 960 margin: 0 0 @space 0;
947 961 padding: 0;
948 962
949 963 .btn {
950 964 display: inline-block;
951 965 }
952 966 }
953 967
954 968 .notification-list {
955 969
956 970 div {
957 971 vertical-align: middle;
958 972 }
959 973
960 974 .container {
961 975 display: block;
962 976 margin: 0 0 @padding 0;
963 977 }
964 978
965 979 .delete-notifications {
966 980 margin-left: @padding;
967 981 text-align: right;
968 982 cursor: pointer;
969 983 }
970 984
971 985 .read-notifications {
972 986 margin-left: @padding/2;
973 987 text-align: right;
974 988 width: 35px;
975 989 cursor: pointer;
976 990 }
977 991
978 992 .icon-minus-sign {
979 993 color: @alert2;
980 994 }
981 995
982 996 .icon-ok-sign {
983 997 color: @alert1;
984 998 }
985 999 }
986 1000
987 1001 .user_settings {
988 1002 float: left;
989 1003 clear: both;
990 1004 display: block;
991 1005 width: 100%;
992 1006
993 1007 .gravatar_box {
994 1008 margin-bottom: @padding;
995 1009
996 1010 &:after {
997 1011 content: " ";
998 1012 clear: both;
999 1013 width: 100%;
1000 1014 }
1001 1015 }
1002 1016
1003 1017 .fields .field {
1004 1018 clear: both;
1005 1019 }
1006 1020 }
1007 1021
1008 1022 .advanced_settings {
1009 1023 margin-bottom: @space;
1010 1024
1011 1025 .help-block {
1012 1026 margin-left: 0;
1013 1027 }
1014 1028
1015 1029 button + .help-block {
1016 1030 margin-top: @padding;
1017 1031 }
1018 1032 }
1019 1033
1020 1034 // admin settings radio buttons and labels
1021 1035 .label-2 {
1022 1036 float: left;
1023 1037 width: @label2-width;
1024 1038
1025 1039 label {
1026 1040 color: @grey1;
1027 1041 }
1028 1042 }
1029 1043 .checkboxes {
1030 1044 float: left;
1031 1045 width: @checkboxes-width;
1032 1046 margin-bottom: @padding;
1033 1047
1034 1048 .checkbox {
1035 1049 width: 100%;
1036 1050
1037 1051 label {
1038 1052 margin: 0;
1039 1053 padding: 0;
1040 1054 }
1041 1055 }
1042 1056
1043 1057 .checkbox + .checkbox {
1044 1058 display: inline-block;
1045 1059 }
1046 1060
1047 1061 label {
1048 1062 margin-right: 1em;
1049 1063 }
1050 1064 }
1051 1065
1052 1066 // CHANGELOG
1053 1067 .container_header {
1054 1068 float: left;
1055 1069 display: block;
1056 1070 width: 100%;
1057 1071 margin: @padding 0 @padding;
1058 1072
1059 1073 #filter_changelog {
1060 1074 float: left;
1061 1075 margin-right: @padding;
1062 1076 }
1063 1077
1064 1078 .breadcrumbs_light {
1065 1079 display: inline-block;
1066 1080 }
1067 1081 }
1068 1082
1069 1083 .info_box {
1070 1084 float: right;
1071 1085 }
1072 1086
1073 1087
1074 1088
1075 1089 #graph_content{
1076 1090
1077 1091 // adjust for table headers so that graph renders properly
1078 1092 // #graph_nodes padding - table cell padding
1079 1093 padding-top: (@space - (@basefontsize * 2.4));
1080 1094
1081 1095 &.graph_full_width {
1082 1096 width: 100%;
1083 1097 max-width: 100%;
1084 1098 }
1085 1099 }
1086 1100
1087 1101 #graph {
1088 1102
1089 1103 .pagination-left {
1090 1104 float: left;
1091 1105 clear: both;
1092 1106 }
1093 1107
1094 1108 .log-container {
1095 1109 max-width: 345px;
1096 1110
1097 1111 .message{
1098 1112 max-width: 340px;
1099 1113 }
1100 1114 }
1101 1115
1102 1116 .graph-col-wrapper {
1103 1117
1104 1118 #graph_nodes {
1105 1119 width: 100px;
1106 1120 position: absolute;
1107 1121 left: 70px;
1108 1122 z-index: -1;
1109 1123 }
1110 1124 }
1111 1125
1112 1126 .load-more-commits {
1113 1127 text-align: center;
1114 1128 }
1115 1129 .load-more-commits:hover {
1116 1130 background-color: @grey7;
1117 1131 }
1118 1132 .load-more-commits {
1119 1133 a {
1120 1134 display: block;
1121 1135 }
1122 1136 }
1123 1137 }
1124 1138
1125 1139 .obsolete-toggle {
1126 1140 line-height: 30px;
1127 1141 margin-left: -15px;
1128 1142 }
1129 1143
1130 1144 #rev_range_container, #rev_range_clear, #rev_range_more {
1131 1145 margin-top: -5px;
1132 1146 margin-bottom: -5px;
1133 1147 }
1134 1148
1135 1149 #filter_changelog {
1136 1150 float: left;
1137 1151 }
1138 1152
1139 1153
1140 1154 //--- THEME ------------------//
1141 1155
1142 1156 #logo {
1143 1157 float: left;
1144 1158 margin: 9px 0 0 0;
1145 1159
1146 1160 .header {
1147 1161 background-color: transparent;
1148 1162 }
1149 1163
1150 1164 a {
1151 1165 display: inline-block;
1152 1166 }
1153 1167
1154 1168 img {
1155 1169 height:30px;
1156 1170 }
1157 1171 }
1158 1172
1159 1173 .logo-wrapper {
1160 1174 float:left;
1161 1175 }
1162 1176
1163 1177 .branding {
1164 1178 float: left;
1165 1179 padding: 9px 2px;
1166 1180 line-height: 1em;
1167 1181 font-size: @navigation-fontsize;
1168 1182
1169 1183 a {
1170 1184 color: @grey5
1171 1185 }
1172 @media screen and (max-width: 1200px) {
1186
1187 // 1024px or smaller
1188 @media screen and (max-width: 1180px) {
1173 1189 display: none;
1174 1190 }
1191
1175 1192 }
1176 1193
1177 1194 img {
1178 1195 border: none;
1179 1196 outline: none;
1180 1197 }
1181 1198 user-profile-header
1182 1199 label {
1183 1200
1184 1201 input[type="checkbox"] {
1185 1202 margin-right: 1em;
1186 1203 }
1187 1204 input[type="radio"] {
1188 1205 margin-right: 1em;
1189 1206 }
1190 1207 }
1191 1208
1192 1209 .review-status {
1193 1210 &.under_review {
1194 1211 color: @alert3;
1195 1212 }
1196 1213 &.approved {
1197 1214 color: @alert1;
1198 1215 }
1199 1216 &.rejected,
1200 1217 &.forced_closed{
1201 1218 color: @alert2;
1202 1219 }
1203 1220 &.not_reviewed {
1204 1221 color: @grey5;
1205 1222 }
1206 1223 }
1207 1224
1208 1225 .review-status-under_review {
1209 1226 color: @alert3;
1210 1227 }
1211 1228 .status-tag-under_review {
1212 1229 border-color: @alert3;
1213 1230 }
1214 1231
1215 1232 .review-status-approved {
1216 1233 color: @alert1;
1217 1234 }
1218 1235 .status-tag-approved {
1219 1236 border-color: @alert1;
1220 1237 }
1221 1238
1222 1239 .review-status-rejected,
1223 1240 .review-status-forced_closed {
1224 1241 color: @alert2;
1225 1242 }
1226 1243 .status-tag-rejected,
1227 1244 .status-tag-forced_closed {
1228 1245 border-color: @alert2;
1229 1246 }
1230 1247
1231 1248 .review-status-not_reviewed {
1232 1249 color: @grey5;
1233 1250 }
1234 1251 .status-tag-not_reviewed {
1235 1252 border-color: @grey5;
1236 1253 }
1237 1254
1238 1255 .test_pattern_preview {
1239 1256 margin: @space 0;
1240 1257
1241 1258 p {
1242 1259 margin-bottom: 0;
1243 1260 border-bottom: @border-thickness solid @border-default-color;
1244 1261 color: @grey3;
1245 1262 }
1246 1263
1247 1264 .btn {
1248 1265 margin-bottom: @padding;
1249 1266 }
1250 1267 }
1251 1268 #test_pattern_result {
1252 1269 display: none;
1253 1270 &:extend(pre);
1254 1271 padding: .9em;
1255 1272 color: @grey3;
1256 1273 background-color: @grey7;
1257 1274 border-right: @border-thickness solid @border-default-color;
1258 1275 border-bottom: @border-thickness solid @border-default-color;
1259 1276 border-left: @border-thickness solid @border-default-color;
1260 1277 }
1261 1278
1262 1279 #repo_vcs_settings {
1263 1280 #inherit_overlay_vcs_default {
1264 1281 display: none;
1265 1282 }
1266 1283 #inherit_overlay_vcs_custom {
1267 1284 display: custom;
1268 1285 }
1269 1286 &.inherited {
1270 1287 #inherit_overlay_vcs_default {
1271 1288 display: block;
1272 1289 }
1273 1290 #inherit_overlay_vcs_custom {
1274 1291 display: none;
1275 1292 }
1276 1293 }
1277 1294 }
1278 1295
1279 1296 .issue-tracker-link {
1280 1297 color: @rcblue;
1281 1298 }
1282 1299
1283 1300 // Issue Tracker Table Show/Hide
1284 1301 #repo_issue_tracker {
1285 1302 #inherit_overlay {
1286 1303 display: none;
1287 1304 }
1288 1305 #custom_overlay {
1289 1306 display: custom;
1290 1307 }
1291 1308 &.inherited {
1292 1309 #inherit_overlay {
1293 1310 display: block;
1294 1311 }
1295 1312 #custom_overlay {
1296 1313 display: none;
1297 1314 }
1298 1315 }
1299 1316 }
1300 1317 table.issuetracker {
1301 1318 &.readonly {
1302 1319 tr, td {
1303 1320 color: @grey3;
1304 1321 }
1305 1322 }
1306 1323 .edit {
1307 1324 display: none;
1308 1325 }
1309 1326 .editopen {
1310 1327 .edit {
1311 1328 display: inline;
1312 1329 }
1313 1330 .entry {
1314 1331 display: none;
1315 1332 }
1316 1333 }
1317 1334 tr td.td-action {
1318 1335 min-width: 117px;
1319 1336 }
1320 1337 td input {
1321 1338 max-width: none;
1322 1339 min-width: 30px;
1323 1340 width: 80%;
1324 1341 }
1325 1342 .issuetracker_pref input {
1326 1343 width: 40%;
1327 1344 }
1328 1345 input.edit_issuetracker_update {
1329 1346 margin-right: 0;
1330 1347 width: auto;
1331 1348 }
1332 1349 }
1333 1350
1334 1351 table.integrations {
1335 1352 .td-icon {
1336 1353 width: 20px;
1337 1354 .integration-icon {
1338 1355 height: 20px;
1339 1356 width: 20px;
1340 1357 }
1341 1358 }
1342 1359 }
1343 1360
1344 1361 .integrations {
1345 1362 a.integration-box {
1346 1363 color: @text-color;
1347 1364 &:hover {
1348 1365 .panel {
1349 1366 background: #fbfbfb;
1350 1367 }
1351 1368 }
1352 1369 .integration-icon {
1353 1370 width: 30px;
1354 1371 height: 30px;
1355 1372 margin-right: 20px;
1356 1373 float: left;
1357 1374 }
1358 1375
1359 1376 .panel-body {
1360 1377 padding: 10px;
1361 1378 }
1362 1379 .panel {
1363 1380 margin-bottom: 10px;
1364 1381 }
1365 1382 h2 {
1366 1383 display: inline-block;
1367 1384 margin: 0;
1368 1385 min-width: 140px;
1369 1386 }
1370 1387 }
1371 1388 a.integration-box.dummy-integration {
1372 1389 color: @grey4
1373 1390 }
1374 1391 }
1375 1392
1376 1393 //Permissions Settings
1377 1394 #add_perm {
1378 1395 margin: 0 0 @padding;
1379 1396 cursor: pointer;
1380 1397 }
1381 1398
1382 1399 .perm_ac {
1383 1400 input {
1384 1401 width: 95%;
1385 1402 }
1386 1403 }
1387 1404
1388 1405 .autocomplete-suggestions {
1389 1406 width: auto !important; // overrides autocomplete.js
1390 1407 min-width: 278px;
1391 1408 margin: 0;
1392 1409 border: @border-thickness solid @grey5;
1393 1410 border-radius: @border-radius;
1394 1411 color: @grey2;
1395 1412 background-color: white;
1396 1413 }
1397 1414
1398 1415 .autocomplete-qfilter-suggestions {
1399 1416 width: auto !important; // overrides autocomplete.js
1400 1417 max-height: 100% !important;
1401 1418 min-width: 376px;
1402 1419 margin: 0;
1403 1420 border: @border-thickness solid @grey5;
1404 1421 color: @grey2;
1405 1422 background-color: white;
1406 1423 }
1407 1424
1408 1425 .autocomplete-selected {
1409 1426 background: #F0F0F0;
1410 1427 }
1411 1428
1412 1429 .ac-container-wrap {
1413 1430 margin: 0;
1414 1431 padding: 8px;
1415 1432 border-bottom: @border-thickness solid @grey5;
1416 1433 list-style-type: none;
1417 1434 cursor: pointer;
1418 1435
1419 1436 &:hover {
1420 1437 background-color: @grey7;
1421 1438 }
1422 1439
1423 1440 img {
1424 1441 height: @gravatar-size;
1425 1442 width: @gravatar-size;
1426 1443 margin-right: 1em;
1427 1444 }
1428 1445
1429 1446 strong {
1430 1447 font-weight: normal;
1431 1448 }
1432 1449 }
1433 1450
1434 1451 // Settings Dropdown
1435 1452 .user-menu .container {
1436 1453 padding: 0 4px;
1437 1454 margin: 0;
1438 1455 }
1439 1456
1440 1457 .user-menu .gravatar {
1441 1458 cursor: pointer;
1442 1459 }
1443 1460
1444 1461 .codeblock {
1445 1462 margin-bottom: @padding;
1446 1463 clear: both;
1447 1464
1448 1465 .stats {
1449 1466 overflow: hidden;
1450 1467 }
1451 1468
1452 1469 .message{
1453 1470 textarea{
1454 1471 margin: 0;
1455 1472 }
1456 1473 }
1457 1474
1458 1475 .code-header {
1459 1476 .stats {
1460 1477 line-height: 2em;
1461 1478
1462 1479 .revision_id {
1463 1480 margin-left: 0;
1464 1481 }
1465 1482 .buttons {
1466 1483 padding-right: 0;
1467 1484 }
1468 1485 }
1469 1486
1470 1487 .item{
1471 1488 margin-right: 0.5em;
1472 1489 }
1473 1490 }
1474 1491
1475 1492 #editor_container {
1476 1493 position: relative;
1477 1494 margin: @padding 10px;
1478 1495 }
1479 1496 }
1480 1497
1481 1498 #file_history_container {
1482 1499 display: none;
1483 1500 }
1484 1501
1485 1502 .file-history-inner {
1486 1503 margin-bottom: 10px;
1487 1504 }
1488 1505
1489 1506 // Pull Requests
1490 1507 .summary-details {
1491 1508 width: 100%;
1492 1509 }
1493 1510 .pr-summary {
1494 1511 border-bottom: @border-thickness solid @grey5;
1495 1512 margin-bottom: @space;
1496 1513 }
1497 1514
1498 1515 .reviewers {
1499 1516 width: 98%;
1500 1517 }
1501 1518
1502 1519 .reviewers ul li {
1503 1520 position: relative;
1504 1521 width: 100%;
1505 1522 padding-bottom: 8px;
1506 1523 list-style-type: none;
1507 1524 }
1508 1525
1509 1526 .reviewer_entry {
1510 1527 min-height: 55px;
1511 1528 }
1512 1529
1513 1530 .reviewer_reason {
1514 1531 padding-left: 20px;
1515 1532 line-height: 1.5em;
1516 1533 }
1517 1534 .reviewer_status {
1518 1535 display: inline-block;
1519 1536 width: 20px;
1520 1537 min-width: 20px;
1521 1538 height: 1.2em;
1522 1539 line-height: 1em;
1523 1540 }
1524 1541
1525 1542 .reviewer_name {
1526 1543 display: inline-block;
1527 1544 max-width: 83%;
1528 1545 padding-right: 20px;
1529 1546 vertical-align: middle;
1530 1547 line-height: 1;
1531 1548
1532 1549 .rc-user {
1533 1550 min-width: 0;
1534 1551 margin: -2px 1em 0 0;
1535 1552 }
1536 1553
1537 1554 .reviewer {
1538 1555 float: left;
1539 1556 }
1540 1557 }
1541 1558
1542 1559 .reviewer_member_mandatory {
1543 1560 width: 16px;
1544 1561 font-size: 11px;
1545 1562 margin: 0;
1546 1563 padding: 0;
1547 1564 color: black;
1548 1565 opacity: 0.4;
1549 1566 }
1550 1567
1551 1568 .reviewer_member_mandatory_remove,
1552 1569 .reviewer_member_remove {
1553 1570 width: 16px;
1554 1571 padding: 0;
1555 1572 color: black;
1573 cursor: pointer;
1556 1574 }
1557 1575
1558 1576 .reviewer_member_mandatory_remove {
1559 1577 color: @grey4;
1560 1578 }
1561 1579
1562 1580 .reviewer_member_status {
1563 1581 margin-top: 5px;
1564 1582 }
1565 1583 .pr-summary #summary{
1566 1584 width: 100%;
1567 1585 }
1568 1586 .pr-summary .action_button:hover {
1569 1587 border: 0;
1570 1588 cursor: pointer;
1571 1589 }
1572 1590 .pr-details-title {
1573 1591 height: 20px;
1574 1592 line-height: 20px;
1575 1593
1576 1594 padding-bottom: 8px;
1577 1595 border-bottom: @border-thickness solid @grey5;
1578 1596
1579 1597 .action_button.disabled {
1580 1598 color: @grey4;
1581 1599 cursor: inherit;
1582 1600 }
1583 1601 .action_button {
1584 1602 color: @rcblue;
1585 1603 }
1586 1604 }
1587 1605 .pr-details-content {
1588 1606 margin-top: @textmargin - 5;
1589 1607 margin-bottom: @textmargin - 5;
1590 1608 }
1591 1609
1592 1610 .pr-reviewer-rules {
1593 1611 padding: 10px 0px 20px 0px;
1594 1612 }
1595 1613
1596 1614 .todo-resolved {
1597 1615 text-decoration: line-through;
1598 1616 }
1599 1617
1600 1618 .todo-table, .comments-table {
1601 1619 width: 100%;
1602 1620
1603 1621 td {
1604 1622 padding: 5px 0px;
1605 1623 }
1606 1624
1607 1625 .td-todo-number {
1608 1626 text-align: left;
1609 1627 white-space: nowrap;
1610 1628 width: 1%;
1611 1629 padding-right: 2px;
1612 1630 }
1613 1631
1614 1632 .td-todo-gravatar {
1615 1633 width: 5%;
1616 1634
1617 1635 img {
1618 1636 margin: -3px 0;
1619 1637 }
1620 1638 }
1621 1639
1622 1640 }
1623 1641
1624 1642 .todo-comment-text-wrapper {
1625 1643 display: inline-grid;
1626 1644 }
1627 1645
1628 1646 .todo-comment-text {
1629 1647 margin-left: 5px;
1630 1648 white-space: nowrap;
1631 1649 overflow: hidden;
1632 1650 text-overflow: ellipsis;
1633 1651 }
1634 1652
1635 1653 table.group_members {
1636 1654 width: 100%
1637 1655 }
1638 1656
1639 1657 .group_members {
1640 1658 margin-top: 0;
1641 1659 padding: 0;
1642 1660
1643 1661 img {
1644 1662 height: @gravatar-size;
1645 1663 width: @gravatar-size;
1646 1664 margin-right: .5em;
1647 1665 margin-left: 3px;
1648 1666 }
1649 1667
1650 1668 .to-delete {
1651 1669 .user {
1652 1670 text-decoration: line-through;
1653 1671 }
1654 1672 }
1655 1673 }
1656 1674
1657 1675 .compare_view_commits_title {
1658 1676 .disabled {
1659 1677 cursor: inherit;
1660 1678 &:hover{
1661 1679 background-color: inherit;
1662 1680 color: inherit;
1663 1681 }
1664 1682 }
1665 1683 }
1666 1684
1667 1685 .subtitle-compare {
1668 1686 margin: -15px 0px 0px 0px;
1669 1687 }
1670 1688
1671 1689 // new entry in group_members
1672 1690 .td-author-new-entry {
1673 1691 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1674 1692 }
1675 1693
1676 1694 .usergroup_member_remove {
1677 1695 width: 16px;
1678 1696 margin-bottom: 10px;
1679 1697 padding: 0;
1680 1698 color: black !important;
1681 1699 cursor: pointer;
1682 1700 }
1683 1701
1684 1702 .reviewer_ac .ac-input {
1685 width: 92%;
1703 width: 100%;
1686 1704 margin-bottom: 1em;
1687 1705 }
1688 1706
1689 1707 .compare_view_commits tr{
1690 1708 height: 20px;
1691 1709 }
1692 1710 .compare_view_commits td {
1693 1711 vertical-align: top;
1694 1712 padding-top: 10px;
1695 1713 }
1696 1714 .compare_view_commits .author {
1697 1715 margin-left: 5px;
1698 1716 }
1699 1717
1700 1718 .compare_view_commits {
1701 1719 .color-a {
1702 1720 color: @alert1;
1703 1721 }
1704 1722
1705 1723 .color-c {
1706 1724 color: @color3;
1707 1725 }
1708 1726
1709 1727 .color-r {
1710 1728 color: @color5;
1711 1729 }
1712 1730
1713 1731 .color-a-bg {
1714 1732 background-color: @alert1;
1715 1733 }
1716 1734
1717 1735 .color-c-bg {
1718 1736 background-color: @alert3;
1719 1737 }
1720 1738
1721 1739 .color-r-bg {
1722 1740 background-color: @alert2;
1723 1741 }
1724 1742
1725 1743 .color-a-border {
1726 1744 border: 1px solid @alert1;
1727 1745 }
1728 1746
1729 1747 .color-c-border {
1730 1748 border: 1px solid @alert3;
1731 1749 }
1732 1750
1733 1751 .color-r-border {
1734 1752 border: 1px solid @alert2;
1735 1753 }
1736 1754
1737 1755 .commit-change-indicator {
1738 1756 width: 15px;
1739 1757 height: 15px;
1740 1758 position: relative;
1741 1759 left: 15px;
1742 1760 }
1743 1761
1744 1762 .commit-change-content {
1745 1763 text-align: center;
1746 1764 vertical-align: middle;
1747 1765 line-height: 15px;
1748 1766 }
1749 1767 }
1750 1768
1751 1769 .compare_view_filepath {
1752 1770 color: @grey1;
1753 1771 }
1754 1772
1755 1773 .show_more {
1756 1774 display: inline-block;
1757 1775 width: 0;
1758 1776 height: 0;
1759 1777 vertical-align: middle;
1760 1778 content: "";
1761 1779 border: 4px solid;
1762 1780 border-right-color: transparent;
1763 1781 border-bottom-color: transparent;
1764 1782 border-left-color: transparent;
1765 1783 font-size: 0;
1766 1784 }
1767 1785
1768 1786 .journal_more .show_more {
1769 1787 display: inline;
1770 1788
1771 1789 &:after {
1772 1790 content: none;
1773 1791 }
1774 1792 }
1775 1793
1776 1794 .compare_view_commits .collapse_commit:after {
1777 1795 cursor: pointer;
1778 1796 content: "\00A0\25B4";
1779 1797 margin-left: -3px;
1780 1798 font-size: 17px;
1781 1799 color: @grey4;
1782 1800 }
1783 1801
1784 1802 .diff_links {
1785 1803 margin-left: 8px;
1786 1804 }
1787 1805
1788 1806 #pull_request_overview {
1789 1807 div.ancestor {
1790 1808 margin: -33px 0;
1791 1809 }
1792 1810 }
1793 1811
1794 1812 div.ancestor {
1795 1813
1796 1814 }
1797 1815
1798 1816 .cs_icon_td input[type="checkbox"] {
1799 1817 display: none;
1800 1818 }
1801 1819
1802 1820 .cs_icon_td .expand_file_icon:after {
1803 1821 cursor: pointer;
1804 1822 content: "\00A0\25B6";
1805 1823 font-size: 12px;
1806 1824 color: @grey4;
1807 1825 }
1808 1826
1809 1827 .cs_icon_td .collapse_file_icon:after {
1810 1828 cursor: pointer;
1811 1829 content: "\00A0\25BC";
1812 1830 font-size: 12px;
1813 1831 color: @grey4;
1814 1832 }
1815 1833
1816 1834 /*new binary
1817 1835 NEW_FILENODE = 1
1818 1836 DEL_FILENODE = 2
1819 1837 MOD_FILENODE = 3
1820 1838 RENAMED_FILENODE = 4
1821 1839 COPIED_FILENODE = 5
1822 1840 CHMOD_FILENODE = 6
1823 1841 BIN_FILENODE = 7
1824 1842 */
1825 1843 .cs_files_expand {
1826 1844 font-size: @basefontsize + 5px;
1827 1845 line-height: 1.8em;
1828 1846 float: right;
1829 1847 }
1830 1848
1831 1849 .cs_files_expand span{
1832 1850 color: @rcblue;
1833 1851 cursor: pointer;
1834 1852 }
1835 1853 .cs_files {
1836 1854 clear: both;
1837 1855 padding-bottom: @padding;
1838 1856
1839 1857 .cur_cs {
1840 1858 margin: 10px 2px;
1841 1859 font-weight: bold;
1842 1860 }
1843 1861
1844 1862 .node {
1845 1863 float: left;
1846 1864 }
1847 1865
1848 1866 .changes {
1849 1867 float: right;
1850 1868 color: white;
1851 1869 font-size: @basefontsize - 4px;
1852 1870 margin-top: 4px;
1853 1871 opacity: 0.6;
1854 1872 filter: Alpha(opacity=60); /* IE8 and earlier */
1855 1873
1856 1874 .added {
1857 1875 background-color: @alert1;
1858 1876 float: left;
1859 1877 text-align: center;
1860 1878 }
1861 1879
1862 1880 .deleted {
1863 1881 background-color: @alert2;
1864 1882 float: left;
1865 1883 text-align: center;
1866 1884 }
1867 1885
1868 1886 .bin {
1869 1887 background-color: @alert1;
1870 1888 text-align: center;
1871 1889 }
1872 1890
1873 1891 /*new binary*/
1874 1892 .bin.bin1 {
1875 1893 background-color: @alert1;
1876 1894 text-align: center;
1877 1895 }
1878 1896
1879 1897 /*deleted binary*/
1880 1898 .bin.bin2 {
1881 1899 background-color: @alert2;
1882 1900 text-align: center;
1883 1901 }
1884 1902
1885 1903 /*mod binary*/
1886 1904 .bin.bin3 {
1887 1905 background-color: @grey2;
1888 1906 text-align: center;
1889 1907 }
1890 1908
1891 1909 /*rename file*/
1892 1910 .bin.bin4 {
1893 1911 background-color: @alert4;
1894 1912 text-align: center;
1895 1913 }
1896 1914
1897 1915 /*copied file*/
1898 1916 .bin.bin5 {
1899 1917 background-color: @alert4;
1900 1918 text-align: center;
1901 1919 }
1902 1920
1903 1921 /*chmod file*/
1904 1922 .bin.bin6 {
1905 1923 background-color: @grey2;
1906 1924 text-align: center;
1907 1925 }
1908 1926 }
1909 1927 }
1910 1928
1911 1929 .cs_files .cs_added, .cs_files .cs_A,
1912 1930 .cs_files .cs_added, .cs_files .cs_M,
1913 1931 .cs_files .cs_added, .cs_files .cs_D {
1914 1932 height: 16px;
1915 1933 padding-right: 10px;
1916 1934 margin-top: 7px;
1917 1935 text-align: left;
1918 1936 }
1919 1937
1920 1938 .cs_icon_td {
1921 1939 min-width: 16px;
1922 1940 width: 16px;
1923 1941 }
1924 1942
1925 1943 .pull-request-merge {
1926 1944 border: 1px solid @grey5;
1927 1945 padding: 10px 0px 20px;
1928 1946 margin-top: 10px;
1929 1947 margin-bottom: 20px;
1930 1948 }
1931 1949
1932 1950 .pull-request-merge-refresh {
1933 1951 margin: 2px 7px;
1934 1952 a {
1935 1953 color: @grey3;
1936 1954 }
1937 1955 }
1938 1956
1939 1957 .pull-request-merge ul {
1940 1958 padding: 0px 0px;
1941 1959 }
1942 1960
1943 1961 .pull-request-merge li {
1944 1962 list-style-type: none;
1945 1963 }
1946 1964
1947 1965 .pull-request-merge .pull-request-wrap {
1948 1966 height: auto;
1949 1967 padding: 0px 0px;
1950 1968 text-align: right;
1951 1969 }
1952 1970
1953 1971 .pull-request-merge span {
1954 1972 margin-right: 5px;
1955 1973 }
1956 1974
1957 1975 .pull-request-merge-actions {
1958 1976 min-height: 30px;
1959 1977 padding: 0px 0px;
1960 1978 }
1961 1979
1962 1980 .pull-request-merge-info {
1963 1981 padding: 0px 5px 5px 0px;
1964 1982 }
1965 1983
1966 1984 .merge-status {
1967 1985 margin-right: 5px;
1968 1986 }
1969 1987
1970 1988 .merge-message {
1971 1989 font-size: 1.2em
1972 1990 }
1973 1991
1974 1992 .merge-message.success i,
1975 1993 .merge-icon.success i {
1976 1994 color:@alert1;
1977 1995 }
1978 1996
1979 1997 .merge-message.warning i,
1980 1998 .merge-icon.warning i {
1981 1999 color: @alert3;
1982 2000 }
1983 2001
1984 2002 .merge-message.error i,
1985 2003 .merge-icon.error i {
1986 2004 color:@alert2;
1987 2005 }
1988 2006
1989 2007 .pr-versions {
1990 2008 font-size: 1.1em;
1991 2009 padding: 7.5px;
1992 2010
1993 2011 table {
1994 2012
1995 2013 }
1996 2014
1997 2015 td {
1998 2016 line-height: 15px;
1999 2017 }
2000 2018
2001 2019 .compare-radio-button {
2002 2020 position: relative;
2003 2021 top: -3px;
2004 2022 }
2005 2023 }
2006 2024
2007 2025
2008 2026 #close_pull_request {
2009 2027 margin-right: 0px;
2010 2028 }
2011 2029
2012 2030 .empty_data {
2013 2031 color: @grey4;
2014 2032 }
2015 2033
2016 2034 #changeset_compare_view_content {
2017 2035 clear: both;
2018 2036 width: 100%;
2019 2037 box-sizing: border-box;
2020 2038 .border-radius(@border-radius);
2021 2039
2022 2040 .help-block {
2023 2041 margin: @padding 0;
2024 2042 color: @text-color;
2025 2043 &.pre-formatting {
2026 2044 white-space: pre;
2027 2045 }
2028 2046 }
2029 2047
2030 2048 .empty_data {
2031 2049 margin: @padding 0;
2032 2050 }
2033 2051
2034 2052 .alert {
2035 2053 margin-bottom: @space;
2036 2054 }
2037 2055 }
2038 2056
2039 2057 .table_disp {
2040 2058 .status {
2041 2059 width: auto;
2042 2060 }
2043 2061 }
2044 2062
2045 2063
2046 2064 .creation_in_progress {
2047 2065 color: @grey4
2048 2066 }
2049 2067
2050 2068 .status_box_menu {
2051 2069 margin: 0;
2052 2070 }
2053 2071
2054 2072 .notification-table{
2055 2073 margin-bottom: @space;
2056 2074 display: table;
2057 2075 width: 100%;
2058 2076
2059 2077 .container{
2060 2078 display: table-row;
2061 2079
2062 2080 .notification-header{
2063 2081 border-bottom: @border-thickness solid @border-default-color;
2064 2082 }
2065 2083
2066 2084 .notification-subject{
2067 2085 display: table-cell;
2068 2086 }
2069 2087 }
2070 2088 }
2071 2089
2072 2090 // Notifications
2073 2091 .notification-header{
2074 2092 display: table;
2075 2093 width: 100%;
2076 2094 padding: floor(@basefontsize/2) 0;
2077 2095 line-height: 1em;
2078 2096
2079 2097 .desc, .delete-notifications, .read-notifications{
2080 2098 display: table-cell;
2081 2099 text-align: left;
2082 2100 }
2083 2101
2084 2102 .delete-notifications, .read-notifications{
2085 2103 width: 35px;
2086 2104 min-width: 35px; //fixes when only one button is displayed
2087 2105 }
2088 2106 }
2089 2107
2090 2108 .notification-body {
2091 2109 .markdown-block,
2092 2110 .rst-block {
2093 2111 padding: @padding 0;
2094 2112 }
2095 2113
2096 2114 .notification-subject {
2097 2115 padding: @textmargin 0;
2098 2116 border-bottom: @border-thickness solid @border-default-color;
2099 2117 }
2100 2118 }
2101 2119
2102 2120 .notice-messages {
2103 2121 .markdown-block,
2104 2122 .rst-block {
2105 2123 padding: 0;
2106 2124 }
2107 2125 }
2108 2126
2109 2127 .notifications_buttons{
2110 2128 float: right;
2111 2129 }
2112 2130
2113 2131 #notification-status{
2114 2132 display: inline;
2115 2133 }
2116 2134
2117 2135 // Repositories
2118 2136
2119 2137 #summary.fields{
2120 2138 display: table;
2121 2139
2122 2140 .field{
2123 2141 display: table-row;
2124 2142
2125 2143 .label-summary{
2126 2144 display: table-cell;
2127 2145 min-width: @label-summary-minwidth;
2128 2146 padding-top: @padding/2;
2129 2147 padding-bottom: @padding/2;
2130 2148 padding-right: @padding/2;
2131 2149 }
2132 2150
2133 2151 .input{
2134 2152 display: table-cell;
2135 2153 padding: @padding/2;
2136 2154
2137 2155 input{
2138 2156 min-width: 29em;
2139 2157 padding: @padding/4;
2140 2158 }
2141 2159 }
2142 2160 .statistics, .downloads{
2143 2161 .disabled{
2144 2162 color: @grey4;
2145 2163 }
2146 2164 }
2147 2165 }
2148 2166 }
2149 2167
2150 2168 #summary{
2151 2169 width: 70%;
2152 2170 }
2153 2171
2154 2172
2155 2173 // Journal
2156 2174 .journal.title {
2157 2175 h5 {
2158 2176 float: left;
2159 2177 margin: 0;
2160 2178 width: 70%;
2161 2179 }
2162 2180
2163 2181 ul {
2164 2182 float: right;
2165 2183 display: inline-block;
2166 2184 margin: 0;
2167 2185 width: 30%;
2168 2186 text-align: right;
2169 2187
2170 2188 li {
2171 2189 display: inline;
2172 2190 font-size: @journal-fontsize;
2173 2191 line-height: 1em;
2174 2192
2175 2193 list-style-type: none;
2176 2194 }
2177 2195 }
2178 2196 }
2179 2197
2180 2198 .filterexample {
2181 2199 position: absolute;
2182 2200 top: 95px;
2183 2201 left: @contentpadding;
2184 2202 color: @rcblue;
2185 2203 font-size: 11px;
2186 2204 font-family: @text-regular;
2187 2205 cursor: help;
2188 2206
2189 2207 &:hover {
2190 2208 color: @rcdarkblue;
2191 2209 }
2192 2210
2193 2211 @media (max-width:768px) {
2194 2212 position: relative;
2195 2213 top: auto;
2196 2214 left: auto;
2197 2215 display: block;
2198 2216 }
2199 2217 }
2200 2218
2201 2219
2202 2220 #journal{
2203 2221 margin-bottom: @space;
2204 2222
2205 2223 .journal_day{
2206 2224 margin-bottom: @textmargin/2;
2207 2225 padding-bottom: @textmargin/2;
2208 2226 font-size: @journal-fontsize;
2209 2227 border-bottom: @border-thickness solid @border-default-color;
2210 2228 }
2211 2229
2212 2230 .journal_container{
2213 2231 margin-bottom: @space;
2214 2232
2215 2233 .journal_user{
2216 2234 display: inline-block;
2217 2235 }
2218 2236 .journal_action_container{
2219 2237 display: block;
2220 2238 margin-top: @textmargin;
2221 2239
2222 2240 div{
2223 2241 display: inline;
2224 2242 }
2225 2243
2226 2244 div.journal_action_params{
2227 2245 display: block;
2228 2246 }
2229 2247
2230 2248 div.journal_repo:after{
2231 2249 content: "\A";
2232 2250 white-space: pre;
2233 2251 }
2234 2252
2235 2253 div.date{
2236 2254 display: block;
2237 2255 margin-bottom: @textmargin;
2238 2256 }
2239 2257 }
2240 2258 }
2241 2259 }
2242 2260
2243 2261 // Files
2244 2262 .edit-file-title {
2245 2263 font-size: 16px;
2246 2264
2247 2265 .title-heading {
2248 2266 padding: 2px;
2249 2267 }
2250 2268 }
2251 2269
2252 2270 .edit-file-fieldset {
2253 2271 margin: @sidebarpadding 0;
2254 2272
2255 2273 .fieldset {
2256 2274 .left-label {
2257 2275 width: 13%;
2258 2276 }
2259 2277 .right-content {
2260 2278 width: 87%;
2261 2279 max-width: 100%;
2262 2280 }
2263 2281 .filename-label {
2264 2282 margin-top: 13px;
2265 2283 }
2266 2284 .commit-message-label {
2267 2285 margin-top: 4px;
2268 2286 }
2269 2287 .file-upload-input {
2270 2288 input {
2271 2289 display: none;
2272 2290 }
2273 2291 margin-top: 10px;
2274 2292 }
2275 2293 .file-upload-label {
2276 2294 margin-top: 10px;
2277 2295 }
2278 2296 p {
2279 2297 margin-top: 5px;
2280 2298 }
2281 2299
2282 2300 }
2283 2301 .custom-path-link {
2284 2302 margin-left: 5px;
2285 2303 }
2286 2304 #commit {
2287 2305 resize: vertical;
2288 2306 }
2289 2307 }
2290 2308
2291 2309 .delete-file-preview {
2292 2310 max-height: 250px;
2293 2311 }
2294 2312
2295 2313 .new-file,
2296 2314 #filter_activate,
2297 2315 #filter_deactivate {
2298 2316 float: right;
2299 2317 margin: 0 0 0 10px;
2300 2318 }
2301 2319
2302 2320 .file-upload-transaction-wrapper {
2303 2321 margin-top: 57px;
2304 2322 clear: both;
2305 2323 }
2306 2324
2307 2325 .file-upload-transaction-wrapper .error {
2308 2326 color: @color5;
2309 2327 }
2310 2328
2311 2329 .file-upload-transaction {
2312 2330 min-height: 200px;
2313 2331 padding: 54px;
2314 2332 border: 1px solid @grey5;
2315 2333 text-align: center;
2316 2334 clear: both;
2317 2335 }
2318 2336
2319 2337 .file-upload-transaction i {
2320 2338 font-size: 48px
2321 2339 }
2322 2340
2323 2341 h3.files_location{
2324 2342 line-height: 2.4em;
2325 2343 }
2326 2344
2327 2345 .browser-nav {
2328 2346 width: 100%;
2329 2347 display: table;
2330 2348 margin-bottom: 20px;
2331 2349
2332 2350 .info_box {
2333 2351 float: left;
2334 2352 display: inline-table;
2335 2353 height: 2.5em;
2336 2354
2337 2355 .browser-cur-rev, .info_box_elem {
2338 2356 display: table-cell;
2339 2357 vertical-align: middle;
2340 2358 }
2341 2359
2342 2360 .drop-menu {
2343 2361 margin: 0 10px;
2344 2362 }
2345 2363
2346 2364 .info_box_elem {
2347 2365 border-top: @border-thickness solid @grey5;
2348 2366 border-bottom: @border-thickness solid @grey5;
2349 2367 box-shadow: @button-shadow;
2350 2368
2351 2369 #at_rev, a {
2352 2370 padding: 0.6em 0.4em;
2353 2371 margin: 0;
2354 2372 .box-shadow(none);
2355 2373 border: 0;
2356 2374 height: 12px;
2357 2375 color: @grey2;
2358 2376 }
2359 2377
2360 2378 input#at_rev {
2361 2379 max-width: 50px;
2362 2380 text-align: center;
2363 2381 }
2364 2382
2365 2383 &.previous {
2366 2384 border: @border-thickness solid @grey5;
2367 2385 border-top-left-radius: @border-radius;
2368 2386 border-bottom-left-radius: @border-radius;
2369 2387
2370 2388 &:hover {
2371 2389 border-color: @grey4;
2372 2390 }
2373 2391
2374 2392 .disabled {
2375 2393 color: @grey5;
2376 2394 cursor: not-allowed;
2377 2395 opacity: 0.5;
2378 2396 }
2379 2397 }
2380 2398
2381 2399 &.next {
2382 2400 border: @border-thickness solid @grey5;
2383 2401 border-top-right-radius: @border-radius;
2384 2402 border-bottom-right-radius: @border-radius;
2385 2403
2386 2404 &:hover {
2387 2405 border-color: @grey4;
2388 2406 }
2389 2407
2390 2408 .disabled {
2391 2409 color: @grey5;
2392 2410 cursor: not-allowed;
2393 2411 opacity: 0.5;
2394 2412 }
2395 2413 }
2396 2414 }
2397 2415
2398 2416 .browser-cur-rev {
2399 2417
2400 2418 span{
2401 2419 margin: 0;
2402 2420 color: @rcblue;
2403 2421 height: 12px;
2404 2422 display: inline-block;
2405 2423 padding: 0.7em 1em ;
2406 2424 border: @border-thickness solid @rcblue;
2407 2425 margin-right: @padding;
2408 2426 }
2409 2427 }
2410 2428
2411 2429 }
2412 2430
2413 2431 .select-index-number {
2414 2432 margin: 0 0 0 20px;
2415 2433 color: @grey3;
2416 2434 }
2417 2435
2418 2436 .search_activate {
2419 2437 display: table-cell;
2420 2438 vertical-align: middle;
2421 2439
2422 2440 input, label{
2423 2441 margin: 0;
2424 2442 padding: 0;
2425 2443 }
2426 2444
2427 2445 input{
2428 2446 margin-left: @textmargin;
2429 2447 }
2430 2448
2431 2449 }
2432 2450 }
2433 2451
2434 2452 .browser-cur-rev{
2435 2453 margin-bottom: @textmargin;
2436 2454 }
2437 2455
2438 2456 #node_filter_box_loading{
2439 2457 .info_text;
2440 2458 }
2441 2459
2442 2460 .browser-search {
2443 2461 margin: -25px 0px 5px 0px;
2444 2462 }
2445 2463
2446 2464 .files-quick-filter {
2447 2465 float: right;
2448 2466 width: 180px;
2449 2467 position: relative;
2450 2468 }
2451 2469
2452 2470 .files-filter-box {
2453 2471 display: flex;
2454 2472 padding: 0px;
2455 2473 border-radius: 3px;
2456 2474 margin-bottom: 0;
2457 2475
2458 2476 a {
2459 2477 border: none !important;
2460 2478 }
2461 2479
2462 2480 li {
2463 2481 list-style-type: none
2464 2482 }
2465 2483 }
2466 2484
2467 2485 .files-filter-box-path {
2468 2486 line-height: 33px;
2469 2487 padding: 0;
2470 2488 width: 20px;
2471 2489 position: absolute;
2472 2490 z-index: 11;
2473 2491 left: 5px;
2474 2492 }
2475 2493
2476 2494 .files-filter-box-input {
2477 2495 margin-right: 0;
2478 2496
2479 2497 input {
2480 2498 border: 1px solid @white;
2481 2499 padding-left: 25px;
2482 2500 width: 145px;
2483 2501
2484 2502 &:hover {
2485 2503 border-color: @grey6;
2486 2504 }
2487 2505
2488 2506 &:focus {
2489 2507 border-color: @grey5;
2490 2508 }
2491 2509 }
2492 2510 }
2493 2511
2494 2512 .browser-result{
2495 2513 td a{
2496 2514 margin-left: 0.5em;
2497 2515 display: inline-block;
2498 2516
2499 2517 em {
2500 2518 font-weight: @text-bold-weight;
2501 2519 font-family: @text-bold;
2502 2520 }
2503 2521 }
2504 2522 }
2505 2523
2506 2524 .browser-highlight{
2507 2525 background-color: @grey5-alpha;
2508 2526 }
2509 2527
2510 2528
2511 2529 .edit-file-fieldset #location,
2512 2530 .edit-file-fieldset #filename {
2513 2531 display: flex;
2514 2532 width: -moz-available; /* WebKit-based browsers will ignore this. */
2515 2533 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2516 2534 width: fill-available;
2517 2535 border: 0;
2518 2536 }
2519 2537
2520 2538 .path-items {
2521 2539 display: flex;
2522 2540 padding: 0;
2523 2541 border: 1px solid #eeeeee;
2524 2542 width: 100%;
2525 2543 float: left;
2526 2544
2527 2545 .breadcrumb-path {
2528 2546 line-height: 30px;
2529 2547 padding: 0 4px;
2530 2548 white-space: nowrap;
2531 2549 }
2532 2550
2533 2551 .upload-form {
2534 2552 margin-top: 46px;
2535 2553 }
2536 2554
2537 2555 .location-path {
2538 2556 width: -moz-available; /* WebKit-based browsers will ignore this. */
2539 2557 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2540 2558 width: fill-available;
2541 2559
2542 2560 .file-name-input {
2543 2561 padding: 0.5em 0;
2544 2562 }
2545 2563
2546 2564 }
2547 2565
2548 2566 ul {
2549 2567 display: flex;
2550 2568 margin: 0;
2551 2569 padding: 0;
2552 2570 width: 100%;
2553 2571 }
2554 2572
2555 2573 li {
2556 2574 list-style-type: none;
2557 2575 }
2558 2576
2559 2577 }
2560 2578
2561 2579 .editor-items {
2562 2580 height: 40px;
2563 2581 margin: 10px 0 -17px 10px;
2564 2582
2565 2583 .editor-action {
2566 2584 cursor: pointer;
2567 2585 }
2568 2586
2569 2587 .editor-action.active {
2570 2588 border-bottom: 2px solid #5C5C5C;
2571 2589 }
2572 2590
2573 2591 li {
2574 2592 list-style-type: none;
2575 2593 }
2576 2594 }
2577 2595
2578 2596 .edit-file-fieldset .message textarea {
2579 2597 border: 1px solid #eeeeee;
2580 2598 }
2581 2599
2582 2600 #files_data .codeblock {
2583 2601 background-color: #F5F5F5;
2584 2602 }
2585 2603
2586 2604 #editor_preview {
2587 2605 background: white;
2588 2606 }
2589 2607
2590 2608 .show-editor {
2591 2609 padding: 10px;
2592 2610 background-color: white;
2593 2611
2594 2612 }
2595 2613
2596 2614 .show-preview {
2597 2615 padding: 10px;
2598 2616 background-color: white;
2599 2617 border-left: 1px solid #eeeeee;
2600 2618 }
2601 2619 // quick filter
2602 2620 .grid-quick-filter {
2603 2621 float: right;
2604 2622 position: relative;
2605 2623 }
2606 2624
2607 2625 .grid-filter-box {
2608 2626 display: flex;
2609 2627 padding: 0px;
2610 2628 border-radius: 3px;
2611 2629 margin-bottom: 0;
2612 2630
2613 2631 a {
2614 2632 border: none !important;
2615 2633 }
2616 2634
2617 2635 li {
2618 2636 list-style-type: none
2619 2637 }
2620 2638 }
2621 2639
2622 2640 .grid-filter-box-icon {
2623 2641 line-height: 33px;
2624 2642 padding: 0;
2625 2643 width: 20px;
2626 2644 position: absolute;
2627 2645 z-index: 11;
2628 2646 left: 5px;
2629 2647 }
2630 2648
2631 2649 .grid-filter-box-input {
2632 2650 margin-right: 0;
2633 2651
2634 2652 input {
2635 2653 border: 1px solid @white;
2636 2654 padding-left: 25px;
2637 2655 width: 145px;
2638 2656
2639 2657 &:hover {
2640 2658 border-color: @grey6;
2641 2659 }
2642 2660
2643 2661 &:focus {
2644 2662 border-color: @grey5;
2645 2663 }
2646 2664 }
2647 2665 }
2648 2666
2649 2667
2650 2668
2651 2669 // Search
2652 2670
2653 2671 .search-form{
2654 2672 #q {
2655 2673 width: @search-form-width;
2656 2674 }
2657 2675 .fields{
2658 2676 margin: 0 0 @space;
2659 2677 }
2660 2678
2661 2679 label{
2662 2680 display: inline-block;
2663 2681 margin-right: @textmargin;
2664 2682 padding-top: 0.25em;
2665 2683 }
2666 2684
2667 2685
2668 2686 .results{
2669 2687 clear: both;
2670 2688 margin: 0 0 @padding;
2671 2689 }
2672 2690
2673 2691 .search-tags {
2674 2692 padding: 5px 0;
2675 2693 }
2676 2694 }
2677 2695
2678 2696 div.search-feedback-items {
2679 2697 display: inline-block;
2680 2698 }
2681 2699
2682 2700 div.search-code-body {
2683 2701 background-color: #ffffff; padding: 5px 0 5px 10px;
2684 2702 pre {
2685 2703 .match { background-color: #faffa6;}
2686 2704 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2687 2705 }
2688 2706 }
2689 2707
2690 2708 .expand_commit.search {
2691 2709 .show_more.open {
2692 2710 height: auto;
2693 2711 max-height: none;
2694 2712 }
2695 2713 }
2696 2714
2697 2715 .search-results {
2698 2716
2699 2717 h2 {
2700 2718 margin-bottom: 0;
2701 2719 }
2702 2720 .codeblock {
2703 2721 border: none;
2704 2722 background: transparent;
2705 2723 }
2706 2724
2707 2725 .codeblock-header {
2708 2726 border: none;
2709 2727 background: transparent;
2710 2728 }
2711 2729
2712 2730 .code-body {
2713 2731 border: @border-thickness solid @grey6;
2714 2732 .border-radius(@border-radius);
2715 2733 }
2716 2734
2717 2735 .td-commit {
2718 2736 &:extend(pre);
2719 2737 border-bottom: @border-thickness solid @border-default-color;
2720 2738 }
2721 2739
2722 2740 .message {
2723 2741 height: auto;
2724 2742 max-width: 350px;
2725 2743 white-space: normal;
2726 2744 text-overflow: initial;
2727 2745 overflow: visible;
2728 2746
2729 2747 .match { background-color: #faffa6;}
2730 2748 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2731 2749 }
2732 2750
2733 2751 .path {
2734 2752 border-bottom: none !important;
2735 2753 border-left: 1px solid @grey6 !important;
2736 2754 border-right: 1px solid @grey6 !important;
2737 2755 }
2738 2756 }
2739 2757
2740 2758 table.rctable td.td-search-results div {
2741 2759 max-width: 100%;
2742 2760 }
2743 2761
2744 2762 #tip-box, .tip-box{
2745 2763 padding: @menupadding/2;
2746 2764 display: block;
2747 2765 border: @border-thickness solid @border-highlight-color;
2748 2766 .border-radius(@border-radius);
2749 2767 background-color: white;
2750 2768 z-index: 99;
2751 2769 white-space: pre-wrap;
2752 2770 }
2753 2771
2754 2772 #linktt {
2755 2773 width: 79px;
2756 2774 }
2757 2775
2758 2776 #help_kb .modal-content{
2759 max-width: 750px;
2777 max-width: 800px;
2760 2778 margin: 10% auto;
2761 2779
2762 2780 table{
2763 2781 td,th{
2764 2782 border-bottom: none;
2765 2783 line-height: 2.5em;
2766 2784 }
2767 2785 th{
2768 2786 padding-bottom: @textmargin/2;
2769 2787 }
2770 2788 td.keys{
2771 2789 text-align: center;
2772 2790 }
2773 2791 }
2774 2792
2775 2793 .block-left{
2776 2794 width: 45%;
2777 2795 margin-right: 5%;
2778 2796 }
2779 2797 .modal-footer{
2780 2798 clear: both;
2781 2799 }
2782 2800 .key.tag{
2783 2801 padding: 0.5em;
2784 2802 background-color: @rcblue;
2785 2803 color: white;
2786 2804 border-color: @rcblue;
2787 2805 .box-shadow(none);
2788 2806 }
2789 2807 }
2790 2808
2791 2809
2792 2810
2793 2811 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2794 2812
2795 2813 @import 'statistics-graph';
2796 2814 @import 'tables';
2797 2815 @import 'forms';
2798 2816 @import 'diff';
2799 2817 @import 'summary';
2800 2818 @import 'navigation';
2801 2819
2802 2820 //--- SHOW/HIDE SECTIONS --//
2803 2821
2804 2822 .btn-collapse {
2805 2823 float: right;
2806 2824 text-align: right;
2807 2825 font-family: @text-light;
2808 2826 font-size: @basefontsize;
2809 2827 cursor: pointer;
2810 2828 border: none;
2811 2829 color: @rcblue;
2812 2830 }
2813 2831
2814 2832 table.rctable,
2815 2833 table.dataTable {
2816 2834 .btn-collapse {
2817 2835 float: right;
2818 2836 text-align: right;
2819 2837 }
2820 2838 }
2821 2839
2822 2840 table.rctable {
2823 2841 &.permissions {
2824 2842
2825 2843 th.td-owner {
2826 2844 padding: 0;
2827 2845 }
2828 2846
2829 2847 th {
2830 2848 font-weight: normal;
2831 2849 padding: 0 5px;
2832 2850 }
2833 2851
2834 2852 }
2835 2853 }
2836 2854
2837 2855
2838 2856 // TODO: johbo: Fix for IE10, this avoids that we see a border
2839 2857 // and padding around checkboxes and radio boxes. Move to the right place,
2840 2858 // or better: Remove this once we did the form refactoring.
2841 2859 input[type=checkbox],
2842 2860 input[type=radio] {
2843 2861 padding: 0;
2844 2862 border: none;
2845 2863 }
2846 2864
2847 2865 .toggle-ajax-spinner{
2848 2866 height: 16px;
2849 2867 width: 16px;
2850 2868 }
2851 2869
2852 2870
2853 2871 .markup-form .clearfix {
2854 2872 .border-radius(@border-radius);
2855 2873 margin: 0px;
2856 2874 }
2857 2875
2858 2876 .markup-form-area {
2859 2877 padding: 8px 12px;
2860 2878 border: 1px solid @grey4;
2861 2879 .border-radius(@border-radius);
2862 2880 }
2863 2881
2864 2882 .markup-form-area-header .nav-links {
2865 2883 display: flex;
2866 2884 flex-flow: row wrap;
2867 2885 -webkit-flex-flow: row wrap;
2868 2886 width: 100%;
2869 2887 }
2870 2888
2871 2889 .markup-form-area-footer {
2872 2890 display: flex;
2873 2891 }
2874 2892
2875 2893 .markup-form-area-footer .toolbar {
2876 2894
2877 2895 }
2878 2896
2879 2897 // markup Form
2880 2898 div.markup-form {
2881 2899 margin-top: 20px;
2882 2900 }
2883 2901
2884 2902 .markup-form strong {
2885 2903 display: block;
2886 2904 margin-bottom: 15px;
2887 2905 }
2888 2906
2889 2907 .markup-form textarea {
2890 2908 width: 100%;
2891 2909 height: 100px;
2892 2910 font-family: @text-monospace;
2893 2911 }
2894 2912
2895 2913 form.markup-form {
2896 2914 margin-top: 10px;
2897 2915 margin-left: 10px;
2898 2916 }
2899 2917
2900 2918 .markup-form .comment-block-ta,
2901 2919 .markup-form .preview-box {
2902 2920 .border-radius(@border-radius);
2903 2921 .box-sizing(border-box);
2904 2922 background-color: white;
2905 2923 }
2906 2924
2907 2925 .markup-form .preview-box.unloaded {
2908 2926 height: 50px;
2909 2927 text-align: center;
2910 2928 padding: 20px;
2911 2929 background-color: white;
2912 2930 }
2913 2931
2914 2932
2915 2933 .dropzone-wrapper {
2916 2934 border: 1px solid @grey5;
2917 2935 padding: 20px;
2918 2936 }
2919 2937
2920 2938 .dropzone,
2921 2939 .dropzone-pure {
2922 2940 border: 2px dashed @grey5;
2923 2941 border-radius: 5px;
2924 2942 background: white;
2925 2943 min-height: 200px;
2926 2944 padding: 54px;
2927 2945
2928 2946 .dz-message {
2929 2947 font-weight: 700;
2930 2948 text-align: center;
2931 2949 margin: 2em 0;
2932 2950 }
2933 2951
2934 2952 }
2935 2953
2936 2954 .dz-preview {
2937 2955 margin: 10px 0 !important;
2938 2956 position: relative;
2939 2957 vertical-align: top;
2940 2958 padding: 10px;
2941 2959 border-bottom: 1px solid @grey5;
2942 2960 }
2943 2961
2944 2962 .dz-filename {
2945 2963 font-weight: 700;
2946 2964 float: left;
2947 2965 }
2948 2966
2949 2967 .dz-sending {
2950 2968 float: right;
2951 2969 }
2952 2970
2953 2971 .dz-response {
2954 2972 clear: both
2955 2973 }
2956 2974
2957 2975 .dz-filename-size {
2958 2976 float: right
2959 2977 }
2960 2978
2961 2979 .dz-error-message {
2962 2980 color: @alert2;
2963 2981 padding-top: 10px;
2964 2982 clear: both;
2965 2983 }
2966 2984
2967 2985
2968 2986 .user-hovercard {
2969 2987 padding: 5px;
2970 2988 }
2971 2989
2972 2990 .user-hovercard-icon {
2973 2991 display: inline;
2974 2992 padding: 0;
2975 2993 box-sizing: content-box;
2976 2994 border-radius: 50%;
2977 2995 float: left;
2978 2996 }
2979 2997
2980 2998 .user-hovercard-name {
2981 2999 float: right;
2982 3000 vertical-align: top;
2983 3001 padding-left: 10px;
2984 3002 min-width: 150px;
2985 3003 }
2986 3004
2987 3005 .user-hovercard-bio {
2988 3006 clear: both;
2989 3007 padding-top: 10px;
2990 3008 }
2991 3009
2992 3010 .user-hovercard-header {
2993 3011 clear: both;
2994 3012 min-height: 10px;
2995 3013 }
2996 3014
2997 3015 .user-hovercard-footer {
2998 3016 clear: both;
2999 3017 min-height: 10px;
3000 3018 }
3001 3019
3002 3020 .user-group-hovercard {
3003 3021 padding: 5px;
3004 3022 }
3005 3023
3006 3024 .user-group-hovercard-icon {
3007 3025 display: inline;
3008 3026 padding: 0;
3009 3027 box-sizing: content-box;
3010 3028 border-radius: 50%;
3011 3029 float: left;
3012 3030 }
3013 3031
3014 3032 .user-group-hovercard-name {
3015 3033 float: left;
3016 3034 vertical-align: top;
3017 3035 padding-left: 10px;
3018 3036 min-width: 150px;
3019 3037 }
3020 3038
3021 3039 .user-group-hovercard-icon i {
3022 3040 border: 1px solid @grey4;
3023 3041 border-radius: 4px;
3024 3042 }
3025 3043
3026 3044 .user-group-hovercard-bio {
3027 3045 clear: both;
3028 3046 padding-top: 10px;
3029 3047 line-height: 1.0em;
3030 3048 }
3031 3049
3032 3050 .user-group-hovercard-header {
3033 3051 clear: both;
3034 3052 min-height: 10px;
3035 3053 }
3036 3054
3037 3055 .user-group-hovercard-footer {
3038 3056 clear: both;
3039 3057 min-height: 10px;
3040 3058 }
3041 3059
3042 3060 .pr-hovercard-header {
3043 3061 clear: both;
3044 3062 display: block;
3045 3063 line-height: 20px;
3046 3064 }
3047 3065
3048 3066 .pr-hovercard-user {
3049 3067 display: flex;
3050 3068 align-items: center;
3051 3069 padding-left: 5px;
3052 3070 }
3053 3071
3054 3072 .pr-hovercard-title {
3055 3073 padding-top: 5px;
3056 } No newline at end of file
3074 }
3075
3076 .action-divider {
3077 opacity: 0.5;
3078 }
3079
3080 .details-inline-block {
3081 display: inline-block;
3082 position: relative;
3083 }
3084
3085 .details-inline-block summary {
3086 list-style: none;
3087 }
3088
3089 details:not([open]) > :not(summary) {
3090 display: none !important;
3091 }
3092
3093 .details-reset > summary {
3094 list-style: none;
3095 }
3096
3097 .details-reset > summary::-webkit-details-marker {
3098 display: none;
3099 }
3100
3101 .details-dropdown {
3102 position: absolute;
3103 top: 100%;
3104 width: 185px;
3105 list-style: none;
3106 background-color: #fff;
3107 background-clip: padding-box;
3108 border: 1px solid @grey5;
3109 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3110 left: -150px;
3111 text-align: left;
3112 z-index: 90;
3113 }
3114
3115 .dropdown-divider {
3116 display: block;
3117 height: 0;
3118 margin: 8px 0;
3119 border-top: 1px solid @grey5;
3120 }
3121
3122 .dropdown-item {
3123 display: block;
3124 padding: 4px 8px 4px 16px;
3125 overflow: hidden;
3126 text-overflow: ellipsis;
3127 white-space: nowrap;
3128 font-weight: normal;
3129 }
3130
3131 .right-sidebar {
3132 position: fixed;
3133 top: 0px;
3134 bottom: 0;
3135 right: 0;
3136
3137 background: #fafafa;
3138 z-index: 50;
3139 }
3140
3141 .right-sidebar {
3142 border-left: 1px solid @grey5;
3143 }
3144
3145 .right-sidebar.right-sidebar-expanded {
3146 width: 300px;
3147 overflow: scroll;
3148 }
3149
3150 .right-sidebar.right-sidebar-collapsed {
3151 width: 40px;
3152 padding: 0;
3153 display: block;
3154 overflow: hidden;
3155 }
3156
3157 .sidenav {
3158 float: right;
3159 will-change: min-height;
3160 background: #fafafa;
3161 width: 100%;
3162 }
3163
3164 .sidebar-toggle {
3165 height: 30px;
3166 text-align: center;
3167 margin: 15px 0px 0 0;
3168 }
3169
3170 .sidebar-toggle a {
3171
3172 }
3173
3174 .sidebar-content {
3175 margin-left: 15px;
3176 margin-right: 15px;
3177 }
3178
3179 .sidebar-heading {
3180 font-size: 1.2em;
3181 font-weight: 700;
3182 margin-top: 10px;
3183 }
3184
3185 .sidebar-element {
3186 margin-top: 20px;
3187 }
3188
3189 .right-sidebar-collapsed-state {
3190 display: flex;
3191 flex-direction: column;
3192 justify-content: center;
3193 align-items: center;
3194 padding: 0 10px;
3195 cursor: pointer;
3196 font-size: 1.3em;
3197 margin: 0 -15px;
3198 }
3199
3200 .right-sidebar-collapsed-state:hover {
3201 background-color: @grey5;
3202 }
3203
3204 .old-comments-marker {
3205 text-align: left;
3206 }
3207
3208 .old-comments-marker td {
3209 padding-top: 15px;
3210 border-bottom: 1px solid @grey5;
3211 }
@@ -1,872 +1,895 b''
1 1 // navigation.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // TOP MAIN DARK NAVIGATION
6 6
7 7 .header .main_nav.horizontal-list {
8 8 float: right;
9 9 color: @grey4;
10 10 > li {
11 11 a {
12 12 color: @grey4;
13 13 }
14 14 }
15 15 }
16 16
17 17 // HEADER NAVIGATION
18 18
19 19 .horizontal-list {
20 20 display: block;
21 21 margin: 0;
22 22 padding: 0;
23 23 -webkit-padding-start: 0;
24 24 text-align: left;
25 25 font-size: @navigation-fontsize;
26 26 color: @grey6;
27 27 z-index:10;
28 28
29 29 li {
30 30 line-height: 1em;
31 31 list-style-type: none;
32 32 margin: 0 20px 0 0;
33 33
34 34 a {
35 35 padding: 0 .5em;
36 36
37 37 &.menu_link_notifications {
38 38 .pill(7px,@rcblue);
39 39 display: inline;
40 40 margin: 0 7px 0 .7em;
41 41 font-size: @basefontsize;
42 42 color: white;
43 43
44 44 &.empty {
45 45 background-color: @grey4;
46 46 }
47 47
48 48 &:hover {
49 49 background-color: @rcdarkblue;
50 50 }
51 51 }
52 52 }
53 53 .pill_container {
54 54 margin: 1.25em 0px 0px 0px;
55 55 float: right;
56 56 }
57 57
58 58 &#quick_login_li {
59 59 &:hover {
60 60 color: @grey5;
61 61 }
62 62
63 63 a.menu_link_notifications {
64 64 color: white;
65 65 }
66 66
67 67 .user {
68 68 padding-bottom: 10px;
69 69 }
70 70 }
71 71
72 72 &:before { content: none; }
73 73
74 74 &:last-child {
75 75 .menulabel {
76 76 padding-right: 0;
77 77 border-right: none;
78 78
79 79 .show_more {
80 80 padding-right: 0;
81 81 }
82 82 }
83 83
84 84 &> a {
85 85 border-bottom: none;
86 86 }
87 87 }
88 88
89 89 &.open {
90 90
91 91 a {
92 92 color: white;
93 93 }
94 94 }
95 95
96 96 &:focus {
97 97 outline: none;
98 98 }
99 99
100 100 ul li {
101 101 display: block;
102 102
103 103 &:last-child> a {
104 104 border-bottom: none;
105 105 }
106 106
107 107 ul li:last-child a {
108 108 /* we don't expect more then 3 levels of submenu and the third
109 109 level can have different html structure */
110 110 border-bottom: none;
111 111 }
112 112 }
113 113 }
114 114
115 115 > li {
116 116 float: left;
117 117 display: block;
118 118 padding: 0;
119 119
120 120 > a,
121 121 &.has_select2 a {
122 122 display: block;
123 123 padding: 10px 0;
124 124 }
125 125
126 126 .menulabel {
127 127 line-height: 1em;
128 128 // for this specifically we do not use a variable
129 129 }
130 130
131 131 .menulink-counter {
132 132 border: 1px solid @grey2;
133 133 border-radius: @border-radius;
134 134 background: @grey7;
135 135 display: inline-block;
136 136 padding: 0px 4px;
137 137 text-align: center;
138 138 font-size: 12px;
139 139 }
140 140
141 141 .pr_notifications {
142 142 padding-left: .5em;
143 143 }
144 144
145 145 .pr_notifications + .menulabel {
146 146 display:inline;
147 147 padding-left: 0;
148 148 }
149 149
150 150 &:hover,
151 151 &.open,
152 152 &.active {
153 153 a {
154 154 color: @rcblue;
155 155 }
156 156 }
157 157 }
158 158
159 159 pre {
160 160 margin: 0;
161 161 padding: 0;
162 162 }
163 163
164 164 .select2-container,
165 165 .menulink.childs {
166 166 position: relative;
167 167 }
168 168
169 169 .menulink {
170 170 &.disabled {
171 171 color: @grey3;
172 172 cursor: default;
173 173 opacity: 0.5;
174 174 }
175 175 }
176 176
177 177 #quick_login {
178 178
179 179 li a {
180 180 padding: .5em 0;
181 181 border-bottom: none;
182 182 color: @grey2;
183 183
184 184 &:hover { color: @rcblue; }
185 185 }
186 186 }
187 187
188 188 #quick_login_link {
189 189 display: inline-block;
190 190
191 191 .gravatar {
192 192 border: 1px solid @grey5;
193 193 }
194 194
195 195 .gravatar-login {
196 196 height: 20px;
197 197 width: 20px;
198 198 margin: -8px 0;
199 199 padding: 0;
200 200 }
201 201
202 202 &:hover .user {
203 203 color: @grey6;
204 204 }
205 205 }
206 206 }
207 207 .header .horizontal-list {
208 208
209 209 li {
210 210
211 211 &#quick_login_li {
212 212 padding-left: .5em;
213 213 margin-right: 0px;
214 214
215 215 &:hover #quick_login_link {
216 216 color: inherit;
217 217 }
218 218
219 219 .menu_link_user {
220 220 padding: 0 2px;
221 221 }
222 222 }
223 223 list-style-type: none;
224 224 }
225 225
226 226 > li {
227 227
228 228 a {
229 229 padding: 18px 0 12px 0;
230 230 color: @nav-grey;
231 231
232 232 &.menu_link_notifications {
233 233 padding: 1px 8px;
234 234 }
235 235 }
236 236
237 237 &:hover,
238 238 &.open,
239 239 &.active {
240 240 .pill_container a {
241 241 // don't select text for the pill container, it has it' own
242 242 // hover behaviour
243 243 color: @nav-grey;
244 244 }
245 245 }
246 246
247 247 &:hover,
248 248 &.open,
249 249 &.active {
250 250 a {
251 251 color: @grey6;
252 252 }
253 253 }
254 254
255 255 .select2-dropdown-open a {
256 256 color: @grey6;
257 257 }
258 258
259 259 .repo-switcher {
260 260 padding-left: 0;
261 261
262 262 .menulabel {
263 263 padding-left: 0;
264 264 }
265 265 }
266 266 }
267 267
268 268 li ul li {
269 269 background-color:@grey2;
270 270
271 271 a {
272 272 padding: .5em 0;
273 273 border-bottom: @border-thickness solid @border-default-color;
274 274 color: @grey6;
275 275 }
276 276
277 277 &:last-child a, &.last a{
278 278 border-bottom: none;
279 279 }
280 280
281 281 &:hover {
282 282 background-color: @grey3;
283 283 }
284 284 }
285 285
286 286 .submenu {
287 287 margin-top: 5px;
288 288 }
289 289 }
290 290
291 291 // SUBMENUS
292 292 .navigation .submenu {
293 293 display: none;
294 294 }
295 295
296 296 .navigation li.open {
297 297 .submenu {
298 298 display: block;
299 299 }
300 300 }
301 301
302 302 .navigation li:last-child .submenu {
303 303 right: auto;
304 304 left: 0;
305 305 border: 1px solid @grey5;
306 306 background: @white;
307 307 box-shadow: @dropdown-shadow;
308 308 }
309 309
310 310 .submenu {
311 311 position: absolute;
312 312 top: 100%;
313 313 left: 0;
314 314 min-width: 180px;
315 315 margin: 2px 0 0;
316 316 padding: 0;
317 317 text-align: left;
318 318 font-family: @text-light;
319 319 border-radius: @border-radius;
320 320 z-index: 20;
321 321
322 322 li {
323 323 display: block;
324 324 margin: 0;
325 325 padding: 0 .5em;
326 326 line-height: 1em;
327 327 color: @grey3;
328 328 background-color: @white;
329 329 list-style-type: none;
330 330
331 331 a {
332 332 display: block;
333 333 width: 100%;
334 334 padding: .5em 0;
335 335 border-right: none;
336 336 border-bottom: @border-thickness solid white;
337 337 color: @grey3;
338 338 }
339 339
340 340 ul {
341 341 display: none;
342 342 position: absolute;
343 343 top: 0;
344 344 right: 100%;
345 345 padding: 0;
346 346 z-index: 30;
347 347 }
348 348 &:hover {
349 349 background-color: @grey7;
350 350 -webkit-transition: background .3s;
351 351 -moz-transition: background .3s;
352 352 -o-transition: background .3s;
353 353 transition: background .3s;
354 354
355 355 ul {
356 356 display: block;
357 357 }
358 358 }
359 359 }
360 360
361 361 }
362 362
363 363
364 364
365 365
366 366 // repo dropdown
367 367 .quick_repo_menu {
368 368 width: 15px;
369 369 text-align: center;
370 370 position: relative;
371 371 cursor: pointer;
372 372
373 373 div {
374 374 overflow: visible !important;
375 375 }
376 376
377 377 &.sorting {
378 378 cursor: auto;
379 379 }
380 380
381 381 &:hover {
382 382 .menu_items_container {
383 383 position: absolute;
384 384 display: block;
385 385 }
386 386 .menu_items {
387 387 display: block;
388 388 }
389 389 }
390 390
391 391 i {
392 392 margin: 0;
393 393 color: @grey4;
394 394 }
395 395
396 396 .menu_items_container {
397 397 position: absolute;
398 398 top: 0;
399 399 left: 100%;
400 400 margin: 0;
401 401 padding: 0;
402 402 list-style: none;
403 403 background-color: @grey6;
404 404 z-index: 999;
405 405 text-align: left;
406 406
407 407 a {
408 408 color: @grey2;
409 409 }
410 410
411 411 ul.menu_items {
412 412 margin: 0;
413 413 padding: 0;
414 414 }
415 415
416 416 li {
417 417 margin: 0;
418 418 padding: 0;
419 419 line-height: 1em;
420 420 list-style-type: none;
421 421
422 422 a {
423 423 display: block;
424 424 height: 16px;
425 425 padding: 8px; //must add up to td height (28px)
426 426 width: 120px; // set width
427 427
428 428 &:hover {
429 429 background-color: @grey5;
430 430 -webkit-transition: background .3s;
431 431 -moz-transition: background .3s;
432 432 -o-transition: background .3s;
433 433 transition: background .3s;
434 434 }
435 435 }
436 436 }
437 437 }
438 438 }
439 439
440 440
441 441 // new objects main action
442 442 .action-menu {
443 443 left: auto;
444 444 right: 0;
445 445 padding: 12px;
446 446 z-index: 999;
447 447 overflow: hidden;
448 448 background-color: #fff;
449 449 border: 1px solid @grey5;
450 450 color: @grey2;
451 451 box-shadow: @dropdown-shadow;
452 452
453 453 .submenu-title {
454 454 font-weight: bold;
455 455 }
456 456
457 457 .submenu-title:not(:first-of-type) {
458 458 padding-top: 10px;
459 459 }
460 460
461 461 &.submenu {
462 462 min-width: 200px;
463 463
464 464 ol {
465 465 padding:0;
466 466 }
467 467
468 468 li {
469 469 display: block;
470 470 margin: 0;
471 471 padding: .2em .5em;
472 472 line-height: 1em;
473 473
474 474 background-color: #fff;
475 475 list-style-type: none;
476 476
477 477 a {
478 478 padding: 4px;
479 479 color: @grey4 !important;
480 480 border-bottom: none;
481 481 }
482 482 }
483 483 li:not(.submenu-title) a:hover{
484 484 color: @grey2 !important;
485 485 }
486 486 }
487 487 }
488 488
489 489
490 490 // Header Repository Switcher
491 491 // Select2 Dropdown
492 492 #select2-drop.select2-drop.repo-switcher-dropdown {
493 493 width: auto !important;
494 494 margin-top: 5px;
495 495 padding: 1em 0;
496 496 text-align: left;
497 497 .border-radius-bottom(@border-radius);
498 498 border-color: transparent;
499 499 color: @grey6;
500 500 background-color: @grey2;
501 501
502 502 input {
503 503 min-width: 90%;
504 504 }
505 505
506 506 ul.select2-result-sub {
507 507
508 508 li {
509 509 line-height: 1em;
510 510
511 511 &:hover,
512 512 &.select2-highlighted {
513 513 background-color: @grey3;
514 514 }
515 515 }
516 516
517 517 &:before { content: none; }
518 518 }
519 519
520 520 ul.select2-results {
521 521 min-width: 200px;
522 522 margin: 0;
523 523 padding: 0;
524 524 list-style-type: none;
525 525 overflow-x: visible;
526 526 overflow-y: scroll;
527 527
528 528 li {
529 529 padding: 0 8px;
530 530 line-height: 1em;
531 531 color: @grey6;
532 532
533 533 &>.select2-result-label {
534 534 padding: 8px 0;
535 535 border-bottom: @border-thickness solid @grey3;
536 536 white-space: nowrap;
537 537 color: @grey5;
538 538 cursor: pointer;
539 539 }
540 540
541 541 &.select2-result-with-children {
542 542 margin: 0;
543 543 padding: 0;
544 544 }
545 545
546 546 &.select2-result-unselectable > .select2-result-label {
547 547 margin: 0 8px;
548 548 }
549 549
550 550 }
551 551 }
552 552
553 553 ul.select2-result-sub {
554 554 margin: 0;
555 555 padding: 0;
556 556
557 557 li {
558 558 display: block;
559 559 margin: 0;
560 560 border-right: none;
561 561 line-height: 1em;
562 562 font-family: @text-light;
563 563 color: @grey2;
564 564 list-style-type: none;
565 565
566 566 &:hover {
567 567 background-color: @grey3;
568 568 }
569 569 }
570 570 }
571 571 }
572 572
573 573
574 574 #context-bar {
575 575 display: block;
576 576 margin: 0 auto 20px 0;
577 577 padding: 0 @header-padding;
578 578 background-color: @grey7;
579 579 border-bottom: 1px solid @grey5;
580 580
581 581 .clear {
582 582 clear: both;
583 583 }
584 584 }
585 585
586 586 ul#context-pages {
587 587 li {
588 588 list-style-type: none;
589 589
590 590 a {
591 591 color: @grey2;
592 592
593 593 &:hover {
594 594 color: @grey1;
595 595 }
596 596 }
597 597
598 598 &.active {
599 599 // special case, non-variable color
600 600 border-bottom: 2px solid @rcblue;
601 601
602 602 a {
603 603 color: @rcblue;
604 604 }
605 605 }
606 606 }
607 607 }
608 608
609 609 // PAGINATION
610 610
611 611 .pagination {
612 612 border: @border-thickness solid @grey5;
613 613 color: @grey2;
614 614 box-shadow: @button-shadow;
615 615
616 616 .current {
617 617 color: @grey4;
618 618 }
619 619 }
620 620
621 621 .dataTables_processing {
622 622 text-align: center;
623 623 font-size: 1.1em;
624 624 position: relative;
625 625 top: 95px;
626 626 height: 0;
627 627 }
628 628
629 629 .dataTables_paginate,
630 630 .pagination-wh {
631 631 text-align: center;
632 632 display: inline-block;
633 633 border-left: 1px solid @grey5;
634 634 float: none;
635 635 overflow: hidden;
636 636 box-shadow: @button-shadow;
637 637
638 638 .paginate_button, .pager_curpage,
639 639 .pager_link, .pg-previous, .pg-next, .pager_dotdot {
640 640 display: inline-block;
641 641 padding: @menupadding/4 @menupadding;
642 642 border: 1px solid @grey5;
643 643 margin-left: -1px;
644 644 color: @grey2;
645 645 cursor: pointer;
646 646 float: left;
647 647 font-weight: 600;
648 648 white-space: nowrap;
649 649 vertical-align: middle;
650 650 user-select: none;
651 651 min-width: 15px;
652 652
653 653 &:hover {
654 654 color: @rcdarkblue;
655 655 }
656 656 }
657 657
658 658 .paginate_button.disabled,
659 659 .disabled {
660 660 color: @grey3;
661 661 cursor: default;
662 662 opacity: 0.5;
663 663 }
664 664
665 665 .paginate_button.current, .pager_curpage {
666 666 background: @rcblue;
667 667 border-color: @rcblue;
668 668 color: @white;
669 669 }
670 670
671 671 .ellipsis {
672 672 display: inline-block;
673 673 text-align: left;
674 674 padding: @menupadding/4 @menupadding;
675 675 border: 1px solid @grey5;
676 676 border-left: 0;
677 677 float: left;
678 678 }
679 679 }
680 680
681 681 // SIDEBAR
682 682
683 683 .sidebar {
684 684 .block-left;
685 685 clear: left;
686 686 max-width: @sidebar-width;
687 687 margin-right: @sidebarpadding;
688 688 padding-right: @sidebarpadding;
689 689 font-family: @text-regular;
690 690 color: @grey1;
691 691
692 692 .nav-pills {
693 693 margin: 0;
694 694 }
695 695
696 696 .nav {
697 697 list-style: none;
698 698 padding: 0;
699 699
700 700 li {
701 701 padding-bottom: @menupadding;
702 702 line-height: 1em;
703 703 color: @grey4;
704 704 list-style-type: none;
705 705
706 706 &.active a {
707 707 color: @grey2;
708 708 }
709 709
710 710 a {
711 711 color: @grey4;
712 712 }
713 713 }
714 714
715 715 }
716 716 }
717 717
718 718 .main_filter_help_box {
719 719 padding: 7px 7px;
720 720 display: inline-block;
721 721 vertical-align: top;
722 722 background: inherit;
723 723 position: absolute;
724 724 right: 0;
725 725 top: 9px;
726 726 }
727 727
728 728 .main_filter_input_box {
729 729 display: inline-block;
730 730
731 731 .searchItems {
732 732 display:flex;
733 733 background: @black;
734 734 padding: 0px;
735 735 border-radius: 3px;
736 736 border: 1px solid @black;
737 737
738 738 a {
739 739 border: none !important;
740 740 }
741 741 }
742 742
743 743 .searchTag {
744 744 line-height: 28px;
745 745 padding: 0 5px;
746 746
747 747 .tag {
748 748 color: @grey5;
749 749 border-color: @grey2;
750 750 background: @grey1;
751 751 }
752 752 }
753 753
754 754 .searchTagFilter {
755 755 background-color: @black !important;
756 756 margin-right: 0;
757 757 }
758 758 .searchTagIcon {
759 759 margin: 0;
760 760 background: @black !important;
761 761 }
762 762 .searchTagHelp {
763 763 background-color: @grey1 !important;
764 764 margin: 0;
765 765 }
766 766 .searchTagHelp:hover {
767 767 background-color: @grey1 !important;
768 768 }
769 769 .searchTagInput {
770 770 background-color: @grey1 !important;
771 771 margin-right: 0;
772 772 }
773 773 }
774 774
775 775 .main_filter_box {
776 776 margin: 9px 0 0 0;
777 777 }
778 778
779 779 #main_filter_help {
780 780 background: @grey1;
781 781 border: 1px solid black;
782 782 position: absolute;
783 783 white-space: pre;
784 784 z-index: 9999;
785 785 color: @nav-grey;
786 786 padding: 0 10px;
787 787 }
788 788
789 789 input {
790 790
791 791 &.main_filter_input {
792 792 padding: 5px 10px;
793 min-width: 340px;
793
794 794 color: @grey7;
795 795 background: @black;
796 796 min-height: 18px;
797 797 border: 0;
798 798
799 799 &:active {
800 800 color: @grey2 !important;
801 801 background: white !important;
802 802 }
803
803 804 &:focus {
804 805 color: @grey2 !important;
805 806 background: white !important;
806 807 }
808
809 min-width: 360px;
810
811 @media screen and (max-width: 1600px) {
812 min-width: 300px;
813 }
814 @media screen and (max-width: 1500px) {
815 min-width: 280px;
816 }
817 @media screen and (max-width: 1400px) {
818 min-width: 260px;
819 }
820 @media screen and (max-width: 1300px) {
821 min-width: 240px;
822 }
823 @media screen and (max-width: 1200px) {
824 min-width: 220px;
825 }
826 @media screen and (max-width: 720px) {
827 min-width: 140px;
828 }
807 829 }
830
808 831 }
809 832
810 833
811 834
812 835 .main_filter_input::placeholder {
813 836 color: @nav-grey;
814 837 opacity: 1;
815 838 }
816 839
817 840 .notice-box {
818 841 display:block !important;
819 842 padding: 9px 0 !important;
820 843 }
821 844
822 845 .menulabel-notice {
823 846
824 847 padding:7px 10px;
825 848
826 849 &.notice-warning {
827 850 border: 1px solid @color3;
828 851 .notice-color-warning
829 852 }
830 853 &.notice-error {
831 854 border: 1px solid @color5;
832 855 .notice-color-error
833 856 }
834 857 &.notice-info {
835 858 border: 1px solid @color1;
836 859 .notice-color-info
837 860 }
838 861 }
839 862
840 863 .notice-messages-container {
841 864 position: absolute;
842 865 top: 45px;
843 866 }
844 867
845 868 .notice-messages {
846 869 display: block;
847 870 position: relative;
848 871 z-index: 300;
849 872 min-width: 500px;
850 873 max-width: 500px;
851 874 min-height: 100px;
852 875 margin-top: 4px;
853 876 margin-bottom: 24px;
854 877 font-size: 14px;
855 878 font-weight: 400;
856 879 padding: 8px 0;
857 880 background-color: #fff;
858 881 border: 1px solid @grey4;
859 882 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
860 883 }
861 884
862 885 .notice-color-warning {
863 886 color: @color3;
864 887 }
865 888
866 889 .notice-color-error {
867 890 color: @color5;
868 891 }
869 892
870 893 .notice-color-info {
871 894 color: @color1;
872 895 }
@@ -1,291 +1,293 b''
1 1 @font-face {
2 2 font-family: 'rcicons';
3 3
4 4 src: url('../fonts/RCIcons/rcicons.eot?44705679');
5 5 src: url('../fonts/RCIcons/rcicons.eot?44705679#iefix') format('embedded-opentype'),
6 6 url('../fonts/RCIcons/rcicons.woff2?44705679') format('woff2'),
7 7 url('../fonts/RCIcons/rcicons.woff?44705679') format('woff'),
8 8 url('../fonts/RCIcons/rcicons.ttf?44705679') format('truetype'),
9 9 url('../fonts/RCIcons/rcicons.svg?44705679#rcicons') format('svg');
10 10
11 11 font-weight: normal;
12 12 font-style: normal;
13 13 }
14 14 /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
15 15 /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
16 16 /*
17 17 @media screen and (-webkit-min-device-pixel-ratio:0) {
18 18 @font-face {
19 19 font-family: 'rcicons';
20 20 src: url('../fonts/RCIcons/rcicons.svg?74666722#rcicons') format('svg');
21 21 }
22 22 }
23 23 */
24 24
25 25 [class^="icon-"]:before, [class*=" icon-"]:before {
26 26 font-family: "rcicons";
27 27 font-style: normal;
28 28 font-weight: normal;
29 29 speak: none;
30 30
31 31 display: inline-block;
32 32 text-decoration: inherit;
33 33 width: 1em;
34 34 margin-right: .2em;
35 35 text-align: center;
36 36 /* opacity: .8; */
37 37
38 38 /* For safety - reset parent styles, that can break glyph codes*/
39 39 font-variant: normal;
40 40 text-transform: none;
41 41
42 42 /* fix buttons height, for twitter bootstrap */
43 43 line-height: 1em;
44 44
45 45 /* Animation center compensation - margins should be symmetric */
46 46 /* remove if not needed */
47 47 margin-left: .2em;
48 48
49 49 /* you can be more comfortable with increased icons size */
50 50 /* font-size: 120%; */
51 51
52 52 /* Font smoothing. That was taken from TWBS */
53 53 -webkit-font-smoothing: antialiased;
54 54 -moz-osx-font-smoothing: grayscale;
55 55
56 56 /* Uncomment for 3D effect */
57 57 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
58 58 }
59 59
60 60 .animate-spin {
61 61 -moz-animation: spin 2s infinite linear;
62 62 -o-animation: spin 2s infinite linear;
63 63 -webkit-animation: spin 2s infinite linear;
64 64 animation: spin 2s infinite linear;
65 65 display: inline-block;
66 66 }
67 67 @-moz-keyframes spin {
68 68 0% {
69 69 -moz-transform: rotate(0deg);
70 70 -o-transform: rotate(0deg);
71 71 -webkit-transform: rotate(0deg);
72 72 transform: rotate(0deg);
73 73 }
74 74
75 75 100% {
76 76 -moz-transform: rotate(359deg);
77 77 -o-transform: rotate(359deg);
78 78 -webkit-transform: rotate(359deg);
79 79 transform: rotate(359deg);
80 80 }
81 81 }
82 82 @-webkit-keyframes spin {
83 83 0% {
84 84 -moz-transform: rotate(0deg);
85 85 -o-transform: rotate(0deg);
86 86 -webkit-transform: rotate(0deg);
87 87 transform: rotate(0deg);
88 88 }
89 89
90 90 100% {
91 91 -moz-transform: rotate(359deg);
92 92 -o-transform: rotate(359deg);
93 93 -webkit-transform: rotate(359deg);
94 94 transform: rotate(359deg);
95 95 }
96 96 }
97 97 @-o-keyframes spin {
98 98 0% {
99 99 -moz-transform: rotate(0deg);
100 100 -o-transform: rotate(0deg);
101 101 -webkit-transform: rotate(0deg);
102 102 transform: rotate(0deg);
103 103 }
104 104
105 105 100% {
106 106 -moz-transform: rotate(359deg);
107 107 -o-transform: rotate(359deg);
108 108 -webkit-transform: rotate(359deg);
109 109 transform: rotate(359deg);
110 110 }
111 111 }
112 112 @-ms-keyframes spin {
113 113 0% {
114 114 -moz-transform: rotate(0deg);
115 115 -o-transform: rotate(0deg);
116 116 -webkit-transform: rotate(0deg);
117 117 transform: rotate(0deg);
118 118 }
119 119
120 120 100% {
121 121 -moz-transform: rotate(359deg);
122 122 -o-transform: rotate(359deg);
123 123 -webkit-transform: rotate(359deg);
124 124 transform: rotate(359deg);
125 125 }
126 126 }
127 127 @keyframes spin {
128 128 0% {
129 129 -moz-transform: rotate(0deg);
130 130 -o-transform: rotate(0deg);
131 131 -webkit-transform: rotate(0deg);
132 132 transform: rotate(0deg);
133 133 }
134 134
135 135 100% {
136 136 -moz-transform: rotate(359deg);
137 137 -o-transform: rotate(359deg);
138 138 -webkit-transform: rotate(359deg);
139 139 transform: rotate(359deg);
140 140 }
141 141 }
142 142
143 143
144 144
145 145 .icon-no-margin::before {
146 146 margin: 0;
147 147
148 148 }
149 149 // -- ICON CLASSES -- //
150 150 // sorter = lambda s: '\n'.join(sorted(s.splitlines()))
151 151
152 152 .icon-delete:before { content: '\e800'; } /* '' */
153 153 .icon-ok:before { content: '\e801'; } /* '' */
154 154 .icon-comment:before { content: '\e802'; } /* '' */
155 155 .icon-bookmark:before { content: '\e803'; } /* '' */
156 156 .icon-branch:before { content: '\e804'; } /* '' */
157 157 .icon-tag:before { content: '\e805'; } /* '' */
158 158 .icon-lock:before { content: '\e806'; } /* '' */
159 159 .icon-unlock:before { content: '\e807'; } /* '' */
160 160 .icon-feed:before { content: '\e808'; } /* '' */
161 161 .icon-left:before { content: '\e809'; } /* '' */
162 162 .icon-right:before { content: '\e80a'; } /* '' */
163 163 .icon-down:before { content: '\e80b'; } /* '' */
164 164 .icon-folder:before { content: '\e80c'; } /* '' */
165 165 .icon-folder-open:before { content: '\e80d'; } /* '' */
166 166 .icon-trash-empty:before { content: '\e80e'; } /* '' */
167 167 .icon-group:before { content: '\e80f'; } /* '' */
168 168 .icon-remove:before { content: '\e810'; } /* '' */
169 169 .icon-fork:before { content: '\e811'; } /* '' */
170 170 .icon-more:before { content: '\e812'; } /* '' */
171 .icon-options:before { content: '\e812'; } /* '' */
171 172 .icon-search:before { content: '\e813'; } /* '' */
172 173 .icon-scissors:before { content: '\e814'; } /* '' */
173 174 .icon-download:before { content: '\e815'; } /* '' */
174 175 .icon-doc:before { content: '\e816'; } /* '' */
175 176 .icon-cog:before { content: '\e817'; } /* '' */
176 177 .icon-cog-alt:before { content: '\e818'; } /* '' */
177 178 .icon-eye:before { content: '\e819'; } /* '' */
178 179 .icon-eye-off:before { content: '\e81a'; } /* '' */
179 180 .icon-cancel-circled2:before { content: '\e81b'; } /* '' */
180 181 .icon-cancel-circled:before { content: '\e81c'; } /* '' */
181 182 .icon-plus:before { content: '\e81d'; } /* '' */
182 183 .icon-plus-circled:before { content: '\e81e'; } /* '' */
183 184 .icon-minus-circled:before { content: '\e81f'; } /* '' */
184 185 .icon-minus:before { content: '\e820'; } /* '' */
185 186 .icon-info-circled:before { content: '\e821'; } /* '' */
186 187 .icon-upload:before { content: '\e822'; } /* '' */
187 188 .icon-home:before { content: '\e823'; } /* '' */
188 189 .icon-flag-filled:before { content: '\e824'; } /* '' */
189 190 .icon-git:before { content: '\e82a'; } /* '' */
190 191 .icon-hg:before { content: '\e82d'; } /* '' */
191 192 .icon-svn:before { content: '\e82e'; } /* '' */
192 193 .icon-comment-add:before { content: '\e82f'; } /* '' */
193 194 .icon-comment-toggle:before { content: '\e830'; } /* '' */
194 195 .icon-rhodecode:before { content: '\e831'; } /* '' */
195 196 .icon-up:before { content: '\e832'; } /* '' */
196 197 .icon-merge:before { content: '\e833'; } /* '' */
197 198 .icon-spin-alt:before { content: '\e834'; } /* '' */
198 199 .icon-spin:before { content: '\e838'; } /* '' */
199 200 .icon-docs:before { content: '\f0c5'; } /* '' */
200 201 .icon-menu:before { content: '\f0c9'; } /* '' */
201 202 .icon-sort:before { content: '\f0dc'; } /* '' */
202 203 .icon-paste:before { content: '\f0ea'; } /* '' */
203 204 .icon-doc-text:before { content: '\f0f6'; } /* '' */
204 205 .icon-plus-squared:before { content: '\f0fe'; } /* '' */
205 206 .icon-angle-left:before { content: '\f104'; } /* '' */
206 207 .icon-angle-right:before { content: '\f105'; } /* '' */
207 208 .icon-angle-up:before { content: '\f106'; } /* '' */
208 209 .icon-angle-down:before { content: '\f107'; } /* '' */
209 210 .icon-circle-empty:before { content: '\f10c'; } /* '' */
210 211 .icon-circle:before { content: '\f111'; } /* '' */
211 212 .icon-folder-empty:before { content: '\f114'; } /* '' */
212 213 .icon-folder-open-empty:before { content: '\f115'; } /* '' */
213 214 .icon-code:before { content: '\f121'; } /* '' */
214 215 .icon-info:before { content: '\f129'; } /* '' */
215 216 .icon-minus-squared:before { content: '\f146'; } /* '' */
216 217 .icon-minus-squared-alt:before { content: '\f147'; } /* '' */
217 218 .icon-doc-inv:before { content: '\f15b'; } /* '' */
218 219 .icon-doc-text-inv:before { content: '\f15c'; } /* '' */
219 220 .icon-plus-squared-alt:before { content: '\f196'; } /* '' */
220 221 .icon-file-code:before { content: '\f1c9'; } /* '' */
221 222 .icon-history:before { content: '\f1da'; } /* '' */
222 223 .icon-circle-thin:before { content: '\f1db'; } /* '' */
223 224 .icon-sliders:before { content: '\f1de'; } /* '' */
224 225 .icon-trash:before { content: '\f1f8'; } /* '' */
225 226
226 227
227 228 // MERGED ICONS BASED ON CURRENT ONES
228 229 .icon-repo-group:before { &:extend(.icon-folder-open:before); }
229 230 .icon-repo-private:before { &:extend(.icon-lock:before); }
230 231 .icon-repo-lock:before { &:extend(.icon-lock:before); }
231 232 .icon-unlock-alt:before { &:extend(.icon-unlock:before); }
232 233 .icon-repo-unlock:before { &:extend(.icon-unlock:before); }
233 234 .icon-repo-public:before { &:extend(.icon-unlock:before); }
234 235 .icon-rss-sign:before { &:extend(.icon-feed:before); }
235 236 .icon-code-fork:before { &:extend(.icon-fork:before); }
236 237 .icon-arrow_up:before { &:extend(.icon-up:before); }
237 238 .icon-file:before { &:extend(.icon-file-code:before); }
238 239 .icon-file-text:before { &:extend(.icon-file-code:before); }
239 240 .icon-directory:before { &:extend(.icon-folder:before); }
240 241 .icon-more-linked:before { &:extend(.icon-more:before); }
241 242 .icon-clipboard:before { &:extend(.icon-docs:before); }
242 243 .icon-copy:before { &:extend(.icon-docs:before); }
243 244 .icon-true:before { &:extend(.icon-ok:before); }
244 245 .icon-false:before { &:extend(.icon-delete:before); }
245 246 .icon-expand-linked:before { &:extend(.icon-down:before); }
246 247 .icon-pr-merge-fail:before { &:extend(.icon-delete:before); }
247 248 .icon-wide-mode:before { &:extend(.icon-sort:before); }
248 249 .icon-flag-filled-red:before { &:extend(.icon-flag-filled:before); }
249 250 .icon-user-group-alt:before { &:extend(.icon-group:before); }
250 251
251 252 // TRANSFORM
252 253 .icon-merge:before {transform: rotate(180deg);}
253 254 .icon-wide-mode:before {transform: rotate(90deg);}
255 .icon-options:before {transform: rotate(90deg);}
254 256
255 257 // -- END ICON CLASSES -- //
256 258
257 259
258 260 //--- ICONS STYLING ------------------//
259 261
260 262 .icon-git { color: @color4 !important; }
261 263 .icon-hg { color: @color8 !important; }
262 264 .icon-svn { color: @color1 !important; }
263 265 .icon-git-inv { color: @color4 !important; }
264 266 .icon-hg-inv { color: @color8 !important; }
265 267 .icon-svn-inv { color: @color1 !important; }
266 268 .icon-repo-lock { color: #FF0000; }
267 269 .icon-repo-unlock { color: #FF0000; }
268 270 .icon-false { color: @grey5 }
269 271 .icon-expand-linked { cursor: pointer; color: @grey3; font-size: 14px }
270 272 .icon-more-linked { cursor: pointer; color: @grey3 }
271 273 .icon-flag-filled-red { color: @color5 !important; }
272 274 .icon-filled-red { color: @color5 !important; }
273 275
274 276 .repo-switcher-dropdown .select2-result-label {
275 277 .icon-git:before {
276 278 &:extend(.icon-git-transparent:before);
277 279 }
278 280 .icon-hg:before {
279 281 &:extend(.icon-hg-transparent:before);
280 282 color: @alert4;
281 283 }
282 284 .icon-svn:before {
283 285 &:extend(.icon-svn-transparent:before);
284 286 }
285 287 }
286 288
287 289 .icon-user-group:before {
288 290 &:extend(.icon-group:before);
289 291 margin: 0;
290 292 font-size: 16px;
291 293 }
@@ -1,137 +1,142 b''
1 1 // Global keyboard bindings
2 2
3 3 function setRCMouseBindings(repoName, repoLandingRev) {
4 4
5 5 /** custom callback for supressing mousetrap from firing */
6 6 Mousetrap.stopCallback = function(e, element) {
7 7 // if the element has the class "mousetrap" then no need to stop
8 8 if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
9 9 return false;
10 10 }
11 11
12 12 // stop for input, select, and textarea
13 13 return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
14 14 };
15 15
16 16 // general help "?"
17 17 Mousetrap.bind(['?'], function(e) {
18 18 $('#help_kb').modal({});
19 19 });
20 20
21 21 // / open the quick filter
22 22 Mousetrap.bind(['/'], function(e) {
23 23 $('#main_filter').get(0).focus();
24 24
25 25 // return false to prevent default browser behavior
26 26 // and stop event from bubbling
27 27 return false;
28 28 });
29 29
30 30 // ctrl/command+b, show the the main bar
31 31 Mousetrap.bind(['command+b', 'ctrl+b'], function(e) {
32 32 var $headerInner = $('#header-inner'),
33 33 $content = $('#content');
34 34 if ($headerInner.hasClass('hover') && $content.hasClass('hover')) {
35 35 $headerInner.removeClass('hover');
36 36 $content.removeClass('hover');
37 37 } else {
38 38 $headerInner.addClass('hover');
39 39 $content.addClass('hover');
40 40 }
41 41 return false;
42 42 });
43 43
44 44 // general nav g + action
45 45 Mousetrap.bind(['g h'], function(e) {
46 46 window.location = pyroutes.url('home');
47 47 });
48 48 Mousetrap.bind(['g g'], function(e) {
49 49 window.location = pyroutes.url('gists_show', {'private': 1});
50 50 });
51 51 Mousetrap.bind(['g G'], function(e) {
52 52 window.location = pyroutes.url('gists_show', {'public': 1});
53 53 });
54 54
55 55 Mousetrap.bind(['g 0'], function(e) {
56 56 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 0});
57 57 });
58 58 Mousetrap.bind(['g 1'], function(e) {
59 59 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 1});
60 60 });
61 61 Mousetrap.bind(['g 2'], function(e) {
62 62 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 2});
63 63 });
64 64 Mousetrap.bind(['g 3'], function(e) {
65 65 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 3});
66 66 });
67 67 Mousetrap.bind(['g 4'], function(e) {
68 68 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 4});
69 69 });
70 70 Mousetrap.bind(['g 5'], function(e) {
71 71 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 5});
72 72 });
73 73 Mousetrap.bind(['g 6'], function(e) {
74 74 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 6});
75 75 });
76 76 Mousetrap.bind(['g 7'], function(e) {
77 77 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 7});
78 78 });
79 79 Mousetrap.bind(['g 8'], function(e) {
80 80 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 8});
81 81 });
82 82 Mousetrap.bind(['g 9'], function(e) {
83 83 window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 9});
84 84 });
85 85
86 86 Mousetrap.bind(['n g'], function(e) {
87 87 window.location = pyroutes.url('gists_new');
88 88 });
89 89 Mousetrap.bind(['n r'], function(e) {
90 90 window.location = pyroutes.url('repo_new');
91 91 });
92 92
93 93 if (repoName && repoName !== '') {
94 94 // nav in repo context
95 95 Mousetrap.bind(['g s'], function(e) {
96 96 window.location = pyroutes.url(
97 97 'repo_summary', {'repo_name': repoName});
98 98 });
99 99 Mousetrap.bind(['g c'], function(e) {
100 100 window.location = pyroutes.url(
101 101 'repo_commits', {'repo_name': repoName});
102 102 });
103 103 Mousetrap.bind(['g F'], function(e) {
104 104 window.location = pyroutes.url(
105 105 'repo_files',
106 106 {
107 107 'repo_name': repoName,
108 108 'commit_id': repoLandingRev,
109 109 'f_path': '',
110 110 'search': '1'
111 111 });
112 112 });
113 113 Mousetrap.bind(['g f'], function(e) {
114 114 window.location = pyroutes.url(
115 115 'repo_files',
116 116 {
117 117 'repo_name': repoName,
118 118 'commit_id': repoLandingRev,
119 119 'f_path': ''
120 120 });
121 121 });
122 122 Mousetrap.bind(['g p'], function(e) {
123 123 window.location = pyroutes.url(
124 124 'pullrequest_show_all', {'repo_name': repoName});
125 125 });
126 126 Mousetrap.bind(['g o'], function(e) {
127 127 window.location = pyroutes.url(
128 128 'edit_repo', {'repo_name': repoName});
129 129 });
130 130 Mousetrap.bind(['g O'], function(e) {
131 131 window.location = pyroutes.url(
132 132 'edit_repo_perms', {'repo_name': repoName});
133 133 });
134 Mousetrap.bind(['t s'], function(e) {
135 if (window.toggleSidebar !== undefined) {
136 window.toggleSidebar();
137 }
138 });
134 139 }
135 140 }
136 141
137 142 setRCMouseBindings(templateContext.repo_name, templateContext.repo_landing_commit);
@@ -1,38 +1,109 b''
1 1 // # Copyright (C) 2010-2020 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 /**
20 20 * QUICK REPO MENU, used on repositories to show shortcuts to files, history
21 21 * etc.
22 22 */
23 23
24 24 var quick_repo_menu = function() {
25 25 var hide_quick_repo_menus = function() {
26 26 $('.menu_items_container').hide();
27 27 $('.active_quick_repo').removeClass('active_quick_repo');
28 28 };
29 29 $('.quick_repo_menu').hover(function() {
30 30 hide_quick_repo_menus();
31 31 if (!$(this).hasClass('active_quick_repo')) {
32 32 $('.menu_items_container', this).removeClass("hidden").show();
33 33 $(this).addClass('active_quick_repo');
34 34 }
35 35 }, function() {
36 36 hide_quick_repo_menus();
37 37 });
38 }; No newline at end of file
38 };
39
40
41 window.toggleElement = function (elem, target) {
42 var $elem = $(elem);
43 var $target = $(target);
44
45 if ($target.is(':visible') || $target.length === 0) {
46 $target.hide();
47 $elem.html($elem.data('toggleOn'))
48 } else {
49 $target.show();
50 $elem.html($elem.data('toggleOff'))
51 }
52
53 return false
54 }
55
56 var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
57 var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
58
59 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
60 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
61
62 var updateStickyHeader = function () {
63 if (window.updateSticky !== undefined) {
64 // potentially our comments change the active window size, so we
65 // notify sticky elements
66 updateSticky()
67 }
68 }
69
70 var expandSidebar = function () {
71 var $sideBar = $('.right-sidebar');
72 $('.outerwrapper').css(marginExpanded);
73 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
74 $('.right-sidebar-collapsed-state').hide();
75 $('.right-sidebar-expanded-state').show();
76 $('.branding').addClass('display-none');
77 $sideBar.addClass('right-sidebar-expanded')
78 $sideBar.removeClass('right-sidebar-collapsed')
79 }
80
81 var collapseSidebar = function () {
82 var $sideBar = $('.right-sidebar');
83 $('.outerwrapper').css(marginCollapsed);
84 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
85 $('.right-sidebar-collapsed-state').show();
86 $('.right-sidebar-expanded-state').hide();
87 $('.branding').removeClass('display-none');
88 $sideBar.removeClass('right-sidebar-expanded')
89 $sideBar.addClass('right-sidebar-collapsed')
90 }
91
92 window.toggleSidebar = function () {
93 var $sideBar = $('.right-sidebar');
94
95 if ($sideBar.hasClass('right-sidebar-expanded')) {
96 // expanded -> collapsed transition
97 collapseSidebar();
98 var sidebarState = 'collapsed';
99
100 } else {
101 // collapsed -> expanded
102 expandSidebar();
103 var sidebarState = 'expanded';
104 }
105
106 // update our other sticky header in same context
107 updateStickyHeader();
108 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
109 }
@@ -1,675 +1,890 b''
1 1 // # Copyright (C) 2010-2020 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
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = value['message'];
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 var rawMessage = elements[0]['message'];
84 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
87 87 // use reference name
88 88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 89 var refType = sourceRefType;
90 90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 91 }
92 92
93 93 return [title, desc]
94 94 };
95 95
96 96
97 97 ReviewersController = function () {
98 98 var self = this;
99 99 this.$reviewRulesContainer = $('#review_rules');
100 100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 101 this.$userRule = $('.pr-user-rule-container');
102 102 this.forbidReviewUsers = undefined;
103 103 this.$reviewMembers = $('#review_members');
104 104 this.currentRequest = null;
105 105 this.diffData = null;
106 106 this.enabledRules = [];
107 107
108 108 //dummy handler, we might register our own later
109 109 this.diffDataHandler = function(data){};
110 110
111 111 this.defaultForbidReviewUsers = function () {
112 112 return [
113 113 {
114 114 'username': 'default',
115 115 'user_id': templateContext.default_user.user_id
116 116 }
117 117 ];
118 118 };
119 119
120 120 this.hideReviewRules = function () {
121 121 self.$reviewRulesContainer.hide();
122 122 $(self.$userRule.selector).hide();
123 123 };
124 124
125 125 this.showReviewRules = function () {
126 126 self.$reviewRulesContainer.show();
127 127 $(self.$userRule.selector).show();
128 128 };
129 129
130 130 this.addRule = function (ruleText) {
131 131 self.showReviewRules();
132 132 self.enabledRules.push(ruleText);
133 133 return '<div>- {0}</div>'.format(ruleText)
134 134 };
135 135
136 136 this.loadReviewRules = function (data) {
137 137 self.diffData = data;
138 138
139 139 // reset forbidden Users
140 140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
141 141
142 142 // reset state of review rules
143 143 self.$rulesList.html('');
144 144
145 145 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
146 146 // default rule, case for older repo that don't have any rules stored
147 147 self.$rulesList.append(
148 148 self.addRule(
149 149 _gettext('All reviewers must vote.'))
150 150 );
151 151 return self.forbidReviewUsers
152 152 }
153 153
154 154 if (data.rules.voting !== undefined) {
155 155 if (data.rules.voting < 0) {
156 156 self.$rulesList.append(
157 157 self.addRule(
158 158 _gettext('All individual reviewers must vote.'))
159 159 )
160 160 } else if (data.rules.voting === 1) {
161 161 self.$rulesList.append(
162 162 self.addRule(
163 163 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
164 164 )
165 165
166 166 } else {
167 167 self.$rulesList.append(
168 168 self.addRule(
169 169 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
170 170 )
171 171 }
172 172 }
173 173
174 174 if (data.rules.voting_groups !== undefined) {
175 175 $.each(data.rules.voting_groups, function (index, rule_data) {
176 176 self.$rulesList.append(
177 177 self.addRule(rule_data.text)
178 178 )
179 179 });
180 180 }
181 181
182 182 if (data.rules.use_code_authors_for_review) {
183 183 self.$rulesList.append(
184 184 self.addRule(
185 185 _gettext('Reviewers picked from source code changes.'))
186 186 )
187 187 }
188 188
189 189 if (data.rules.forbid_adding_reviewers) {
190 190 $('#add_reviewer_input').remove();
191 191 self.$rulesList.append(
192 192 self.addRule(
193 193 _gettext('Adding new reviewers is forbidden.'))
194 194 )
195 195 }
196 196
197 197 if (data.rules.forbid_author_to_review) {
198 198 self.forbidReviewUsers.push(data.rules_data.pr_author);
199 199 self.$rulesList.append(
200 200 self.addRule(
201 201 _gettext('Author is not allowed to be a reviewer.'))
202 202 )
203 203 }
204 204
205 205 if (data.rules.forbid_commit_author_to_review) {
206 206
207 207 if (data.rules_data.forbidden_users) {
208 208 $.each(data.rules_data.forbidden_users, function (index, member_data) {
209 209 self.forbidReviewUsers.push(member_data)
210 210 });
211 211
212 212 }
213 213
214 214 self.$rulesList.append(
215 215 self.addRule(
216 216 _gettext('Commit Authors are not allowed to be a reviewer.'))
217 217 )
218 218 }
219 219
220 220 // we don't have any rules set, so we inform users about it
221 221 if (self.enabledRules.length === 0) {
222 222 self.addRule(
223 223 _gettext('No review rules set.'))
224 224 }
225 225
226 226 return self.forbidReviewUsers
227 227 };
228 228
229 229 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230 230
231 231 if (self.currentRequest) {
232 232 // make sure we cleanup old running requests before triggering this again
233 233 self.currentRequest.abort();
234 234 }
235 235
236 236 $('.calculate-reviewers').show();
237 237 // reset reviewer members
238 238 self.$reviewMembers.empty();
239 239
240 240 prButtonLock(true, null, 'reviewers');
241 241 $('#user').hide(); // hide user autocomplete before load
242 242
243 243 // lock PR button, so we cannot send PR before it's calculated
244 244 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245 245
246 246 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 247 // don't load defaults in case we're missing some refs...
248 248 $('.calculate-reviewers').hide();
249 249 return
250 250 }
251 251
252 252 var url = pyroutes.url('repo_default_reviewers_data',
253 253 {
254 254 'repo_name': templateContext.repo_name,
255 255 'source_repo': sourceRepo,
256 256 'source_ref': sourceRef[2],
257 257 'target_repo': targetRepo,
258 258 'target_ref': targetRef[2]
259 259 });
260 260
261 261 self.currentRequest = $.ajax({
262 262 url: url,
263 263 headers: {'X-PARTIAL-XHR': true},
264 264 type: 'GET',
265 265 success: function (data) {
266 266
267 267 self.currentRequest = null;
268 268
269 269 // review rules
270 270 self.loadReviewRules(data);
271 271 self.handleDiffData(data["diff_info"]);
272 272
273 273 for (var i = 0; i < data.reviewers.length; i++) {
274 274 var reviewer = data.reviewers[i];
275 275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
276 276 }
277 277 $('.calculate-reviewers').hide();
278 278 prButtonLock(false, null, 'reviewers');
279 279 $('#user').show(); // show user autocomplete after load
280 280
281 281 var commitElements = data["diff_info"]['commits'];
282
282 283 if (commitElements.length === 0) {
283 prButtonLock(true, _gettext('no commits'), 'all');
284 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
285 _gettext('There are no commits to merge.'));
286 prButtonLock(true, noCommitsMsg, 'all');
284 287
285 288 } else {
286 289 // un-lock PR button, so we cannot send PR before it's calculated
287 290 prButtonLock(false, null, 'compare');
288 291 }
289 292
290 293 },
291 294 error: function (jqXHR, textStatus, errorThrown) {
292 295 var prefix = "Loading diff and reviewers failed\n"
293 296 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
294 297 ajaxErrorSwal(message);
295 298 }
296 299 });
297 300
298 301 };
299 302
300 303 // check those, refactor
301 304 this.removeReviewMember = function (reviewer_id, mark_delete) {
302 305 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
303 306
304 307 if (typeof (mark_delete) === undefined) {
305 308 mark_delete = false;
306 309 }
307 310
308 311 if (mark_delete === true) {
309 312 if (reviewer) {
310 313 // now delete the input
311 314 $('#reviewer_{0} input'.format(reviewer_id)).remove();
312 315 // mark as to-delete
313 316 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
314 317 obj.addClass('to-delete');
315 318 obj.css({"text-decoration": "line-through", "opacity": 0.5});
316 319 }
317 320 } else {
318 321 $('#reviewer_{0}'.format(reviewer_id)).remove();
319 322 }
320 323 };
321 324
322 325 this.reviewMemberEntry = function () {
323 326
324 327 };
325 328
326 329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
327 var members = self.$reviewMembers.get(0);
328 330 var id = reviewer_obj.user_id;
329 331 var username = reviewer_obj.username;
330 332
331 333 var reasons = reasons || [];
332 334 var mandatory = mandatory || false;
333 335
334 336 // register IDS to check if we don't have this ID already in
335 337 var currentIds = [];
336 var _els = self.$reviewMembers.find('li').toArray();
337 for (el in _els) {
338 currentIds.push(_els[el].id)
339 }
338
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
340 currentIds.push($(value).data('reviewerUserId'))
341 })
340 342
341 343 var userAllowedReview = function (userId) {
342 344 var allowed = true;
343 345 $.each(self.forbidReviewUsers, function (index, member_data) {
344 346 if (parseInt(userId) === member_data['user_id']) {
345 347 allowed = false;
346 348 return false // breaks the loop
347 349 }
348 350 });
349 351 return allowed
350 352 };
351 353
352 354 var userAllowed = userAllowedReview(id);
353 355 if (!userAllowed) {
354 356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
355 357 } else {
356 358 // only add if it's not there
357 var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
359 var alreadyReviewer = currentIds.indexOf(id) != -1;
358 360
359 361 if (alreadyReviewer) {
360 362 alert(_gettext('User `{0}` already in reviewers').format(username));
361 363 } else {
362 members.innerHTML += renderTemplate('reviewMemberEntry', {
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
363 365 'member': reviewer_obj,
364 366 'mandatory': mandatory,
365 367 'reasons': reasons,
366 368 'allowed_to_update': true,
367 369 'review_status': 'not_reviewed',
368 370 'review_status_label': _gettext('Not Reviewed'),
369 371 'user_group': reviewer_obj.user_group,
370 372 'create': true,
371 });
373 'rule_show': true,
374 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
372 376 tooltipActivate();
373 377 }
374 378 }
375 379
376 380 };
377 381
378 382 this.updateReviewers = function (repo_name, pull_request_id) {
379 383 var postData = $('#reviewers input').serialize();
380 384 _updatePullRequest(repo_name, pull_request_id, postData);
381 385 };
382 386
383 387 this.handleDiffData = function (data) {
384 388 self.diffDataHandler(data)
385 389 }
386 390 };
387 391
388 392
389 393 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
390 394 var url = pyroutes.url(
391 395 'pullrequest_update',
392 396 {"repo_name": repo_name, "pull_request_id": pull_request_id});
393 397 if (typeof postData === 'string' ) {
394 398 postData += '&csrf_token=' + CSRF_TOKEN;
395 399 } else {
396 400 postData.csrf_token = CSRF_TOKEN;
397 401 }
398 402
399 403 var success = function(o) {
400 404 var redirectUrl = o['redirect_url'];
401 405 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
402 406 window.location = redirectUrl;
403 407 } else {
404 408 window.location.reload();
405 409 }
406 410 };
407 411
408 412 ajaxPOST(url, postData, success);
409 413 };
410 414
411 415 /**
412 416 * PULL REQUEST update commits
413 417 */
414 418 var updateCommits = function(repo_name, pull_request_id, force) {
415 419 var postData = {
416 420 'update_commits': true
417 421 };
418 422 if (force !== undefined && force === true) {
419 423 postData['force_refresh'] = true
420 424 }
421 425 _updatePullRequest(repo_name, pull_request_id, postData);
422 426 };
423 427
424 428
425 429 /**
426 430 * PULL REQUEST edit info
427 431 */
428 432 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
429 433 var url = pyroutes.url(
430 434 'pullrequest_update',
431 435 {"repo_name": repo_name, "pull_request_id": pull_request_id});
432 436
433 437 var postData = {
434 438 'title': title,
435 439 'description': description,
436 440 'description_renderer': renderer,
437 441 'edit_pull_request': true,
438 442 'csrf_token': CSRF_TOKEN
439 443 };
440 444 var success = function(o) {
441 445 window.location.reload();
442 446 };
443 447 ajaxPOST(url, postData, success);
444 448 };
445 449
446 450
447 451 /**
448 452 * Reviewer autocomplete
449 453 */
450 454 var ReviewerAutoComplete = function(inputId) {
451 455 $(inputId).autocomplete({
452 456 serviceUrl: pyroutes.url('user_autocomplete_data'),
453 457 minChars:2,
454 458 maxHeight:400,
455 459 deferRequestBy: 300, //miliseconds
456 460 showNoSuggestionNotice: true,
457 461 tabDisabled: true,
458 462 autoSelectFirst: true,
459 463 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
460 464 formatResult: autocompleteFormatResult,
461 465 lookupFilter: autocompleteFilterResult,
462 466 onSelect: function(element, data) {
463 467 var mandatory = false;
464 468 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
465 469
466 470 // add whole user groups
467 471 if (data.value_type == 'user_group') {
468 472 reasons.push(_gettext('member of "{0}"').format(data.value_display));
469 473
470 474 $.each(data.members, function(index, member_data) {
471 475 var reviewer = member_data;
472 476 reviewer['user_id'] = member_data['id'];
473 477 reviewer['gravatar_link'] = member_data['icon_link'];
474 478 reviewer['user_link'] = member_data['profile_link'];
475 479 reviewer['rules'] = [];
476 480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
477 481 })
478 482 }
479 483 // add single user
480 484 else {
481 485 var reviewer = data;
482 486 reviewer['user_id'] = data['id'];
483 487 reviewer['gravatar_link'] = data['icon_link'];
484 488 reviewer['user_link'] = data['profile_link'];
485 489 reviewer['rules'] = [];
486 490 reviewersController.addReviewMember(reviewer, reasons, mandatory);
487 491 }
488 492
489 493 $(inputId).val('');
490 494 }
491 495 });
492 496 };
493 497
494 498
495 VersionController = function () {
499 window.VersionController = function () {
496 500 var self = this;
497 501 this.$verSource = $('input[name=ver_source]');
498 502 this.$verTarget = $('input[name=ver_target]');
499 503 this.$showVersionDiff = $('#show-version-diff');
500 504
501 505 this.adjustRadioSelectors = function (curNode) {
502 506 var getVal = function (item) {
503 507 if (item == 'latest') {
504 508 return Number.MAX_SAFE_INTEGER
505 509 }
506 510 else {
507 511 return parseInt(item)
508 512 }
509 513 };
510 514
511 515 var curVal = getVal($(curNode).val());
512 516 var cleared = false;
513 517
514 518 $.each(self.$verSource, function (index, value) {
515 519 var elVal = getVal($(value).val());
516 520
517 521 if (elVal > curVal) {
518 522 if ($(value).is(':checked')) {
519 523 cleared = true;
520 524 }
521 525 $(value).attr('disabled', 'disabled');
522 526 $(value).removeAttr('checked');
523 527 $(value).css({'opacity': 0.1});
524 528 }
525 529 else {
526 530 $(value).css({'opacity': 1});
527 531 $(value).removeAttr('disabled');
528 532 }
529 533 });
530 534
531 535 if (cleared) {
532 536 // if we unchecked an active, set the next one to same loc.
533 537 $(this.$verSource).filter('[value={0}]'.format(
534 538 curVal)).attr('checked', 'checked');
535 539 }
536 540
537 541 self.setLockAction(false,
538 542 $(curNode).data('verPos'),
539 543 $(this.$verSource).filter(':checked').data('verPos')
540 544 );
541 545 };
542 546
543 547
544 548 this.attachVersionListener = function () {
545 549 self.$verTarget.change(function (e) {
546 550 self.adjustRadioSelectors(this)
547 551 });
548 552 self.$verSource.change(function (e) {
549 553 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
550 554 });
551 555 };
552 556
553 557 this.init = function () {
554 558
555 559 var curNode = self.$verTarget.filter(':checked');
556 560 self.adjustRadioSelectors(curNode);
557 561 self.setLockAction(true);
558 562 self.attachVersionListener();
559 563
560 564 };
561 565
562 566 this.setLockAction = function (state, selectedVersion, otherVersion) {
563 567 var $showVersionDiff = this.$showVersionDiff;
564 568
565 569 if (state) {
566 570 $showVersionDiff.attr('disabled', 'disabled');
567 571 $showVersionDiff.addClass('disabled');
568 572 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
569 573 }
570 574 else {
571 575 $showVersionDiff.removeAttr('disabled');
572 576 $showVersionDiff.removeClass('disabled');
573 577
574 578 if (selectedVersion == otherVersion) {
575 579 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
576 580 } else {
577 581 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
578 582 }
579 583 }
580 584
581 585 };
582 586
583 587 this.showVersionDiff = function () {
584 588 var target = self.$verTarget.filter(':checked');
585 589 var source = self.$verSource.filter(':checked');
586 590
587 591 if (target.val() && source.val()) {
588 592 var params = {
589 593 'pull_request_id': templateContext.pull_request_data.pull_request_id,
590 594 'repo_name': templateContext.repo_name,
591 595 'version': target.val(),
592 596 'from_version': source.val()
593 597 };
594 598 window.location = pyroutes.url('pullrequest_show', params)
595 599 }
596 600
597 601 return false;
598 602 };
599 603
600 604 this.toggleVersionView = function (elem) {
601 605
602 606 if (this.$showVersionDiff.is(':visible')) {
603 607 $('.version-pr').hide();
604 608 this.$showVersionDiff.hide();
605 609 $(elem).html($(elem).data('toggleOn'))
606 610 } else {
607 611 $('.version-pr').show();
608 612 this.$showVersionDiff.show();
609 613 $(elem).html($(elem).data('toggleOff'))
610 614 }
611 615
612 616 return false
613 617 };
614 618
615 this.toggleElement = function (elem, target) {
616 var $elem = $(elem);
617 var $target = $(target);
618
619 if ($target.is(':visible') || $target.length === 0) {
620 $target.hide();
621 $elem.html($elem.data('toggleOn'))
622 } else {
623 $target.show();
624 $elem.html($elem.data('toggleOff'))
625 }
626
627 return false
628 }
629
630 619 };
631 620
632 621
633 UpdatePrController = function () {
622 window.UpdatePrController = function () {
634 623 var self = this;
635 624 this.$updateCommits = $('#update_commits');
636 625 this.$updateCommitsSwitcher = $('#update_commits_switcher');
637 626
638 627 this.lockUpdateButton = function (label) {
639 628 self.$updateCommits.attr('disabled', 'disabled');
640 629 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
641 630
642 631 self.$updateCommits.addClass('disabled');
643 632 self.$updateCommitsSwitcher.addClass('disabled');
644 633
645 634 self.$updateCommits.removeClass('btn-primary');
646 635 self.$updateCommitsSwitcher.removeClass('btn-primary');
647 636
648 637 self.$updateCommits.text(_gettext(label));
649 638 };
650 639
651 640 this.isUpdateLocked = function () {
652 641 return self.$updateCommits.attr('disabled') !== undefined;
653 642 };
654 643
655 644 this.updateCommits = function (curNode) {
656 645 if (self.isUpdateLocked()) {
657 646 return
658 647 }
659 648 self.lockUpdateButton(_gettext('Updating...'));
660 649 updateCommits(
661 650 templateContext.repo_name,
662 651 templateContext.pull_request_data.pull_request_id);
663 652 };
664 653
665 654 this.forceUpdateCommits = function () {
666 655 if (self.isUpdateLocked()) {
667 656 return
668 657 }
669 658 self.lockUpdateButton(_gettext('Force updating...'));
670 659 var force = true;
671 660 updateCommits(
672 661 templateContext.repo_name,
673 662 templateContext.pull_request_data.pull_request_id, force);
674 663 };
675 }; No newline at end of file
664 };
665
666 /**
667 * Reviewer display panel
668 */
669 window.ReviewersPanel = {
670 editButton: null,
671 closeButton: null,
672 addButton: null,
673 removeButtons: null,
674 reviewRules: null,
675 setReviewers: null,
676
677 setSelectors: function () {
678 var self = this;
679 self.editButton = $('#open_edit_reviewers');
680 self.closeButton =$('#close_edit_reviewers');
681 self.addButton = $('#add_reviewer');
682 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
683 },
684
685 init: function (reviewRules, setReviewers) {
686 var self = this;
687 self.setSelectors();
688
689 this.reviewRules = reviewRules;
690 this.setReviewers = setReviewers;
691
692 this.editButton.on('click', function (e) {
693 self.edit();
694 });
695 this.closeButton.on('click', function (e) {
696 self.close();
697 self.renderReviewers();
698 });
699
700 self.renderReviewers();
701
702 },
703
704 renderReviewers: function () {
705
706 $('#review_members').html('')
707 $.each(this.setReviewers.reviewers, function (key, val) {
708 var member = val;
709
710 var entry = renderTemplate('reviewMemberEntry', {
711 'member': member,
712 'mandatory': member.mandatory,
713 'reasons': member.reasons,
714 'allowed_to_update': member.allowed_to_update,
715 'review_status': member.review_status,
716 'review_status_label': member.review_status_label,
717 'user_group': member.user_group,
718 'create': false
719 });
720
721 $('#review_members').append(entry)
722 });
723 tooltipActivate();
724
725 },
726
727 edit: function (event) {
728 this.editButton.hide();
729 this.closeButton.show();
730 this.addButton.show();
731 $(this.removeButtons.selector).css('visibility', 'visible');
732 // review rules
733 reviewersController.loadReviewRules(this.reviewRules);
734 },
735
736 close: function (event) {
737 this.editButton.show();
738 this.closeButton.hide();
739 this.addButton.hide();
740 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
742 reviewersController.hideReviewRules()
743 }
744 };
745
746
747 /**
748 * OnLine presence using channelstream
749 */
750 window.ReviewerPresenceController = function (channel) {
751 var self = this;
752 this.channel = channel;
753 this.users = {};
754
755 this.storeUsers = function (users) {
756 self.users = {}
757 $.each(users, function (index, value) {
758 var userId = value.state.id;
759 self.users[userId] = value.state;
760 })
761 }
762
763 this.render = function () {
764 $.each($('.reviewer_entry'), function (index, value) {
765 var userData = $(value).data();
766 if (self.users[userData.reviewerUserId] !== undefined) {
767 $(value).find('.presence-state').show();
768 } else {
769 $(value).find('.presence-state').hide();
770 }
771 })
772 };
773
774 this.handlePresence = function (data) {
775 if (data.type == 'presence' && data.channel === self.channel) {
776 this.storeUsers(data.users);
777 this.render()
778 }
779 };
780
781 this.handleChannelUpdate = function (data) {
782 if (data.channel === this.channel) {
783 this.storeUsers(data.state.users);
784 this.render()
785 }
786
787 };
788
789 /* subscribe to the current presence */
790 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
791 /* subscribe to updates e.g connect/disconnect */
792 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
793
794 };
795
796 window.refreshComments = function (version) {
797 version = version || templateContext.pull_request_data.pull_request_version || '';
798
799 // Pull request case
800 if (templateContext.pull_request_data.pull_request_id !== null) {
801 var params = {
802 'pull_request_id': templateContext.pull_request_data.pull_request_id,
803 'repo_name': templateContext.repo_name,
804 'version': version,
805 };
806 var loadUrl = pyroutes.url('pullrequest_comments', params);
807 } // commit case
808 else {
809 return
810 }
811
812 var currentIDs = []
813 $.each($('.comment'), function (idx, element) {
814 currentIDs.push($(element).data('commentId'));
815 });
816 var data = {"comments[]": currentIDs};
817
818 var $targetElem = $('.comments-content-table');
819 $targetElem.css('opacity', 0.3);
820 $targetElem.load(
821 loadUrl, data, function (responseText, textStatus, jqXHR) {
822 if (jqXHR.status !== 200) {
823 return false;
824 }
825 var $counterElem = $('#comments-count');
826 var newCount = $(responseText).data('counter');
827 if (newCount !== undefined) {
828 var callback = function () {
829 $counterElem.animate({'opacity': 1.00}, 200)
830 $counterElem.html(newCount);
831 };
832 $counterElem.animate({'opacity': 0.15}, 200, callback);
833 }
834
835 $targetElem.css('opacity', 1);
836 tooltipActivate();
837 }
838 );
839 }
840
841 window.refreshTODOs = function (version) {
842 version = version || templateContext.pull_request_data.pull_request_version || '';
843 // Pull request case
844 if (templateContext.pull_request_data.pull_request_id !== null) {
845 var params = {
846 'pull_request_id': templateContext.pull_request_data.pull_request_id,
847 'repo_name': templateContext.repo_name,
848 'version': version,
849 };
850 var loadUrl = pyroutes.url('pullrequest_comments', params);
851 } // commit case
852 else {
853 return
854 }
855
856 var currentIDs = []
857 $.each($('.comment'), function (idx, element) {
858 currentIDs.push($(element).data('commentId'));
859 });
860
861 var data = {"comments[]": currentIDs};
862 var $targetElem = $('.todos-content-table');
863 $targetElem.css('opacity', 0.3);
864 $targetElem.load(
865 loadUrl, data, function (responseText, textStatus, jqXHR) {
866 if (jqXHR.status !== 200) {
867 return false;
868 }
869 var $counterElem = $('#todos-count')
870 var newCount = $(responseText).data('counter');
871 if (newCount !== undefined) {
872 var callback = function () {
873 $counterElem.animate({'opacity': 1.00}, 200)
874 $counterElem.html(newCount);
875 };
876 $counterElem.animate({'opacity': 0.15}, 200, callback);
877 }
878
879 $targetElem.css('opacity', 1);
880 tooltipActivate();
881 }
882 );
883 }
884
885 window.refreshAllComments = function (version) {
886 version = version || templateContext.pull_request_data.pull_request_version || '';
887
888 refreshComments(version);
889 refreshTODOs(version);
890 };
@@ -1,1223 +1,1254 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 4 from rhodecode.lib import html_filters
5 5 %>
6 6
7 7 <%inherit file="root.mako"/>
8 8
9 9 <%include file="/ejs_templates/templates.html"/>
10 10
11 11 <div class="outerwrapper">
12 12 <!-- HEADER -->
13 13 <div class="header">
14 14 <div id="header-inner" class="wrapper">
15 15 <div id="logo">
16 16 <div class="logo-wrapper">
17 17 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
18 18 </div>
19 19 % if c.rhodecode_name:
20 20 <div class="branding">
21 21 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
22 22 </div>
23 23 % endif
24 24 </div>
25 25 <!-- MENU BAR NAV -->
26 26 ${self.menu_bar_nav()}
27 27 <!-- END MENU BAR NAV -->
28 28 </div>
29 29 </div>
30 30 ${self.menu_bar_subnav()}
31 31 <!-- END HEADER -->
32 32
33 33 <!-- CONTENT -->
34 34 <div id="content" class="wrapper">
35 35
36 36 <rhodecode-toast id="notifications"></rhodecode-toast>
37 37
38 38 <div class="main">
39 39 ${next.main()}
40 40 </div>
41 41
42 42 </div>
43 43 <!-- END CONTENT -->
44 44
45 45 </div>
46 46
47 47 <!-- FOOTER -->
48 48 <div id="footer">
49 49 <div id="footer-inner" class="title wrapper">
50 50 <div>
51 51 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
52 52
53 53 <p class="footer-link-right">
54 54 <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}">
55 55 RhodeCode
56 56 % if c.visual.show_version:
57 57 ${c.rhodecode_version}
58 58 % endif
59 59 ${c.rhodecode_edition}
60 60 </a> |
61 61
62 62 % if c.visual.rhodecode_support_url:
63 63 <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> |
64 64 <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a>
65 65 % endif
66 66
67 67 </p>
68 68
69 69 <p class="server-instance" style="display:${sid}">
70 70 ## display hidden instance ID if specially defined
71 71 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
72 72 % if c.rhodecode_instanceid:
73 73 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
74 74 % endif
75 75 </p>
76 76 </div>
77 77 </div>
78 78 </div>
79 79
80 80 <!-- END FOOTER -->
81 81
82 82 ### MAKO DEFS ###
83 83
84 84 <%def name="menu_bar_subnav()">
85 85 </%def>
86 86
87 87 <%def name="breadcrumbs(class_='breadcrumbs')">
88 88 <div class="${class_}">
89 89 ${self.breadcrumbs_links()}
90 90 </div>
91 91 </%def>
92 92
93 93 <%def name="admin_menu(active=None)">
94 94
95 95 <div id="context-bar">
96 96 <div class="wrapper">
97 97 <div class="title">
98 98 <div class="title-content">
99 99 <div class="title-main">
100 100 % if c.is_super_admin:
101 101 ${_('Super-admin Panel')}
102 102 % else:
103 103 ${_('Delegated Admin Panel')}
104 104 % endif
105 105 </div>
106 106 </div>
107 107 </div>
108 108
109 109 <ul id="context-pages" class="navigation horizontal-list">
110 110
111 111 ## super-admin case
112 112 % if c.is_super_admin:
113 113 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
114 114 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
115 115 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
116 116 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
117 117 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
118 118 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
119 119 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
120 120 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
121 121 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
122 122 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
123 123
124 124 ## delegated admin
125 125 % elif c.is_delegated_admin:
126 126 <%
127 127 repositories=c.auth_user.repositories_admin or c.can_create_repo
128 128 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
129 129 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
130 130 %>
131 131
132 132 %if repositories:
133 133 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
134 134 %endif
135 135 %if repository_groups:
136 136 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
137 137 %endif
138 138 %if user_groups:
139 139 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
140 140 %endif
141 141 % endif
142 142 </ul>
143 143
144 144 </div>
145 145 <div class="clear"></div>
146 146 </div>
147 147 </%def>
148 148
149 149 <%def name="dt_info_panel(elements)">
150 150 <dl class="dl-horizontal">
151 151 %for dt, dd, title, show_items in elements:
152 152 <dt>${dt}:</dt>
153 153 <dd title="${h.tooltip(title)}">
154 154 %if callable(dd):
155 155 ## allow lazy evaluation of elements
156 156 ${dd()}
157 157 %else:
158 158 ${dd}
159 159 %endif
160 160 %if show_items:
161 161 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
162 162 %endif
163 163 </dd>
164 164
165 165 %if show_items:
166 166 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
167 167 %for item in show_items:
168 168 <dt></dt>
169 169 <dd>${item}</dd>
170 170 %endfor
171 171 </div>
172 172 %endif
173 173
174 174 %endfor
175 175 </dl>
176 176 </%def>
177 177
178 178 <%def name="tr_info_entry(element)">
179 179 <% key, val, title, show_items = element %>
180 180
181 181 <tr>
182 182 <td style="vertical-align: top">${key}</td>
183 183 <td title="${h.tooltip(title)}">
184 184 %if callable(val):
185 185 ## allow lazy evaluation of elements
186 186 ${val()}
187 187 %else:
188 188 ${val}
189 189 %endif
190 190 %if show_items:
191 191 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
192 192 % for item in show_items:
193 193 <dt></dt>
194 194 <dd>${item}</dd>
195 195 % endfor
196 196 </div>
197 197 %endif
198 198 </td>
199 199 <td style="vertical-align: top">
200 200 %if show_items:
201 201 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
202 202 %endif
203 203 </td>
204 204 </tr>
205 205
206 206 </%def>
207 207
208 208 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
209 209 <%
210 210 if size > 16:
211 211 gravatar_class = ['gravatar','gravatar-large']
212 212 else:
213 213 gravatar_class = ['gravatar']
214 214
215 215 data_hovercard_url = ''
216 216 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
217 217
218 218 if tooltip:
219 219 gravatar_class += ['tooltip-hovercard']
220 220 if extra_class:
221 221 gravatar_class += extra_class
222 222 if tooltip and user:
223 223 if user.username == h.DEFAULT_USER:
224 224 gravatar_class.pop(-1)
225 225 else:
226 226 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
227 227 gravatar_class = ' '.join(gravatar_class)
228 228
229 229 %>
230 230 <%doc>
231 231 TODO: johbo: For now we serve double size images to make it smooth
232 232 for retina. This is how it worked until now. Should be replaced
233 233 with a better solution at some point.
234 234 </%doc>
235 235
236 236 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
237 237 </%def>
238 238
239 239
240 240 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
241 241 <%
242 242 email = h.email_or_none(contact)
243 243 rc_user = h.discover_user(contact)
244 244 %>
245 245
246 246 <div class="${_class}">
247 247 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
248 248 <span class="${('user user-disabled' if show_disabled else 'user')}">
249 249 ${h.link_to_user(rc_user or contact)}
250 250 </span>
251 251 </div>
252 252 </%def>
253 253
254 254
255 255 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
256 256 <%
257 257 if (size > 16):
258 258 gravatar_class = 'icon-user-group-alt'
259 259 else:
260 260 gravatar_class = 'icon-user-group-alt'
261 261
262 262 if tooltip:
263 263 gravatar_class += ' tooltip-hovercard'
264 264
265 265 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
266 266 %>
267 267 <%doc>
268 268 TODO: johbo: For now we serve double size images to make it smooth
269 269 for retina. This is how it worked until now. Should be replaced
270 270 with a better solution at some point.
271 271 </%doc>
272 272
273 273 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
274 274 </%def>
275 275
276 276 <%def name="repo_page_title(repo_instance)">
277 277 <div class="title-content repo-title">
278 278
279 279 <div class="title-main">
280 280 ## SVN/HG/GIT icons
281 281 %if h.is_hg(repo_instance):
282 282 <i class="icon-hg"></i>
283 283 %endif
284 284 %if h.is_git(repo_instance):
285 285 <i class="icon-git"></i>
286 286 %endif
287 287 %if h.is_svn(repo_instance):
288 288 <i class="icon-svn"></i>
289 289 %endif
290 290
291 291 ## public/private
292 292 %if repo_instance.private:
293 293 <i class="icon-repo-private"></i>
294 294 %else:
295 295 <i class="icon-repo-public"></i>
296 296 %endif
297 297
298 298 ## repo name with group name
299 299 ${h.breadcrumb_repo_link(repo_instance)}
300 300
301 301 ## Context Actions
302 302 <div class="pull-right">
303 303 %if c.rhodecode_user.username != h.DEFAULT_USER:
304 304 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
305 305
306 306 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
307 307 % if c.repository_is_user_following:
308 308 <i class="icon-eye-off"></i>${_('Unwatch')}
309 309 % else:
310 310 <i class="icon-eye"></i>${_('Watch')}
311 311 % endif
312 312
313 313 </a>
314 314 %else:
315 315 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
316 316 %endif
317 317 </div>
318 318
319 319 </div>
320 320
321 321 ## FORKED
322 322 %if repo_instance.fork:
323 323 <p class="discreet">
324 324 <i class="icon-code-fork"></i> ${_('Fork of')}
325 325 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
326 326 </p>
327 327 %endif
328 328
329 329 ## IMPORTED FROM REMOTE
330 330 %if repo_instance.clone_uri:
331 331 <p class="discreet">
332 332 <i class="icon-code-fork"></i> ${_('Clone from')}
333 333 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
334 334 </p>
335 335 %endif
336 336
337 337 ## LOCKING STATUS
338 338 %if repo_instance.locked[0]:
339 339 <p class="locking_locked discreet">
340 340 <i class="icon-repo-lock"></i>
341 341 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
342 342 </p>
343 343 %elif repo_instance.enable_locking:
344 344 <p class="locking_unlocked discreet">
345 345 <i class="icon-repo-unlock"></i>
346 346 ${_('Repository not locked. Pull repository to lock it.')}
347 347 </p>
348 348 %endif
349 349
350 350 </div>
351 351 </%def>
352 352
353 353 <%def name="repo_menu(active=None)">
354 354 <%
355 355 ## determine if we have "any" option available
356 356 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
357 357 has_actions = can_lock
358 358
359 359 %>
360 360 % if c.rhodecode_db_repo.archived:
361 361 <div class="alert alert-warning text-center">
362 362 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
363 363 </div>
364 364 % endif
365 365
366 366 <!--- REPO CONTEXT BAR -->
367 367 <div id="context-bar">
368 368 <div class="wrapper">
369 369
370 370 <div class="title">
371 371 ${self.repo_page_title(c.rhodecode_db_repo)}
372 372 </div>
373 373
374 374 <ul id="context-pages" class="navigation horizontal-list">
375 375 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
376 376 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
377 377 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li>
378 378 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
379 379
380 380 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
381 381 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
382 382 <li class="${h.is_active('showpullrequest', active)}">
383 383 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
384 384 <div class="menulabel">
385 385 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
386 386 </div>
387 387 </a>
388 388 </li>
389 389 %endif
390 390
391 391 <li class="${h.is_active('artifacts', active)}">
392 392 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
393 393 <div class="menulabel">
394 394 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
395 395 </div>
396 396 </a>
397 397 </li>
398 398
399 399 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
400 400 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
401 401 %endif
402 402
403 403 <li class="${h.is_active('options', active)}">
404 404 % if has_actions:
405 405 <a class="menulink dropdown">
406 406 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
407 407 </a>
408 408 <ul class="submenu">
409 409 %if can_lock:
410 410 %if c.rhodecode_db_repo.locked[0]:
411 411 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
412 412 %else:
413 413 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
414 414 %endif
415 415 %endif
416 416 </ul>
417 417 % endif
418 418 </li>
419 419
420 420 </ul>
421 421 </div>
422 422 <div class="clear"></div>
423 423 </div>
424 424
425 425 <!--- REPO END CONTEXT BAR -->
426 426
427 427 </%def>
428 428
429 429 <%def name="repo_group_page_title(repo_group_instance)">
430 430 <div class="title-content">
431 431 <div class="title-main">
432 432 ## Repository Group icon
433 433 <i class="icon-repo-group"></i>
434 434
435 435 ## repo name with group name
436 436 ${h.breadcrumb_repo_group_link(repo_group_instance)}
437 437 </div>
438 438
439 439 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
440 440 <div class="repo-group-desc discreet">
441 441 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
442 442 </div>
443 443
444 444 </div>
445 445 </%def>
446 446
447 447
448 448 <%def name="repo_group_menu(active=None)">
449 449 <%
450 450 gr_name = c.repo_group.group_name if c.repo_group else None
451 451 # create repositories with write permission on group is set to true
452 452 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
453 453
454 454 %>
455 455
456 456
457 457 <!--- REPO GROUP CONTEXT BAR -->
458 458 <div id="context-bar">
459 459 <div class="wrapper">
460 460 <div class="title">
461 461 ${self.repo_group_page_title(c.repo_group)}
462 462 </div>
463 463
464 464 <ul id="context-pages" class="navigation horizontal-list">
465 465 <li class="${h.is_active('home', active)}">
466 466 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
467 467 </li>
468 468 % if c.is_super_admin or group_admin:
469 469 <li class="${h.is_active('settings', active)}">
470 470 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
471 471 </li>
472 472 % endif
473 473
474 474 </ul>
475 475 </div>
476 476 <div class="clear"></div>
477 477 </div>
478 478
479 479 <!--- REPO GROUP CONTEXT BAR -->
480 480
481 481 </%def>
482 482
483 483
484 484 <%def name="usermenu(active=False)">
485 485 <%
486 486 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
487 487
488 488 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
489 489 # create repositories with write permission on group is set to true
490 490
491 491 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
492 492 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
493 493 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
494 494 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
495 495
496 496 can_create_repos = c.is_super_admin or c.can_create_repo
497 497 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
498 498
499 499 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
500 500 can_create_repo_groups_in_group = c.is_super_admin or group_admin
501 501 %>
502 502
503 503 % if not_anonymous:
504 504 <%
505 505 default_target_group = dict()
506 506 if c.rhodecode_user.personal_repo_group:
507 507 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
508 508 %>
509 509
510 510 ## create action
511 511 <li>
512 512 <a href="#create-actions" onclick="return false;" class="menulink childs">
513 513 <i class="icon-plus-circled"></i>
514 514 </a>
515 515
516 516 <div class="action-menu submenu">
517 517
518 518 <ol>
519 519 ## scope of within a repository
520 520 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
521 521 <li class="submenu-title">${_('This Repository')}</li>
522 522 <li>
523 523 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
524 524 </li>
525 525 % if can_fork:
526 526 <li>
527 527 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
528 528 </li>
529 529 % endif
530 530 % endif
531 531
532 532 ## scope of within repository groups
533 533 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
534 534 <li class="submenu-title">${_('This Repository Group')}</li>
535 535
536 536 % if can_create_repos_in_group:
537 537 <li>
538 538 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
539 539 </li>
540 540 % endif
541 541
542 542 % if can_create_repo_groups_in_group:
543 543 <li>
544 544 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
545 545 </li>
546 546 % endif
547 547 % endif
548 548
549 549 ## personal group
550 550 % if c.rhodecode_user.personal_repo_group:
551 551 <li class="submenu-title">Personal Group</li>
552 552
553 553 <li>
554 554 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
555 555 </li>
556 556
557 557 <li>
558 558 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
559 559 </li>
560 560 % endif
561 561
562 562 ## Global actions
563 563 <li class="submenu-title">RhodeCode</li>
564 564 % if can_create_repos:
565 565 <li>
566 566 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
567 567 </li>
568 568 % endif
569 569
570 570 % if can_create_repo_groups:
571 571 <li>
572 572 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
573 573 </li>
574 574 % endif
575 575
576 576 <li>
577 577 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
578 578 </li>
579 579
580 580 </ol>
581 581
582 582 </div>
583 583 </li>
584 584
585 585 ## notifications
586 586 <li>
587 587 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
588 588 ${c.unread_notifications}
589 589 </a>
590 590 </li>
591 591 % endif
592 592
593 593 ## USER MENU
594 594 <li id="quick_login_li" class="${'active' if active else ''}">
595 595 % if c.rhodecode_user.username == h.DEFAULT_USER:
596 596 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
597 597 ${gravatar(c.rhodecode_user.email, 20)}
598 598 <span class="user">
599 599 <span>${_('Sign in')}</span>
600 600 </span>
601 601 </a>
602 602 % else:
603 603 ## logged in user
604 604 <a id="quick_login_link" class="menulink childs">
605 605 ${gravatar(c.rhodecode_user.email, 20)}
606 606 <span class="user">
607 607 <span class="menu_link_user">${c.rhodecode_user.username}</span>
608 608 <div class="show_more"></div>
609 609 </span>
610 610 </a>
611 611 ## subnav with menu for logged in user
612 612 <div class="user-menu submenu">
613 613 <div id="quick_login">
614 614 %if c.rhodecode_user.username != h.DEFAULT_USER:
615 615 <div class="">
616 616 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
617 617 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
618 618 <div class="email">${c.rhodecode_user.email}</div>
619 619 </div>
620 620 <div class="">
621 621 <ol class="links">
622 622 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
623 623 % if c.rhodecode_user.personal_repo_group:
624 624 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
625 625 % endif
626 626 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
627 627
628 628 % if c.debug_style:
629 629 <li>
630 630 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
631 631 <div class="menulabel">${_('[Style]')}</div>
632 632 </a>
633 633 </li>
634 634 % endif
635 635
636 636 ## bookmark-items
637 637 <li class="bookmark-items">
638 638 ${_('Bookmarks')}
639 639 <div class="pull-right">
640 640 <a href="${h.route_path('my_account_bookmarks')}">
641 641
642 642 <i class="icon-cog"></i>
643 643 </a>
644 644 </div>
645 645 </li>
646 646 % if not c.bookmark_items:
647 647 <li>
648 648 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
649 649 </li>
650 650 % endif
651 651 % for item in c.bookmark_items:
652 652 <li>
653 653 % if item.repository:
654 654 <div>
655 655 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
656 656 <code>${item.position}</code>
657 657 % if item.repository.repo_type == 'hg':
658 658 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
659 659 % elif item.repository.repo_type == 'git':
660 660 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
661 661 % elif item.repository.repo_type == 'svn':
662 662 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
663 663 % endif
664 664 ${(item.title or h.shorter(item.repository.repo_name, 30))}
665 665 </a>
666 666 </div>
667 667 % elif item.repository_group:
668 668 <div>
669 669 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
670 670 <code>${item.position}</code>
671 671 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
672 672 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
673 673 </a>
674 674 </div>
675 675 % else:
676 676 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
677 677 <code>${item.position}</code>
678 678 ${item.title}
679 679 </a>
680 680 % endif
681 681 </li>
682 682 % endfor
683 683
684 684 <li class="logout">
685 685 ${h.secure_form(h.route_path('logout'), request=request)}
686 686 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
687 687 ${h.end_form()}
688 688 </li>
689 689 </ol>
690 690 </div>
691 691 %endif
692 692 </div>
693 693 </div>
694 694
695 695 % endif
696 696 </li>
697 697 </%def>
698 698
699 699 <%def name="menu_items(active=None)">
700 700 <%
701 701 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
702 702 notice_display = 'none' if len(notice_messages) == 0 else ''
703 703 %>
704 <style>
705
706 </style>
707 704
708 705 <ul id="quick" class="main_nav navigation horizontal-list">
709 706 ## notice box for important system messages
710 707 <li style="display: ${notice_display}">
711 708 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
712 709 <div class="menulabel-notice ${notice_level}" >
713 710 ${len(notice_messages)}
714 711 </div>
715 712 </a>
716 713 </li>
717 714 <div class="notice-messages-container" style="display: none">
718 715 <div class="notice-messages">
719 716 <table class="rctable">
720 717 % for notice in notice_messages:
721 718 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
722 719 <td style="vertical-align: text-top; width: 20px">
723 720 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
724 721 </td>
725 722 <td>
726 723 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
727 724 ${notice['subject']}
728 725
729 726 <div id="notice-${notice['msg_id']}" style="display: none">
730 727 ${h.render(notice['body'], renderer='markdown')}
731 728 </div>
732 729 </td>
733 730 <td style="vertical-align: text-top; width: 35px;">
734 731 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
735 732 <i class="icon-remove icon-filled-red"></i>
736 733 </a>
737 734 </td>
738 735 </tr>
739 736
740 737 % endfor
741 738 </table>
742 739 </div>
743 740 </div>
744 741 ## Main filter
745 742 <li>
746 743 <div class="menulabel main_filter_box">
747 744 <div class="main_filter_input_box">
748 745 <ul class="searchItems">
749 746
750 747 <li class="searchTag searchTagIcon">
751 748 <i class="icon-search"></i>
752 749 </li>
753 750
754 751 % if c.template_context['search_context']['repo_id']:
755 752 <li class="searchTag searchTagFilter searchTagHidable" >
756 753 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
757 754 <span class="tag">
758 755 This repo
759 756 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
760 757 </span>
761 758 ##</a>
762 759 </li>
763 760 % elif c.template_context['search_context']['repo_group_id']:
764 761 <li class="searchTag searchTagFilter searchTagHidable">
765 762 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
766 763 <span class="tag">
767 764 This group
768 765 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
769 766 </span>
770 767 ##</a>
771 768 </li>
772 769 % endif
773 770
774 771 <li class="searchTagInput">
775 772 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
776 773 </li>
777 774 <li class="searchTag searchTagHelp">
778 775 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
779 776 </li>
780 777 </ul>
781 778 </div>
782 779 </div>
783 780
784 781 <div id="main_filter_help" style="display: none">
785 782 - Use '/' key to quickly access this field.
786 783
787 784 - Enter a name of repository, or repository group for quick search.
788 785
789 786 - Prefix query to allow special search:
790 787
791 788 user:admin, to search for usernames, always global
792 789
793 790 user_group:devops, to search for user groups, always global
794 791
795 792 pr:303, to search for pull request number, title, or description, always global
796 793
797 794 commit:efced4, to search for commits, scoped to repositories or groups
798 795
799 796 file:models.py, to search for file paths, scoped to repositories or groups
800 797
801 798 % if c.template_context['search_context']['repo_id']:
802 799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
803 800 % elif c.template_context['search_context']['repo_group_id']:
804 801 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
805 802 % else:
806 803 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
807 804 % endif
808 805 </div>
809 806 </li>
810 807
811 808 ## ROOT MENU
812 809 <li class="${h.is_active('home', active)}">
813 810 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
814 811 <div class="menulabel">${_('Home')}</div>
815 812 </a>
816 813 </li>
817 814
818 815 %if c.rhodecode_user.username != h.DEFAULT_USER:
819 816 <li class="${h.is_active('journal', active)}">
820 817 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
821 818 <div class="menulabel">${_('Journal')}</div>
822 819 </a>
823 820 </li>
824 821 %else:
825 822 <li class="${h.is_active('journal', active)}">
826 823 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
827 824 <div class="menulabel">${_('Public journal')}</div>
828 825 </a>
829 826 </li>
830 827 %endif
831 828
832 829 <li class="${h.is_active('gists', active)}">
833 830 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
834 831 <div class="menulabel">${_('Gists')}</div>
835 832 </a>
836 833 </li>
837 834
838 835 % if c.is_super_admin or c.is_delegated_admin:
839 836 <li class="${h.is_active('admin', active)}">
840 837 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
841 838 <div class="menulabel">${_('Admin')} </div>
842 839 </a>
843 840 </li>
844 841 % endif
845 842
846 843 ## render extra user menu
847 844 ${usermenu(active=(active=='my_account'))}
848 845
849 846 </ul>
850 847
851 848 <script type="text/javascript">
852 849 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
853 850
854 851 var formatRepoResult = function(result, container, query, escapeMarkup) {
855 852 return function(data, escapeMarkup) {
856 853 if (!data.repo_id){
857 854 return data.text; // optgroup text Repositories
858 855 }
859 856
860 857 var tmpl = '';
861 858 var repoType = data['repo_type'];
862 859 var repoName = data['text'];
863 860
864 861 if(data && data.type == 'repo'){
865 862 if(repoType === 'hg'){
866 863 tmpl += '<i class="icon-hg"></i> ';
867 864 }
868 865 else if(repoType === 'git'){
869 866 tmpl += '<i class="icon-git"></i> ';
870 867 }
871 868 else if(repoType === 'svn'){
872 869 tmpl += '<i class="icon-svn"></i> ';
873 870 }
874 871 if(data['private']){
875 872 tmpl += '<i class="icon-lock" ></i> ';
876 873 }
877 874 else if(visualShowPublicIcon){
878 875 tmpl += '<i class="icon-unlock-alt"></i> ';
879 876 }
880 877 }
881 878 tmpl += escapeMarkup(repoName);
882 879 return tmpl;
883 880
884 881 }(result, escapeMarkup);
885 882 };
886 883
887 884 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
888 885 return function(data, escapeMarkup) {
889 886 if (!data.repo_group_id){
890 887 return data.text; // optgroup text Repositories
891 888 }
892 889
893 890 var tmpl = '';
894 891 var repoGroupName = data['text'];
895 892
896 893 if(data){
897 894
898 895 tmpl += '<i class="icon-repo-group"></i> ';
899 896
900 897 }
901 898 tmpl += escapeMarkup(repoGroupName);
902 899 return tmpl;
903 900
904 901 }(result, escapeMarkup);
905 902 };
906 903
907 904 var escapeRegExChars = function (value) {
908 905 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
909 906 };
910 907
911 908 var getRepoIcon = function(repo_type) {
912 909 if (repo_type === 'hg') {
913 910 return '<i class="icon-hg"></i> ';
914 911 }
915 912 else if (repo_type === 'git') {
916 913 return '<i class="icon-git"></i> ';
917 914 }
918 915 else if (repo_type === 'svn') {
919 916 return '<i class="icon-svn"></i> ';
920 917 }
921 918 return ''
922 919 };
923 920
924 921 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
925 922
926 923 if (value.split(':').length === 2) {
927 924 value = value.split(':')[1]
928 925 }
929 926
930 927 var searchType = data['type'];
931 928 var searchSubType = data['subtype'];
932 929 var valueDisplay = data['value_display'];
933 930 var valueIcon = data['value_icon'];
934 931
935 932 var pattern = '(' + escapeRegExChars(value) + ')';
936 933
937 934 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
938 935
939 936 // highlight match
940 937 if (searchType != 'text') {
941 938 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
942 939 }
943 940
944 941 var icon = '';
945 942
946 943 if (searchType === 'hint') {
947 944 icon += '<i class="icon-repo-group"></i> ';
948 945 }
949 946 // full text search/hints
950 947 else if (searchType === 'search') {
951 948 if (valueIcon === undefined) {
952 949 icon += '<i class="icon-more"></i> ';
953 950 } else {
954 951 icon += valueIcon + ' ';
955 952 }
956 953
957 954 if (searchSubType !== undefined && searchSubType == 'repo') {
958 955 valueDisplay += '<div class="pull-right tag">repository</div>';
959 956 }
960 957 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
961 958 valueDisplay += '<div class="pull-right tag">repo group</div>';
962 959 }
963 960 }
964 961 // repository
965 962 else if (searchType === 'repo') {
966 963
967 964 var repoIcon = getRepoIcon(data['repo_type']);
968 965 icon += repoIcon;
969 966
970 967 if (data['private']) {
971 968 icon += '<i class="icon-lock" ></i> ';
972 969 }
973 970 else if (visualShowPublicIcon) {
974 971 icon += '<i class="icon-unlock-alt"></i> ';
975 972 }
976 973 }
977 974 // repository groups
978 975 else if (searchType === 'repo_group') {
979 976 icon += '<i class="icon-repo-group"></i> ';
980 977 }
981 978 // user group
982 979 else if (searchType === 'user_group') {
983 980 icon += '<i class="icon-group"></i> ';
984 981 }
985 982 // user
986 983 else if (searchType === 'user') {
987 984 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
988 985 }
989 986 // pull request
990 987 else if (searchType === 'pull_request') {
991 988 icon += '<i class="icon-merge"></i> ';
992 989 }
993 990 // commit
994 991 else if (searchType === 'commit') {
995 992 var repo_data = data['repo_data'];
996 993 var repoIcon = getRepoIcon(repo_data['repository_type']);
997 994 if (repoIcon) {
998 995 icon += repoIcon;
999 996 } else {
1000 997 icon += '<i class="icon-tag"></i>';
1001 998 }
1002 999 }
1003 1000 // file
1004 1001 else if (searchType === 'file') {
1005 1002 var repo_data = data['repo_data'];
1006 1003 var repoIcon = getRepoIcon(repo_data['repository_type']);
1007 1004 if (repoIcon) {
1008 1005 icon += repoIcon;
1009 1006 } else {
1010 1007 icon += '<i class="icon-tag"></i>';
1011 1008 }
1012 1009 }
1013 1010 // generic text
1014 1011 else if (searchType === 'text') {
1015 1012 icon = '';
1016 1013 }
1017 1014
1018 1015 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1019 1016 return tmpl.format(icon, valueDisplay);
1020 1017 };
1021 1018
1022 1019 var handleSelect = function(element, suggestion) {
1023 1020 if (suggestion.type === "hint") {
1024 1021 // we skip action
1025 1022 $('#main_filter').focus();
1026 1023 }
1027 1024 else if (suggestion.type === "text") {
1028 1025 // we skip action
1029 1026 $('#main_filter').focus();
1030 1027
1031 1028 } else {
1032 1029 window.location = suggestion['url'];
1033 1030 }
1034 1031 };
1035 1032
1036 1033 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1037 1034 if (queryLowerCase.split(':').length === 2) {
1038 1035 queryLowerCase = queryLowerCase.split(':')[1]
1039 1036 }
1040 1037 if (suggestion.type === "text") {
1041 1038 // special case we don't want to "skip" display for
1042 1039 return true
1043 1040 }
1044 1041 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1045 1042 };
1046 1043
1047 1044 var cleanContext = {
1048 1045 repo_view_type: null,
1049 1046
1050 1047 repo_id: null,
1051 1048 repo_name: "",
1052 1049
1053 1050 repo_group_id: null,
1054 1051 repo_group_name: null
1055 1052 };
1056 1053 var removeGoToFilter = function () {
1057 1054 $('.searchTagHidable').hide();
1058 1055 $('#main_filter').autocomplete(
1059 1056 'setOptions', {params:{search_context: cleanContext}});
1060 1057 };
1061 1058
1062 1059 $('#main_filter').autocomplete({
1063 1060 serviceUrl: pyroutes.url('goto_switcher_data'),
1064 1061 params: {
1065 1062 "search_context": templateContext.search_context
1066 1063 },
1067 1064 minChars:2,
1068 1065 maxHeight:400,
1069 1066 deferRequestBy: 300, //miliseconds
1070 1067 tabDisabled: true,
1071 1068 autoSelectFirst: false,
1072 1069 containerClass: 'autocomplete-qfilter-suggestions',
1073 1070 formatResult: autocompleteMainFilterFormatResult,
1074 1071 lookupFilter: autocompleteMainFilterResult,
1075 1072 onSelect: function (element, suggestion) {
1076 1073 handleSelect(element, suggestion);
1077 1074 return false;
1078 1075 },
1079 1076 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1080 1077 if (jqXHR !== 'abort') {
1081 1078 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1082 1079 SwalNoAnimation.fire({
1083 1080 icon: 'error',
1084 1081 title: _gettext('Error during search operation'),
1085 1082 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1086 1083 }).then(function(result) {
1087 1084 window.location.reload();
1088 1085 })
1089 1086 }
1090 1087 },
1091 1088 onSearchStart: function (params) {
1092 1089 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1093 1090 },
1094 1091 onSearchComplete: function (query, suggestions) {
1095 1092 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1096 1093 },
1097 1094 });
1098 1095
1099 1096 showMainFilterBox = function () {
1100 1097 $('#main_filter_help').toggle();
1101 1098 };
1102 1099
1103 1100 $('#main_filter').on('keydown.autocomplete', function (e) {
1104 1101
1105 1102 var BACKSPACE = 8;
1106 1103 var el = $(e.currentTarget);
1107 1104 if(e.which === BACKSPACE){
1108 1105 var inputVal = el.val();
1109 1106 if (inputVal === ""){
1110 1107 removeGoToFilter()
1111 1108 }
1112 1109 }
1113 1110 });
1114 1111
1115 1112 var dismissNotice = function(noticeId) {
1116 1113
1117 1114 var url = pyroutes.url('user_notice_dismiss',
1118 1115 {"user_id": templateContext.rhodecode_user.user_id});
1119 1116
1120 1117 var postData = {
1121 1118 'csrf_token': CSRF_TOKEN,
1122 1119 'notice_id': noticeId,
1123 1120 };
1124 1121
1125 1122 var success = function(response) {
1126 1123 $('#notice-message-' + noticeId).remove();
1127 1124 return false;
1128 1125 };
1129 1126 var failure = function(data, textStatus, xhr) {
1130 1127 alert("error processing request: " + textStatus);
1131 1128 return false;
1132 1129 };
1133 1130 ajaxPOST(url, postData, success, failure);
1134 1131 }
1135 1132
1136 1133 var hideLicenseWarning = function () {
1137 1134 var fingerprint = templateContext.session_attrs.license_fingerprint;
1138 1135 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1139 1136 $('#notifications').hide();
1140 1137 }
1141 1138
1142 1139 var hideLicenseError = function () {
1143 1140 var fingerprint = templateContext.session_attrs.license_fingerprint;
1144 1141 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1145 1142 $('#notifications').hide();
1146 1143 }
1147 1144
1148 1145 </script>
1149 1146 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1150 1147 </%def>
1151 1148
1152 1149 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1153 1150 <div class="modal-dialog">
1154 1151 <div class="modal-content">
1155 1152 <div class="modal-header">
1156 1153 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1157 1154 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1158 1155 </div>
1159 1156 <div class="modal-body">
1160 1157 <div class="block-left">
1161 1158 <table class="keyboard-mappings">
1162 1159 <tbody>
1163 1160 <tr>
1164 1161 <th></th>
1165 1162 <th>${_('Site-wide shortcuts')}</th>
1166 1163 </tr>
1167 1164 <%
1168 1165 elems = [
1169 1166 ('/', 'Use quick search box'),
1170 1167 ('g h', 'Goto home page'),
1171 1168 ('g g', 'Goto my private gists page'),
1172 1169 ('g G', 'Goto my public gists page'),
1173 1170 ('g 0-9', 'Goto bookmarked items from 0-9'),
1174 1171 ('n r', 'New repository page'),
1175 1172 ('n g', 'New gist page'),
1176 1173 ]
1177 1174 %>
1178 1175 %for key, desc in elems:
1179 1176 <tr>
1180 1177 <td class="keys">
1181 1178 <span class="key tag">${key}</span>
1182 1179 </td>
1183 1180 <td>${desc}</td>
1184 1181 </tr>
1185 1182 %endfor
1186 1183 </tbody>
1187 1184 </table>
1188 1185 </div>
1189 1186 <div class="block-left">
1190 1187 <table class="keyboard-mappings">
1191 1188 <tbody>
1192 1189 <tr>
1193 1190 <th></th>
1194 1191 <th>${_('Repositories')}</th>
1195 1192 </tr>
1196 1193 <%
1197 1194 elems = [
1198 1195 ('g s', 'Goto summary page'),
1199 1196 ('g c', 'Goto changelog page'),
1200 1197 ('g f', 'Goto files page'),
1201 1198 ('g F', 'Goto files page with file search activated'),
1202 1199 ('g p', 'Goto pull requests page'),
1203 1200 ('g o', 'Goto repository settings'),
1204 1201 ('g O', 'Goto repository access permissions settings'),
1202 ('t s', 'Toggle sidebar on some pages'),
1205 1203 ]
1206 1204 %>
1207 1205 %for key, desc in elems:
1208 1206 <tr>
1209 1207 <td class="keys">
1210 1208 <span class="key tag">${key}</span>
1211 1209 </td>
1212 1210 <td>${desc}</td>
1213 1211 </tr>
1214 1212 %endfor
1215 1213 </tbody>
1216 1214 </table>
1217 1215 </div>
1218 1216 </div>
1219 1217 <div class="modal-footer">
1220 1218 </div>
1221 1219 </div><!-- /.modal-content -->
1222 1220 </div><!-- /.modal-dialog -->
1223 1221 </div><!-- /.modal -->
1222
1223
1224 <script type="text/javascript">
1225 (function () {
1226 "use sctrict";
1227
1228 var $sideBar = $('.right-sidebar');
1229 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1230 var sidebarState = templateContext.session_attrs.sidebarState;
1231 var sidebarEnabled = $('aside.right-sidebar').get(0);
1232
1233 if (sidebarState === 'expanded') {
1234 expanded = true
1235 } else if (sidebarState === 'collapsed') {
1236 expanded = false
1237 }
1238 if (sidebarEnabled) {
1239 // show sidebar since it's hidden on load
1240 $('.right-sidebar').show();
1241
1242 // init based on set initial class, or if defined user session attrs
1243 if (expanded) {
1244 window.expandSidebar();
1245 window.updateStickyHeader();
1246
1247 } else {
1248 window.collapseSidebar();
1249 window.updateStickyHeader();
1250 }
1251 }
1252 })()
1253
1254 </script>
@@ -1,302 +1,431 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
6 6 <%namespace name="file_base" file="/files/base.mako"/>
7 <%namespace name="sidebar" file="/base/sidebar.mako"/>
8
7 9
8 10 <%def name="title()">
9 11 ${_('{} Commit').format(c.repo_name)} - ${h.show_id(c.commit)}
10 12 %if c.rhodecode_name:
11 13 &middot; ${h.branding(c.rhodecode_name)}
12 14 %endif
13 15 </%def>
14 16
15 17 <%def name="menu_bar_nav()">
16 18 ${self.menu_items(active='repositories')}
17 19 </%def>
18 20
19 21 <%def name="menu_bar_subnav()">
20 22 ${self.repo_menu(active='commits')}
21 23 </%def>
22 24
23 25 <%def name="main()">
24 26 <script type="text/javascript">
25 27 // TODO: marcink switch this to pyroutes
26 28 AJAX_COMMENT_DELETE_URL = "${h.route_path('repo_commit_comment_delete',repo_name=c.repo_name,commit_id=c.commit.raw_id,comment_id='__COMMENT_ID__')}";
27 29 templateContext.commit_data.commit_id = "${c.commit.raw_id}";
28 30 </script>
29 31
30 32 <div class="box">
31 33
32 34 <div class="summary">
33 35
34 36 <div class="fieldset">
35 37 <div class="left-content">
36 38 <%
37 39 rc_user = h.discover_user(c.commit.author_email)
38 40 %>
39 41 <div class="left-content-avatar">
40 42 ${base.gravatar(c.commit.author_email, 30, tooltip=(True if rc_user else False), user=rc_user)}
41 43 </div>
42 44
43 45 <div class="left-content-message">
44 46 <div class="fieldset collapsable-content no-hide" data-toggle="summary-details">
45 47 <div class="commit truncate-wrap">${h.urlify_commit_message(h.chop_at_smart(c.commit.message, '\n', suffix_if_chopped='...'), c.repo_name)}</div>
46 48 </div>
47 49
48 50 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none">
49 51 <div class="commit">${h.urlify_commit_message(c.commit.message,c.repo_name)}</div>
50 52 </div>
51 53
52 54 <div class="fieldset" data-toggle="summary-details">
53 55 <div class="">
54 56 <table>
55 57 <tr class="file_author">
56 58
57 59 <td>
58 60 <span class="user commit-author">${h.link_to_user(rc_user or c.commit.author)}</span>
59 61 <span class="commit-date">- ${h.age_component(c.commit.date)}</span>
60 62 </td>
61 63
62 64 <td>
63 65 ## second cell for consistency with files
64 66 </td>
65 67 </tr>
66 68 </table>
67 69 </div>
68 70 </div>
69 71
70 72 </div>
71 73 </div>
72 74
73 75 <div class="right-content">
74 76
75 77 <div data-toggle="summary-details">
76 78 <div class="tags tags-main">
77 79 <code><a href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">${h.show_id(c.commit)}</a></code>
78 80 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.commit.raw_id}" title="${_('Copy the full commit id')}"></i>
79 81 ${file_base.refs(c.commit)}
80 82
81 83 ## phase
82 84 % if hasattr(c.commit, 'phase') and getattr(c.commit, 'phase') != 'public':
83 85 <span class="tag phase-${c.commit.phase} tooltip" title="${_('Commit phase')}">
84 86 <i class="icon-info"></i>${c.commit.phase}
85 87 </span>
86 88 % endif
87 89
88 90 ## obsolete commits
89 91 % if getattr(c.commit, 'obsolete', False):
90 92 <span class="tag obsolete-${c.commit.obsolete} tooltip" title="${_('Evolve State')}">
91 93 ${_('obsolete')}
92 94 </span>
93 95 % endif
94 96
95 97 ## hidden commits
96 98 % if getattr(c.commit, 'hidden', False):
97 99 <span class="tag hidden-${c.commit.hidden} tooltip" title="${_('Evolve State')}">
98 100 ${_('hidden')}
99 101 </span>
100 102 % endif
101 103 </div>
102 104
103 %if c.statuses:
104 <div class="tag status-tag-${c.statuses[0]} pull-right">
105 <i class="icon-circle review-status-${c.statuses[0]}"></i>
106 <div class="pull-right">${h.commit_status_lbl(c.statuses[0])}</div>
107 </div>
108 %endif
109
110 </div>
111
112 </div>
113 </div>
114
115 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
116 <div class="left-label-summary">
117 <p>${_('Commit navigation')}:</p>
118 <div class="right-label-summary">
119 105 <span id="parent_link" class="tag tagtag">
120 106 <a href="#parentCommit" title="${_('Parent Commit')}"><i class="icon-left icon-no-margin"></i>${_('parent')}</a>
121 107 </span>
122 108
123 109 <span id="child_link" class="tag tagtag">
124 110 <a href="#childCommit" title="${_('Child Commit')}">${_('child')}<i class="icon-right icon-no-margin"></i></a>
125 111 </span>
112
126 113 </div>
114
127 115 </div>
128 116 </div>
129 117
130 118 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
131 119 <div class="left-label-summary">
132 120 <p>${_('Diff options')}:</p>
133 121 <div class="right-label-summary">
134 122 <div class="diff-actions">
135 123 <a href="${h.route_path('repo_commit_raw',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
136 124 ${_('Raw Diff')}
137 125 </a>
138 126 |
139 127 <a href="${h.route_path('repo_commit_patch',repo_name=c.repo_name,commit_id=c.commit.raw_id)}">
140 128 ${_('Patch Diff')}
141 129 </a>
142 130 |
143 131 <a href="${h.route_path('repo_commit_download',repo_name=c.repo_name,commit_id=c.commit.raw_id,_query=dict(diff='download'))}">
144 132 ${_('Download Diff')}
145 133 </a>
146 134 </div>
147 135 </div>
148 136 </div>
149 137 </div>
150 138
151 139 <div class="clear-fix"></div>
152 140
153 141 <div class="btn-collapse" data-toggle="summary-details">
154 142 ${_('Show More')}
155 143 </div>
156 144
157 145 </div>
158 146
159 147 <div class="cs_files">
160 148 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 149 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
162 150 ${cbdiffs.render_diffset(
163 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
151 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,
152 inline_comments=c.inline_comments,
153 show_todos=False)}
164 154 </div>
165 155
166 156 ## template for inline comment form
167 157 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
168 158
169 159 ## comments heading with count
170 160 <div class="comments-heading">
171 161 <i class="icon-comment"></i>
172 ${_('Comments')} ${len(c.comments)}
162 ${_('General Comments')} ${len(c.comments)}
173 163 </div>
174 164
175 165 ## render comments
176 166 ${comment.generate_comments(c.comments)}
177 167
178 168 ## main comment form and it status
179 169 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id=c.commit.raw_id),
180 170 h.commit_status(c.rhodecode_db_repo, c.commit.raw_id))}
181 171 </div>
182 172
183 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
184 <script type="text/javascript">
173 ### NAV SIDEBAR
174 <aside class="right-sidebar right-sidebar-expanded" id="commit-nav-sticky" style="display: none">
175 <div class="sidenav navbar__inner" >
176 ## TOGGLE
177 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
178 <a href="#toggleSidebar" class="grey-link-action">
179
180 </a>
181 </div>
182
183 ## CONTENT
184 <div class="sidebar-content">
185 185
186 $(document).ready(function() {
186 ## RULES SUMMARY/RULES
187 <div class="sidebar-element clear-both">
188 <% vote_title = _ungettext(
189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
191 %>
192
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
196 </div>
197 </div>
187 198
188 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
189 if($('#trimmed_message_box').height() === boxmax){
190 $('#message_expand').show();
191 }
199 ## REVIEWERS
200 <div class="right-sidebar-expanded-state pr-details-title">
201 <span class="tooltip sidebar-heading" title="${vote_title}">
202 <i class="icon-circle review-status-${c.commit_review_status}"></i>
203 ${_('Reviewers')}
204 </span>
205 </div>
206
207 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
208
209 <table id="review_members" class="group_members">
210 ## This content is loaded via JS and ReviewersPanel
211 </table>
212
213 </div>
214
215 ## TODOs
216 <div class="sidebar-element clear-both">
217 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
218 <i class="icon-flag-filled"></i>
219 <span id="todos-count">${len(c.unresolved_comments)}</span>
220 </div>
221
222 <div class="right-sidebar-expanded-state pr-details-title">
223 ## Only show unresolved, that is only what matters
224 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
225 <i class="icon-flag-filled"></i>
226 TODOs
227 </span>
228
229 % if c.resolved_comments:
230 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
231 % else:
232 <span class="block-right last-item noselect">Show resolved</span>
233 % endif
234
235 </div>
192 236
193 $('#message_expand').on('click', function(e){
194 $('#trimmed_message_box').css('max-height', 'none');
195 $(this).hide();
196 });
237 <div class="right-sidebar-expanded-state pr-details-content">
238 % if c.unresolved_comments + c.resolved_comments:
239 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True, is_pr=False)}
240 % else:
241 <table>
242 <tr>
243 <td>
244 ${_('No TODOs yet')}
245 </td>
246 </tr>
247 </table>
248 % endif
249 </div>
250 </div>
251
252 ## COMMENTS
253 <div class="sidebar-element clear-both">
254 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
255 <i class="icon-comment" style="color: #949494"></i>
256 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
257 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
258 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
259 </div>
260
261 <div class="right-sidebar-expanded-state pr-details-title">
262 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
263 <i class="icon-comment" style="color: #949494"></i>
264 ${_('Comments')}
265 </span>
266
267 </div>
197 268
198 $('.show-inline-comments').on('click', function(e){
199 var boxid = $(this).attr('data-comment-id');
200 var button = $(this);
269 <div class="right-sidebar-expanded-state pr-details-content">
270 % if c.inline_comments_flat + c.comments:
271 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments), is_pr=False)}
272 % else:
273 <table>
274 <tr>
275 <td>
276 ${_('No Comments yet')}
277 </td>
278 </tr>
279 </table>
280 % endif
281 </div>
282
283 </div>
284
285 </div>
286
287 </div>
288 </aside>
201 289
202 if(button.hasClass("comments-visible")) {
203 $('#{0} .inline-comments'.format(boxid)).each(function(index){
204 $(this).hide();
290 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
291 <script type="text/javascript">
292 window.setReviewersData = ${c.commit_set_reviewers_data_json | n};
293
294 $(document).ready(function () {
295 var boxmax = parseInt($('#trimmed_message_box').css('max-height'), 10);
296
297 if ($('#trimmed_message_box').height() === boxmax) {
298 $('#message_expand').show();
299 }
300
301 $('#message_expand').on('click', function (e) {
302 $('#trimmed_message_box').css('max-height', 'none');
303 $(this).hide();
304 });
305
306 $('.show-inline-comments').on('click', function (e) {
307 var boxid = $(this).attr('data-comment-id');
308 var button = $(this);
309
310 if (button.hasClass("comments-visible")) {
311 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
312 $(this).hide();
205 313 });
206 314 button.removeClass("comments-visible");
207 } else {
208 $('#{0} .inline-comments'.format(boxid)).each(function(index){
209 $(this).show();
315 } else {
316 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
317 $(this).show();
210 318 });
211 319 button.addClass("comments-visible");
212 }
213 });
320 }
321 });
214 322
215 // next links
216 $('#child_link').on('click', function(e){
217 // fetch via ajax what is going to be the next link, if we have
218 // >1 links show them to user to choose
219 if(!$('#child_link').hasClass('disabled')){
220 $.ajax({
323 // next links
324 $('#child_link').on('click', function (e) {
325 // fetch via ajax what is going to be the next link, if we have
326 // >1 links show them to user to choose
327 if (!$('#child_link').hasClass('disabled')) {
328 $.ajax({
221 329 url: '${h.route_path('repo_commit_children',repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
222 success: function(data) {
223 if(data.results.length === 0){
224 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
225 }
226 if(data.results.length === 1){
227 var commit = data.results[0];
228 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
229 }
230 else if(data.results.length === 2){
231 $('#child_link').addClass('disabled');
232 $('#child_link').addClass('double');
330 success: function (data) {
331 if (data.results.length === 0) {
332 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
333 }
334 if (data.results.length === 1) {
335 var commit = data.results[0];
336 window.location = pyroutes.url('repo_commit', {
337 'repo_name': '${c.repo_name}',
338 'commit_id': commit.raw_id
339 });
340 } else if (data.results.length === 2) {
341 $('#child_link').addClass('disabled');
342 $('#child_link').addClass('double');
233 343
234 var _html = '';
235 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
236 .replace('__branch__', data.results[0].branch)
237 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
238 .replace('__title__', data.results[0].message)
239 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
240 _html +=' | ';
241 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
242 .replace('__branch__', data.results[1].branch)
243 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
244 .replace('__title__', data.results[1].message)
245 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
246 $('#child_link').html(_html);
247 }
344 var _html = '';
345 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
346 .replace('__branch__', data.results[0].branch)
347 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
348 .replace('__title__', data.results[0].message)
349 .replace('__url__', pyroutes.url('repo_commit', {
350 'repo_name': '${c.repo_name}',
351 'commit_id': data.results[0].raw_id
352 }));
353 _html += ' | ';
354 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a> '
355 .replace('__branch__', data.results[1].branch)
356 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
357 .replace('__title__', data.results[1].message)
358 .replace('__url__', pyroutes.url('repo_commit', {
359 'repo_name': '${c.repo_name}',
360 'commit_id': data.results[1].raw_id
361 }));
362 $('#child_link').html(_html);
363 }
248 364 }
249 });
250 e.preventDefault();
251 }
252 });
365 });
366 e.preventDefault();
367 }
368 });
253 369
254 // prev links
255 $('#parent_link').on('click', function(e){
256 // fetch via ajax what is going to be the next link, if we have
257 // >1 links show them to user to choose
258 if(!$('#parent_link').hasClass('disabled')){
259 $.ajax({
370 // prev links
371 $('#parent_link').on('click', function (e) {
372 // fetch via ajax what is going to be the next link, if we have
373 // >1 links show them to user to choose
374 if (!$('#parent_link').hasClass('disabled')) {
375 $.ajax({
260 376 url: '${h.route_path("repo_commit_parents",repo_name=c.repo_name, commit_id=c.commit.raw_id)}',
261 success: function(data) {
262 if(data.results.length === 0){
263 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
264 }
265 if(data.results.length === 1){
266 var commit = data.results[0];
267 window.location = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': commit.raw_id});
268 }
269 else if(data.results.length === 2){
270 $('#parent_link').addClass('disabled');
271 $('#parent_link').addClass('double');
377 success: function (data) {
378 if (data.results.length === 0) {
379 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
380 }
381 if (data.results.length === 1) {
382 var commit = data.results[0];
383 window.location = pyroutes.url('repo_commit', {
384 'repo_name': '${c.repo_name}',
385 'commit_id': commit.raw_id
386 });
387 } else if (data.results.length === 2) {
388 $('#parent_link').addClass('disabled');
389 $('#parent_link').addClass('double');
272 390
273 var _html = '';
274 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
275 .replace('__branch__', data.results[0].branch)
276 .replace('__rev__','r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0,6)))
277 .replace('__title__', data.results[0].message)
278 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[0].raw_id}));
279 _html +=' | ';
280 _html +='<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
281 .replace('__branch__', data.results[1].branch)
282 .replace('__rev__','r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0,6)))
283 .replace('__title__', data.results[1].message)
284 .replace('__url__', pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}','commit_id': data.results[1].raw_id}));
285 $('#parent_link').html(_html);
286 }
391 var _html = '';
392 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
393 .replace('__branch__', data.results[0].branch)
394 .replace('__rev__', 'r{0}:{1}'.format(data.results[0].revision, data.results[0].raw_id.substr(0, 6)))
395 .replace('__title__', data.results[0].message)
396 .replace('__url__', pyroutes.url('repo_commit', {
397 'repo_name': '${c.repo_name}',
398 'commit_id': data.results[0].raw_id
399 }));
400 _html += ' | ';
401 _html += '<a title="__title__" href="__url__"><span class="tag branchtag"><i class="icon-code-fork"></i>__branch__</span> __rev__</a>'
402 .replace('__branch__', data.results[1].branch)
403 .replace('__rev__', 'r{0}:{1}'.format(data.results[1].revision, data.results[1].raw_id.substr(0, 6)))
404 .replace('__title__', data.results[1].message)
405 .replace('__url__', pyroutes.url('repo_commit', {
406 'repo_name': '${c.repo_name}',
407 'commit_id': data.results[1].raw_id
408 }));
409 $('#parent_link').html(_html);
410 }
287 411 }
288 });
289 e.preventDefault();
290 }
291 });
412 });
413 e.preventDefault();
414 }
415 });
292 416
293 // browse tree @ revision
294 $('#files_link').on('click', function(e){
295 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
296 e.preventDefault();
297 });
417 // browse tree @ revision
418 $('#files_link').on('click', function (e) {
419 window.location = '${h.route_path('repo_files:default_path',repo_name=c.repo_name, commit_id=c.commit.raw_id)}';
420 e.preventDefault();
421 });
298 422
299 })
300 </script>
423 ReviewersPanel.init(null, setReviewersData);
424
425 var channel = '${c.commit_broadcast_channel}';
426 new ReviewerPresenceController(channel)
427
428 })
429 </script>
301 430
302 431 </%def>
@@ -1,493 +1,518 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6
7 7 <%!
8 8 from rhodecode.lib import html_filters
9 9 %>
10 10
11 11 <%namespace name="base" file="/base/base.mako"/>
12 12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 13
14 <%
15 from rhodecode.model.comment import CommentsModel
16 comment_model = CommentsModel()
17 %>
14 18 <% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
15 19 <% latest_ver = len(getattr(c, 'versions', [])) %>
16 20
17 21 % if inline:
18 22 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
19 23 % else:
20 24 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
21 25 % endif
22 26
23 27 <div class="comment
24 28 ${'comment-inline' if inline else 'comment-general'}
25 29 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
26 30 id="comment-${comment.comment_id}"
27 31 line="${comment.line_no}"
28 32 data-comment-id="${comment.comment_id}"
29 33 data-comment-type="${comment.comment_type}"
30 34 data-comment-renderer="${comment.renderer}"
31 35 data-comment-text="${comment.text | html_filters.base64,n}"
32 36 data-comment-line-no="${comment.line_no}"
33 37 data-comment-inline=${h.json.dumps(inline)}
34 38 style="${'display: none;' if outdated_at_ver else ''}">
35 39
36 40 <div class="meta">
37 41 <div class="comment-type-label">
38 42 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
39 43
40 44 ## TODO COMMENT
41 45 % if comment.comment_type == 'todo':
42 46 % if comment.resolved:
43 47 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
44 48 <i class="icon-flag-filled"></i>
45 49 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
46 50 </div>
47 51 % else:
48 52 <div class="resolved tooltip" style="display: none">
49 53 <span>${comment.comment_type}</span>
50 54 </div>
51 55 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
52 56 <i class="icon-flag-filled"></i>
53 57 ${comment.comment_type}
54 58 </div>
55 59 % endif
56 60 ## NOTE COMMENT
57 61 % else:
58 62 ## RESOLVED NOTE
59 63 % if comment.resolved_comment:
60 64 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
61 65 fix
62 66 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
63 67 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
64 68 </a>
65 69 </div>
66 70 ## STATUS CHANGE NOTE
67 71 % elif not comment.is_inline and comment.status_change:
68 72 <%
69 73 if comment.pull_request:
70 74 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
71 75 else:
72 76 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
73 77 %>
74 78
75 79 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
76 80 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
77 81 ${comment.status_change[0].status_lbl}
78 82 </div>
79 83 % else:
80 84 <div>
81 85 <i class="icon-comment"></i>
82 86 ${(comment.comment_type or 'note')}
83 87 </div>
84 88 % endif
85 89 % endif
86 90
87 91 </div>
88 92 </div>
89 93
90 94 % if 0 and comment.status_change:
91 95 <div class="pull-left">
92 96 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
93 97 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
94 98 ${'!{}'.format(comment.pull_request.pull_request_id)}
95 99 </a>
96 100 </span>
97 101 </div>
98 102 % endif
99 103
100 104 <div class="author ${'author-inline' if inline else 'author-general'}">
101 105 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
102 106 </div>
103 107
104 108 <div class="date">
105 109 ${h.age_component(comment.modified_at, time_is_local=True)}
106 110 </div>
107 111
108 112 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
109 113 <span class="tag authortag tooltip" title="${_('Pull request author')}">
110 114 ${_('author')}
111 115 </span>
112 116 % endif
113 117
114 118 <%
115 119 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
116 120 %>
117 121
118 122 % if comment.history:
119 123 <div class="date">
120 124
121 125 <input id="${comment_version_selector}" name="${comment_version_selector}"
122 126 type="hidden"
123 127 data-last-version="${comment.history[-1].version}">
124 128
125 129 <script type="text/javascript">
126 130
127 131 var preLoadVersionData = [
128 132 % for comment_history in comment.history:
129 133 {
130 134 id: ${comment_history.comment_history_id},
131 135 text: 'v${comment_history.version}',
132 136 action: function () {
133 137 Rhodecode.comments.showVersion(
134 138 "${comment.comment_id}",
135 139 "${comment_history.comment_history_id}"
136 140 )
137 141 },
138 142 comment_version: "${comment_history.version}",
139 143 comment_author_username: "${comment_history.author.username}",
140 144 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
141 145 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
142 146 },
143 147 % endfor
144 148 ]
145 149 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
146 150
147 151 </script>
148 152
149 153 </div>
150 154 % else:
151 155 <div class="date" style="display: none">
152 156 <input id="${comment_version_selector}" name="${comment_version_selector}"
153 157 type="hidden"
154 158 data-last-version="0">
155 159 </div>
156 160 %endif
157 161
158 <a class="permalink" href="#comment-${comment.comment_id}">&para; #${comment.comment_id}</a>
159
160 162 <div class="comment-links-block">
161 163
162 164 % if inline:
163 165 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
164 166 % if outdated_at_ver:
165 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">
166 outdated ${'v{}'.format(comment_ver)} |
167 </code>
167 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
168 <code class="action-divider">|</code>
168 169 % elif comment_ver:
169 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">
170 ${'v{}'.format(comment_ver)} |
171 </code>
170 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
171 <code class="action-divider">|</code>
172 172 % endif
173 173 </a>
174 174 % else:
175 175 % if comment_ver:
176 176
177 177 % if comment.outdated:
178 178 <a class="pr-version"
179 179 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
180 180 >
181 181 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
182 </a> |
182 </a>
183 <code class="action-divider">|</code>
183 184 % else:
184 185 <a class="tooltip pr-version"
185 186 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
186 187 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
187 188 >
188 <code class="pr-version-num">
189 ${'v{}'.format(comment_ver)}
190 </code>
191 </a> |
189 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
190 </a>
191 <code class="action-divider">|</code>
192 192 % endif
193 193
194 194 % endif
195 195 % endif
196 196
197 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
198 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
199 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
200 ## permissions to delete
201 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
202 <a onclick="return Rhodecode.comments.editComment(this);"
203 class="edit-comment">${_('Edit')}</a>
204 | <a onclick="return Rhodecode.comments.deleteComment(this);"
205 class="delete-comment">${_('Delete')}</a>
206 %else:
207 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
208 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
209 %endif
210 %else:
211 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
212 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
213 %endif
197 <details class="details-reset details-inline-block">
198 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
199 <details-menu class="details-dropdown">
200
201 <div class="dropdown-item">
202 ${_('Comment')} #${comment.comment_id}
203 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
204 </div>
214 205
206 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
207 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
208 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
209 ## permissions to delete
210 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
211 <div class="dropdown-divider"></div>
212 <div class="dropdown-item">
213 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
214 </div>
215 <div class="dropdown-item">
216 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
217 </div>
218 %else:
219 <div class="dropdown-divider"></div>
220 <div class="dropdown-item">
221 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
222 </div>
223 <div class="dropdown-item">
224 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
225 </div>
226 %endif
227 %else:
228 <div class="dropdown-divider"></div>
229 <div class="dropdown-item">
230 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
231 </div>
232 <div class="dropdown-item">
233 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
234 </div>
235 %endif
236 </details-menu>
237 </details>
238
239 <code class="action-divider">|</code>
215 240 % if outdated_at_ver:
216 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
217 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
241 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
242 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
218 243 % else:
219 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
220 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
244 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
245 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
221 246 % endif
222 247
223 248 </div>
224 249 </div>
225 250 <div class="text">
226 251 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
227 252 </div>
228 253
229 254 </div>
230 255 </%def>
231 256
232 257 ## generate main comments
233 258 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
234 259 <%
235 260 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
236 261 %>
237 262
238 263 <div class="general-comments" id="comments">
239 264 %for comment in comments:
240 265 <div id="comment-tr-${comment.comment_id}">
241 266 ## only render comments that are not from pull request, or from
242 267 ## pull request and a status change
243 268 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
244 269 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
245 270 %endif
246 271 </div>
247 272 %endfor
248 273 ## to anchor ajax comments
249 274 <div id="injected_page_comments"></div>
250 275 </div>
251 276 </%def>
252 277
253 278
254 279 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
255 280
256 281 <div class="comments">
257 282 <%
258 283 if is_pull_request:
259 284 placeholder = _('Leave a comment on this Pull Request.')
260 285 elif is_compare:
261 286 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
262 287 else:
263 288 placeholder = _('Leave a comment on this Commit.')
264 289 %>
265 290
266 291 % if c.rhodecode_user.username != h.DEFAULT_USER:
267 292 <div class="js-template" id="cb-comment-general-form-template">
268 293 ## template generated for injection
269 294 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
270 295 </div>
271 296
272 297 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
273 298 ## inject form here
274 299 </div>
275 300 <script type="text/javascript">
276 301 var lineNo = 'general';
277 302 var resolvesCommentId = null;
278 303 var generalCommentForm = Rhodecode.comments.createGeneralComment(
279 304 lineNo, "${placeholder}", resolvesCommentId);
280 305
281 306 // set custom success callback on rangeCommit
282 307 % if is_compare:
283 308 generalCommentForm.setHandleFormSubmit(function(o) {
284 309 var self = generalCommentForm;
285 310
286 311 var text = self.cm.getValue();
287 312 var status = self.getCommentStatus();
288 313 var commentType = self.getCommentType();
289 314
290 315 if (text === "" && !status) {
291 316 return;
292 317 }
293 318
294 319 // we can pick which commits we want to make the comment by
295 320 // selecting them via click on preview pane, this will alter the hidden inputs
296 321 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
297 322
298 323 var commitIds = [];
299 324 $('#changeset_compare_view_content .compare_select').each(function(el) {
300 325 var commitId = this.id.replace('row-', '');
301 326 if ($(this).hasClass('hl') || !cherryPicked) {
302 327 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
303 328 commitIds.push(commitId);
304 329 } else {
305 330 $("input[data-commit-id='{0}']".format(commitId)).val('')
306 331 }
307 332 });
308 333
309 334 self.setActionButtonsDisabled(true);
310 335 self.cm.setOption("readOnly", true);
311 336 var postData = {
312 337 'text': text,
313 338 'changeset_status': status,
314 339 'comment_type': commentType,
315 340 'commit_ids': commitIds,
316 341 'csrf_token': CSRF_TOKEN
317 342 };
318 343
319 344 var submitSuccessCallback = function(o) {
320 345 location.reload(true);
321 346 };
322 347 var submitFailCallback = function(){
323 348 self.resetCommentFormState(text)
324 349 };
325 350 self.submitAjaxPOST(
326 351 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
327 352 });
328 353 % endif
329 354
330 355 </script>
331 356 % else:
332 357 ## form state when not logged in
333 358 <div class="comment-form ac">
334 359
335 360 <div class="comment-area">
336 361 <div class="comment-area-header">
337 362 <ul class="nav-links clearfix">
338 363 <li class="active">
339 364 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
340 365 </li>
341 366 <li class="">
342 367 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
343 368 </li>
344 369 </ul>
345 370 </div>
346 371
347 372 <div class="comment-area-write" style="display: block;">
348 373 <div id="edit-container">
349 374 <div style="padding: 40px 0">
350 375 ${_('You need to be logged in to leave comments.')}
351 376 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
352 377 </div>
353 378 </div>
354 379 <div id="preview-container" class="clearfix" style="display: none;">
355 380 <div id="preview-box" class="preview-box"></div>
356 381 </div>
357 382 </div>
358 383
359 384 <div class="comment-area-footer">
360 385 <div class="toolbar">
361 386 <div class="toolbar-text">
362 387 </div>
363 388 </div>
364 389 </div>
365 390 </div>
366 391
367 392 <div class="comment-footer">
368 393 </div>
369 394
370 395 </div>
371 396 % endif
372 397
373 398 <script type="text/javascript">
374 399 bindToggleButtons();
375 400 </script>
376 401 </div>
377 402 </%def>
378 403
379 404
380 405 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
381 406
382 407 ## comment injected based on assumption that user is logged in
383 408 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
384 409
385 410 <div class="comment-area">
386 411 <div class="comment-area-header">
387 412 <div class="pull-left">
388 413 <ul class="nav-links clearfix">
389 414 <li class="active">
390 415 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
391 416 </li>
392 417 <li class="">
393 418 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
394 419 </li>
395 420 </ul>
396 421 </div>
397 422 <div class="pull-right">
398 423 <span class="comment-area-text">${_('Mark as')}:</span>
399 424 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
400 425 % for val in c.visual.comment_types:
401 426 <option value="${val}">${val.upper()}</option>
402 427 % endfor
403 428 </select>
404 429 </div>
405 430 </div>
406 431
407 432 <div class="comment-area-write" style="display: block;">
408 433 <div id="edit-container_${lineno_id}">
409 434 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
410 435 </div>
411 436 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
412 437 <div id="preview-box_${lineno_id}" class="preview-box"></div>
413 438 </div>
414 439 </div>
415 440
416 441 <div class="comment-area-footer comment-attachment-uploader">
417 442 <div class="toolbar">
418 443
419 444 <div class="comment-attachment-text">
420 445 <div class="dropzone-text">
421 446 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
422 447 </div>
423 448 <div class="dropzone-upload" style="display:none">
424 449 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
425 450 </div>
426 451 </div>
427 452
428 453 ## comments dropzone template, empty on purpose
429 454 <div style="display: none" class="comment-attachment-uploader-template">
430 455 <div class="dz-file-preview" style="margin: 0">
431 456 <div class="dz-error-message"></div>
432 457 </div>
433 458 </div>
434 459
435 460 </div>
436 461 </div>
437 462 </div>
438 463
439 464 <div class="comment-footer">
440 465
441 466 ## inject extra inputs into the form
442 467 % if form_extras and isinstance(form_extras, (list, tuple)):
443 468 <div id="comment_form_extras">
444 469 % for form_ex_el in form_extras:
445 470 ${form_ex_el|n}
446 471 % endfor
447 472 </div>
448 473 % endif
449 474
450 475 <div class="action-buttons">
451 476 % if form_type != 'inline':
452 477 <div class="action-buttons-extra"></div>
453 478 % endif
454 479
455 480 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
456 481
457 482 ## inline for has a file, and line-number together with cancel hide button.
458 483 % if form_type == 'inline':
459 484 <input type="hidden" name="f_path" value="{0}">
460 485 <input type="hidden" name="line" value="${lineno_id}">
461 486 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
462 487 ${_('Cancel')}
463 488 </button>
464 489 % endif
465 490 </div>
466 491
467 492 % if review_statuses:
468 493 <div class="status_box">
469 494 <select id="change_status_${lineno_id}" name="changeset_status">
470 495 <option></option> ## Placeholder
471 496 % for status, lbl in review_statuses:
472 497 <option value="${status}" data-status="${status}">${lbl}</option>
473 498 %if is_pull_request and change_status and status in ('approved', 'rejected'):
474 499 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
475 500 %endif
476 501 % endfor
477 502 </select>
478 503 </div>
479 504 % endif
480 505
481 506 <div class="toolbar-text">
482 507 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
483 508 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
484 509 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
485 510 ${_('and')}
486 511 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
487 512 ${_('actions supported.')}
488 513 </div>
489 514 </div>
490 515
491 516 </form>
492 517
493 518 </%def> No newline at end of file
@@ -1,116 +1,121 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Commits') % c.repo_name} -
6 6 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
7 7 ...
8 8 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
9 9 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
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()"></%def>
16 16
17 17 <%def name="menu_bar_nav()">
18 18 ${self.menu_items(active='repositories')}
19 19 </%def>
20 20
21 21 <%def name="menu_bar_subnav()">
22 22 ${self.repo_menu(active='commits')}
23 23 </%def>
24 24
25 25 <%def name="main()">
26 26
27 27 <div class="box">
28 28 <div class="summary changeset">
29 29 <div class="summary-detail">
30 30 <div class="summary-detail-header">
31 31 <span class="breadcrumbs files_location">
32 32 <h4>
33 33 ${_('Commit Range')}
34 34 </h4>
35 35 </span>
36 36
37 37 <div class="clear-fix"></div>
38 38 </div>
39 39
40 40 <div class="fieldset">
41 41 <div class="left-label-summary">
42 42 <p class="spacing">${_('Range')}:</p>
43 43 <div class="right-label-summary">
44 44 <div class="code-header" >
45 45 <div class="compare_header">
46 46 <code class="fieldset-text-line">
47 47 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}
48 48 ...
49 49 r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
50 50 ${_ungettext('(%s commit)','(%s commits)', len(c.commit_ranges)) % len(c.commit_ranges)}
51 51 </code>
52 52 </div>
53 53 </div>
54 54 </div>
55 55 </div>
56 56 </div>
57 57
58 58 <div class="fieldset">
59 59 <div class="left-label-summary">
60 60 <p class="spacing">${_('Diff Option')}:</p>
61 61 <div class="right-label-summary">
62 62 <div class="code-header" >
63 63 <div class="compare_header">
64 64 <a class="btn btn-primary" href="${h.route_path('repo_compare',
65 65 repo_name=c.repo_name,
66 66 source_ref_type='rev',
67 67 source_ref=getattr(c.commit_ranges[0].parents[0] if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id'),
68 68 target_ref_type='rev',
69 69 target_ref=c.commit_ranges[-1].raw_id)}"
70 70 >
71 71 ${_('Show combined diff')}
72 72 </a>
73 73 </div>
74 74 </div>
75 75 </div>
76 76 </div>
77 77 </div>
78 78
79 79 <div class="clear-fix"></div>
80 80 </div> <!-- end summary-detail -->
81 81 </div> <!-- end summary -->
82 82
83 83 <div id="changeset_compare_view_content">
84 84 <div class="pull-left">
85 85 <div class="btn-group">
86 86 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
87 87 % if c.collapse_all_commits:
88 88 <i class="icon-plus-squared-alt icon-no-margin"></i>
89 89 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
90 90 % else:
91 91 <i class="icon-minus-squared-alt icon-no-margin"></i>
92 92 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
93 93 % endif
94 94 </a>
95 95 </div>
96 96 </div>
97 97 ## Commit range generated below
98 98 <%include file="../compare/compare_commits.mako"/>
99 99 <div class="cs_files">
100 100 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
101 101 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
102 102 <%namespace name="diff_block" file="/changeset/diff_block.mako"/>
103 103
104 104 %for commit in c.commit_ranges:
105 ## commit range header for each individual diff
106 <h3>
107 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=c.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
108 </h3>
109
105 110 ${cbdiffs.render_diffset_menu(c.changes[commit.raw_id])}
106 111 ${cbdiffs.render_diffset(
107 112 diffset=c.changes[commit.raw_id],
108 113 collapse_when_files_over=5,
109 114 commit=commit,
110 115 )}
111 116 %endfor
112 117 </div>
113 118 </div>
114 119 </div>
115 120
116 121 </%def>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now